commit af2562096a14acb32cd4de96df6edb611c51d539 Author: michael Date: Sun Aug 24 16:28:33 2025 -0500 initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..bcd98cd Binary files /dev/null and b/.DS_Store differ diff --git a/.github/workflows/cd-backend.yml b/.github/workflows/cd-backend.yml new file mode 100644 index 0000000..981bdc7 --- /dev/null +++ b/.github/workflows/cd-backend.yml @@ -0,0 +1,182 @@ +name: Deploy Backend + +on: + push: + branches: [ main ] + paths: + - 'backend/**' + - '.github/workflows/cd-backend.yml' + workflow_dispatch: + +env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_REGION: us-central1 + SERVICE_NAME: accessible-video-api + WORKER_SERVICE_NAME: accessible-video-worker + +jobs: + deploy-api: + name: Deploy API to Cloud Run + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.WIF_PROVIDER }} + service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker auth + run: gcloud auth configure-docker + + - name: Build and push Docker image + working-directory: ./backend + run: | + # Build image with multi-stage optimization + docker build \ + --target production \ + --tag gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.SERVICE_NAME }}:${{ github.sha }} \ + --tag gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.SERVICE_NAME }}:latest \ + . + + # Push images + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.SERVICE_NAME }}:${{ github.sha }} + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.SERVICE_NAME }}:latest + + - name: Deploy to Cloud Run + run: | + gcloud run deploy ${{ env.SERVICE_NAME }} \ + --image gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.SERVICE_NAME }}:${{ github.sha }} \ + --region ${{ env.GCP_REGION }} \ + --platform managed \ + --allow-unauthenticated \ + --set-env-vars APP_ENV=prod \ + --set-secrets JWT_SECRET=jwt-secret:latest,MONGODB_URI=mongodb-uri:latest,REDIS_URL=redis-url:latest,GEMINI_API_KEY=gemini-api-key:latest,SENDGRID_API_KEY=sendgrid-api-key:latest,SENTRY_DSN=sentry-dsn:latest \ + --memory 2Gi \ + --cpu 2 \ + --max-instances 100 \ + --min-instances 1 \ + --port 8000 \ + --timeout 300 \ + --concurrency 80 + + - name: Update traffic to new revision + run: | + gcloud run services update-traffic ${{ env.SERVICE_NAME }} \ + --region ${{ env.GCP_REGION }} \ + --to-latest + + deploy-worker: + name: Deploy Worker to Cloud Run + runs-on: ubuntu-latest + needs: [deploy-api] + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.WIF_PROVIDER }} + service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker auth + run: gcloud auth configure-docker + + - name: Build and push worker image + working-directory: ./backend + run: | + # Build worker image + docker build \ + --target worker \ + --tag gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.WORKER_SERVICE_NAME }}:${{ github.sha }} \ + --tag gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.WORKER_SERVICE_NAME }}:latest \ + . + + # Push images + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.WORKER_SERVICE_NAME }}:${{ github.sha }} + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.WORKER_SERVICE_NAME }}:latest + + - name: Deploy worker to Cloud Run + run: | + gcloud run deploy ${{ env.WORKER_SERVICE_NAME }} \ + --image gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.WORKER_SERVICE_NAME }}:${{ github.sha }} \ + --region ${{ env.GCP_REGION }} \ + --platform managed \ + --no-allow-unauthenticated \ + --set-env-vars APP_ENV=prod \ + --set-secrets JWT_SECRET=jwt-secret:latest,MONGODB_URI=mongodb-uri:latest,REDIS_URL=redis-url:latest,GEMINI_API_KEY=gemini-api-key:latest,SENDGRID_API_KEY=sendgrid-api-key:latest,SENTRY_DSN=sentry-dsn:latest \ + --memory 4Gi \ + --cpu 2 \ + --max-instances 50 \ + --min-instances 0 \ + --timeout 1800 \ + --concurrency 1 + + smoke-tests: + name: Run Smoke Tests + runs-on: ubuntu-latest + needs: [deploy-api, deploy-worker] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Install dependencies + working-directory: ./backend + run: poetry install + + - name: Run smoke tests against production + working-directory: ./backend + env: + API_BASE_URL: https://${{ env.SERVICE_NAME }}-${{ secrets.GCP_REGION_HASH }}-uc.a.run.app + SMOKE_TEST_EMAIL: ${{ secrets.SMOKE_TEST_EMAIL }} + SMOKE_TEST_PASSWORD: ${{ secrets.SMOKE_TEST_PASSWORD }} + run: | + poetry run pytest tests/e2e/test_smoke.py -v + + notify-deployment: + name: Notify Deployment Status + runs-on: ubuntu-latest + needs: [smoke-tests] + if: always() + + steps: + - name: Notify success + if: needs.smoke-tests.result == 'success' + run: | + echo "✅ Backend deployment completed successfully" + # Add Slack/email notification here if needed + + - name: Notify failure + if: needs.smoke-tests.result == 'failure' + run: | + echo "❌ Backend deployment failed" + # Add Slack/email notification here if needed \ No newline at end of file diff --git a/.github/workflows/cd-frontend.yml b/.github/workflows/cd-frontend.yml new file mode 100644 index 0000000..f5b816d --- /dev/null +++ b/.github/workflows/cd-frontend.yml @@ -0,0 +1,147 @@ +name: Deploy Frontend + +on: + push: + branches: [ main ] + paths: + - 'frontend/**' + - '.github/workflows/cd-frontend.yml' + workflow_dispatch: + +env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + GCP_REGION: us-central1 + BUCKET_NAME: ${{ secrets.FRONTEND_BUCKET_NAME }} + CDN_URL_MAP: accessible-video-frontend + NODE_VERSION: "20" + +jobs: + build-and-deploy: + name: Build and Deploy Frontend + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: ./frontend + run: npm ci + + - name: Build for production + working-directory: ./frontend + env: + VITE_API_BASE_URL: ${{ secrets.PRODUCTION_API_URL }} + VITE_APP_ENV: production + VITE_SENTRY_DSN: ${{ secrets.FRONTEND_SENTRY_DSN }} + run: npm run build + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.WIF_PROVIDER }} + service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }} + + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v2 + + - name: Deploy to Cloud Storage + working-directory: ./frontend + run: | + # Sync build files to Cloud Storage bucket + gsutil -m rsync -r -d dist/ gs://${{ env.BUCKET_NAME }}/ + + # Set public read permissions for web assets + gsutil -m acl ch -r -u AllUsers:R gs://${{ env.BUCKET_NAME }} + + # Set cache headers for different file types + gsutil -m setmeta -h "Cache-Control:public, max-age=31536000, immutable" "gs://${{ env.BUCKET_NAME }}/**/*.js" + gsutil -m setmeta -h "Cache-Control:public, max-age=31536000, immutable" "gs://${{ env.BUCKET_NAME }}/**/*.css" + gsutil -m setmeta -h "Cache-Control:public, max-age=86400" "gs://${{ env.BUCKET_NAME }}/**/*.html" + gsutil -m setmeta -h "Cache-Control:public, max-age=86400" "gs://${{ env.BUCKET_NAME }}/index.html" + + - name: Configure Load Balancer and CDN + run: | + # Create backend bucket if it doesn't exist + gcloud compute backend-buckets describe ${{ env.BUCKET_NAME }}-backend || \ + gcloud compute backend-buckets create ${{ env.BUCKET_NAME }}-backend \ + --gcs-bucket-name=${{ env.BUCKET_NAME }} + + # Update the URL map to route to the bucket + gcloud compute url-maps describe ${{ env.CDN_URL_MAP }} || \ + gcloud compute url-maps create ${{ env.CDN_URL_MAP }} \ + --default-backend-bucket=${{ env.BUCKET_NAME }}-backend + + # Create or update HTTPS proxy + gcloud compute target-https-proxies describe ${{ env.CDN_URL_MAP }}-https-proxy || \ + gcloud compute target-https-proxies create ${{ env.CDN_URL_MAP }}-https-proxy \ + --url-map=${{ env.CDN_URL_MAP }} \ + --ssl-certificates=${{ secrets.SSL_CERT_NAME }} + + # Create or update global forwarding rule + gcloud compute forwarding-rules describe ${{ env.CDN_URL_MAP }}-https-rule --global || \ + gcloud compute forwarding-rules create ${{ env.CDN_URL_MAP }}-https-rule \ + --global \ + --target-https-proxy=${{ env.CDN_URL_MAP }}-https-proxy \ + --ports=443 + + - name: Invalidate CDN cache + run: | + # Invalidate CDN cache for immediate deployment + gcloud compute url-maps invalidate-cdn-cache ${{ env.CDN_URL_MAP }} \ + --path="/*" \ + --async + + - name: Run smoke tests + working-directory: ./frontend + env: + FRONTEND_URL: https://${{ secrets.FRONTEND_DOMAIN }} + run: | + # Wait a bit for CDN propagation + sleep 30 + + # Basic smoke test - check if main page loads + curl -f -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL" | grep -q "200" || { + echo "Frontend smoke test failed - main page not accessible" + exit 1 + } + + # Check if assets are loading + curl -f -s -o /dev/null -w "%{http_code}" "$FRONTEND_URL/assets/" | grep -qE "(200|404)" || { + echo "Frontend smoke test failed - assets not accessible" + exit 1 + } + + echo "✅ Frontend smoke tests passed" + + notify-deployment: + name: Notify Deployment Status + runs-on: ubuntu-latest + needs: [build-and-deploy] + if: always() + + steps: + - name: Notify success + if: needs.build-and-deploy.result == 'success' + run: | + echo "✅ Frontend deployment completed successfully" + echo "Frontend is now live at: https://${{ secrets.FRONTEND_DOMAIN }}" + # Add Slack/email notification here if needed + + - name: Notify failure + if: needs.build-and-deploy.result == 'failure' + run: | + echo "❌ Frontend deployment failed" + # Add Slack/email notification here if needed \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f95c496 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,312 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +env: + PYTHON_VERSION: "3.11" + NODE_VERSION: "20" + +jobs: + backend-lint-and-test: + name: Backend Lint & Test + runs-on: ubuntu-latest + + services: + mongodb: + image: mongo:7.0 + ports: + - 27017:27017 + options: >- + --health-cmd "echo 'db.runCommand("ping").ok' | mongosh --quiet" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: latest + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Load cached dependencies + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: backend/.venv + key: poetry-${{ runner.os }}-${{ env.PYTHON_VERSION }}-${{ hashFiles('backend/poetry.lock') }} + + - name: Install dependencies + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + working-directory: ./backend + run: poetry install --no-interaction --no-root + + - name: Install project + working-directory: ./backend + run: poetry install --no-interaction + + - name: Run linting (ruff) + working-directory: ./backend + run: poetry run ruff check . + + - name: Run type checking (mypy) + working-directory: ./backend + run: poetry run mypy . + + - name: Run unit tests + working-directory: ./backend + env: + MONGODB_URI: mongodb://localhost:27017 + MONGODB_DB: test_accessible_video + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test_jwt_secret_for_ci + GEMINI_API_KEY: fake_key_for_testing + GCP_PROJECT_ID: test-project + GCS_BUCKET: test-bucket + SENDGRID_API_KEY: fake_sendgrid_key + EMAIL_FROM: test@example.com + CLIENT_BASE_URL: http://localhost:3000 + run: | + poetry run pytest tests/unit/ -v --cov=app --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./backend/coverage.xml + flags: backend + name: backend-coverage + + frontend-lint-and-test: + name: Frontend Lint & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: ./frontend + run: npm ci + + - name: Run linting (ESLint) + working-directory: ./frontend + run: npm run lint + + - name: Run type checking (TypeScript) + working-directory: ./frontend + run: npm run type-check + + - name: Run unit tests (Vitest) + working-directory: ./frontend + run: npm run test -- --coverage --reporter=verbose + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./frontend/coverage/lcov.info + flags: frontend + name: frontend-coverage + + integration-tests: + name: Integration Tests + runs-on: ubuntu-latest + needs: [backend-lint-and-test, frontend-lint-and-test] + + services: + mongodb: + image: mongo:7.0 + ports: + - 27017:27017 + options: >- + --health-cmd "echo 'db.runCommand("ping").ok' | mongosh --quiet" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Install backend dependencies + working-directory: ./backend + run: poetry install + + - name: Run integration tests + working-directory: ./backend + env: + MONGODB_URI: mongodb://localhost:27017 + MONGODB_DB: test_accessible_video_integration + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test_jwt_secret_for_integration + GEMINI_API_KEY: fake_key_for_testing + GCP_PROJECT_ID: test-project + GCS_BUCKET: test-bucket + SENDGRID_API_KEY: fake_sendgrid_key + EMAIL_FROM: test@example.com + CLIENT_BASE_URL: http://localhost:3000 + run: | + poetry run pytest tests/integration/ -v + + build-backend: + name: Build Backend Docker Image + runs-on: ubuntu-latest + needs: [backend-lint-and-test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build backend image + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + push: false + tags: accessible-video-backend:${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + build-frontend: + name: Build Frontend + runs-on: ubuntu-latest + needs: [frontend-lint-and-test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + working-directory: ./frontend + run: npm ci + + - name: Build for production + working-directory: ./frontend + env: + VITE_API_BASE_URL: https://api.example.com # Placeholder for production + VITE_APP_ENV: production + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: frontend/dist/ + retention-days: 7 + + security-scan: + name: Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + - name: Run Semgrep security scan + uses: semgrep/semgrep-action@v1 + with: + config: auto + generateBaseline: false + + dependency-check: + name: Dependency Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Check backend dependencies + working-directory: ./backend + run: | + poetry check + poetry run pip-audit + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Check frontend dependencies + working-directory: ./frontend + run: | + npm audit --audit-level moderate + npx better-npm-audit audit \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d838da9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +examples/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6b3358a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,148 @@ +# Accessible Video Processing Platform - Development Guide + +## Project Overview +This is a comprehensive video accessibility platform that automatically generates closed captions and audio descriptions using AI, with quality control workflows and multi-language support. + +**Core Tech Stack:** +- Frontend: React 18 + Vite SPA (TypeScript) +- Backend: FastAPI + Celery workers (Python 3.11+) +- Database: MongoDB Atlas +- Storage: Google Cloud Storage with signed URLs +- AI: Gemini 2.5 Pro, Google Cloud Translate, ElevenLabs TTS +- Queue: Redis + Celery +- Auth: JWT with HttpOnly refresh cookies + +## Development Instructions + +### CRITICAL: Always Read the Full Development Plan +**Before starting any development work, ALWAYS read the entire `video_accessibility_development_plan.txt` file.** This document contains: +- Complete technical specifications +- API contracts and schemas +- Database models and indexes +- Worker pipeline details +- Frontend component specifications +- Security requirements +- Testing strategies + +The development plan is the authoritative source for all implementation details. Refer to it frequently during development to ensure consistency with the overall architecture. + +## Key Implementation Phases + +### Phase 1: Foundation & Setup +- Monorepo structure (backend/, frontend/, infra/) +- FastAPI backend initialization +- React + Vite frontend setup +- MongoDB and Redis configuration +- JWT authentication with RBAC + +### Phase 2: Core Services +- Google Cloud Storage integration +- Gemini 2.5 Pro service +- Job model with state machine +- Celery worker infrastructure + +### Phase 3: Ingestion & AI Pipeline +- Video upload system +- Ingestion worker task +- VTT generation +- Gemini prompt system + +### Phase 4: Quality Control System +- VTT editor component +- QC dashboard for reviewers +- Approval/rejection workflow +- Video player with captions + +### Phase 5: Translation & TTS Pipeline +- Google Cloud Translate integration +- Transcreation system +- Translation worker +- TTS service integration + +### Phase 6: Final Review & Delivery +- Final review interface +- Job completion workflow +- Email notifications +- Client download portal + +### Phase 7: Production Readiness +- Comprehensive testing +- Security hardening +- Observability setup +- CI/CD configuration + +## Job Status State Machine +``` +created → ingesting → ai_processing → pending_qc → approved_english | rejected → translating → tts_generating → pending_final_review → completed +``` + +## Key Architecture Decisions + +### Security +- Access tokens stored in memory (not localStorage) +- Refresh tokens in HttpOnly cookies +- RBAC enforcement server-side +- Signed URLs for file access (24h expiry) +- Audit logs for all reviewer actions + +### Data Flow +1. Client uploads MP4 → GCS + MongoDB record +2. Celery worker processes video with Gemini 2.5 Pro +3. Generates English captions.vtt and audio_description.vtt +4. Reviewer QC approval triggers translation pipeline +5. Multi-language assets generated (translate/transcreate + TTS) +6. Final review and client notification with download links + +### File Structure +``` +gs://accessible-video/{jobId}/ + source.mp4 + en/ + captions.vtt + ad.vtt + ad.mp3 + {lang}/ + captions.vtt + ad.vtt + ad.mp3 +``` + +## Development Guidelines + +### Before Each Session +1. Read the complete `video_accessibility_development_plan.txt` +2. Review the current todo list and phase +3. Check existing code patterns and conventions +4. Understand the security and accessibility requirements + +### Code Standards +- Follow existing patterns in the codebase +- Implement proper error handling and retries +- Add OpenTelemetry tracing for observability +- Ensure RBAC is enforced on all endpoints +- Validate all VTT outputs for correctness +- Write unit tests for all services and utilities + +### Testing Requirements +- Unit tests ≥80% coverage for services/utils +- Integration tests with mocked AI services +- E2E tests for complete workflows +- Performance testing for video processing + +### Lint/Type Check Commands +- Backend: `ruff check .` and `mypy .` +- Frontend: `npm run lint` and `npm run type-check` + +## Important Files to Reference +- `video_accessibility_development_plan.txt` - Complete specification +- Backend schemas in section 17 of the plan +- API design in section 7 of the plan +- Frontend component specs in section 10 of the plan +- Security requirements in section 11 of the plan + +## Risk Mitigations +- Invalid JSON from AI models: Pydantic validation + self-heal prompts +- Timestamp drift: Preserve cue timings in translations +- TTS alignment: Per-cue synthesis with crossfades +- Queue backlog: Autoscaling workers with monitoring +- Security: Secret Manager, least-privilege IAM, no client secrets \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7f18d4f --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +.PHONY: help install dev-backend dev-frontend dev-worker test lint clean + +help: ## Show this help message + @echo 'Usage: make [target]' + @echo '' + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +install: ## Install all dependencies + @echo "Installing backend dependencies..." + cd backend && poetry install + @echo "Installing frontend dependencies..." + cd frontend && npm install + +dev-backend: ## Start backend development server + cd backend && poetry run uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +dev-frontend: ## Start frontend development server + cd frontend && npm run dev + +dev-worker: ## Start Celery worker + cd backend && poetry run celery -A celery_worker.celery_app worker --loglevel=info + +test-backend: ## Run backend tests + cd backend && poetry run pytest + +test-frontend: ## Run frontend tests + cd frontend && npm run test + +lint-backend: ## Lint backend code + cd backend && poetry run ruff check . && poetry run mypy . + +lint-frontend: ## Lint frontend code + cd frontend && npm run lint && npm run type-check + +lint: lint-backend lint-frontend ## Lint all code + +clean: ## Clean build artifacts + cd backend && rm -rf __pycache__ .pytest_cache .mypy_cache + cd frontend && rm -rf node_modules/.cache dist + +build-backend: ## Build backend Docker image + cd backend && docker build -t accessible-video-backend . + +build-frontend: ## Build frontend for production + cd frontend && npm run build + +# Development helpers +setup-env: ## Copy environment templates + cp backend/.env.example backend/.env + cp frontend/.env.example frontend/.env + @echo "Environment files created. Please update with your actual values." + +dev: ## Start all development services (requires tmux) + tmux new-session -d -s accessible-video + tmux send-keys -t accessible-video 'make dev-backend' C-m + tmux split-window -t accessible-video + tmux send-keys -t accessible-video 'make dev-frontend' C-m + tmux split-window -t accessible-video + tmux send-keys -t accessible-video 'make dev-worker' C-m + tmux select-layout -t accessible-video tiled + tmux attach -t accessible-video \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7bbdc0 --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Accessible Video Processing Platform + +An AI-powered platform for generating accessible video content including closed captions, audio descriptions, and multi-language translations. + +## Features + +- **AI-Powered Processing**: Uses Gemini 2.5 Pro for intelligent caption and audio description generation +- **Multi-Language Support**: Automatic translation and cultural transcreation +- **Quality Control Workflow**: Built-in review and approval process +- **Audio Description**: Text-to-speech generation for voiceovers +- **Secure File Handling**: Google Cloud Storage with signed URLs +- **Role-Based Access**: Client, reviewer, and admin roles with appropriate permissions + +## Tech Stack + +### Backend +- **FastAPI** - Modern Python web framework +- **Celery** - Distributed task queue for video processing +- **MongoDB** - Document database for job and user data +- **Redis** - Task queue broker and caching +- **Google Cloud Services** - Storage, AI, and TTS + +### Frontend +- **React 18** - UI framework +- **Vite** - Fast build tool and dev server +- **TypeScript** - Type safety +- **TanStack Query** - Data fetching and caching +- **Tailwind CSS** - Utility-first styling + +## Getting Started + +### Prerequisites + +- Python 3.11+ +- Node.js 18+ +- Poetry (for Python dependency management) +- MongoDB (Atlas recommended) +- Redis +- Google Cloud Project with required APIs enabled + +### Installation + +1. **Clone and setup environment:** + ```bash + git clone + cd accessible-video + make setup-env + ``` + +2. **Install dependencies:** + ```bash + make install + ``` + +3. **Configure environment variables:** + - Update `backend/.env` with your database, API keys, and service credentials + - Update `frontend/.env` with your API base URL + +### Development + +**Start all services (requires tmux):** +```bash +make dev +``` + +**Or start services individually:** + +```bash +# Terminal 1 - Backend API +make dev-backend + +# Terminal 2 - Frontend SPA +make dev-frontend + +# Terminal 3 - Celery Worker +make dev-worker +``` + +The application will be available at: +- Frontend: http://localhost:5173 +- Backend API: http://localhost:8000 +- API Docs: http://localhost:8000/docs + +### Testing + +```bash +# Run all tests +make test-backend +make test-frontend + +# Lint code +make lint +``` + +## Architecture + +### Job Processing Pipeline + +1. **Upload**: Client uploads MP4 video +2. **Ingestion**: Video is processed and analyzed by Gemini 2.5 Pro +3. **QC Review**: Human reviewer approves/rejects English captions and audio descriptions +4. **Translation**: Approved content is translated to target languages +5. **TTS Generation**: Audio descriptions are converted to speech +6. **Final Review**: Reviewer approves final multi-language assets +7. **Delivery**: Client receives email with download links + +### File Structure + +``` +backend/ # FastAPI application +├── app/ +│ ├── api/ # REST API routes +│ ├── core/ # Configuration and shared utilities +│ ├── models/ # Pydantic data models +│ ├── services/ # External service integrations +│ ├── tasks/ # Celery background tasks +│ └── prompts/ # AI prompt templates +└── tests/ # Test suite + +frontend/ # React SPA +├── src/ +│ ├── components/ # Reusable UI components +│ ├── routes/ # Page components +│ ├── lib/ # Utilities and API client +│ ├── hooks/ # Custom React hooks +│ └── types/ # TypeScript definitions +└── public/ # Static assets +``` + +## Configuration + +### Required Environment Variables + +**Backend (.env):** +- `MONGODB_URI` - MongoDB connection string +- `REDIS_URL` - Redis connection string +- `JWT_SECRET` - Secret for JWT token signing +- `GEMINI_API_KEY` - Google Gemini API key +- `GCS_BUCKET` - Google Cloud Storage bucket name +- `SENDGRID_API_KEY` - SendGrid for email notifications + +**Frontend (.env):** +- `VITE_API_BASE_URL` - Backend API URL + +### Google Cloud Setup + +1. Create a GCP project +2. Enable required APIs: + - Cloud Storage API + - Cloud Translation API + - Cloud Text-to-Speech API + - Vertex AI API (for Gemini) +3. Create service account with appropriate permissions +4. Download service account key and configure `GOOGLE_APPLICATION_CREDENTIALS` + +## Deployment + +The application is designed for deployment on Google Cloud: + +- **Backend**: Cloud Run with auto-scaling +- **Workers**: Cloud Run with Celery +- **Frontend**: Cloud Storage + Cloud CDN +- **Database**: MongoDB Atlas +- **Queue**: Cloud Memorystore (Redis) + +See `/infra` directory for deployment configurations. + +## Security + +- JWT authentication with refresh token rotation +- Role-based access control (RBAC) +- Signed URLs for secure file access +- Audit logging for all reviewer actions +- HTTPS enforcement in production + +## Development Guide + +Always refer to the complete development plan in `video_accessibility_development_plan.txt` for detailed specifications and requirements. The CLAUDE.md file contains additional development guidelines and phase-by-phase implementation details. \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..53317a1 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,92 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Poetry (keep poetry.lock for reproducible builds) +# poetry.lock + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Testing +.coverage +.pytest_cache/ +.mypy_cache/ +.tox/ +htmlcov/ +coverage.xml +*.cover +.hypothesis/ + +# Documentation +docs/ +*.md +README* + +# Logs +*.log +logs/ + +# Git +.git/ +.gitignore + +# Docker +Dockerfile* +.dockerignore +docker-compose* + +# CI/CD +.github/ + +# Local development +.env.local +.env.development +.env.test + +# Temporary files +tmp/ +temp/ +*.tmp +*.bak \ No newline at end of file diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..787ca98 --- /dev/null +++ b/backend/.env @@ -0,0 +1,42 @@ +APP_ENV=dev +API_BASE_URL=http://localhost:8000 + +# Auth +JWT_SECRET=this_is_a_jwt_secret +JWT_ALG=HS256 +JWT_ACCESS_TTL_MIN=240 +JWT_REFRESH_TTL_DAYS=7 +COOKIE_DOMAIN=localdomain.com +COOKIE_SECURE=true +COOKIE_SAMESITE=Lax + +# MongoDB +MONGODB_URI=mongodb://admin:password123@localhost:27017/accessible_video?authSource=admin&replicaSet=rs0 +MONGODB_DB=accessible_video + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# Celery (uses Redis) +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + +# GCP +GCP_PROJECT_ID=optical-414516 +GCS_BUCKET=accessible-video +GOOGLE_APPLICATION_CREDENTIALS=/Users/michael.clervi/Documents/projects/video_accessibility/backend/optical-414516-80e2475f6412.json + +# AI +GEMINI_API_KEY=AIzaSyAuuVGcvqfoP7pqX-YwieGszPsNSeAft-0 +TRANSLATE_API_KEY=... +ELEVENLABS_API_KEY=... +GOOGLE_TTS_CREDENTIALS=/secrets/gcp_tts.json + +# Email +SENDGRID_API_KEY=disabled_for_local_testing +EMAIL_FROM=test@localhost.com +CLIENT_BASE_URL=http://localhost:5173 + +# Observability +SENTRY_DSN=... +OTEL_EXPORTER_OTLP_ENDPOINT= diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..355330c --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,42 @@ +APP_ENV=dev +API_BASE_URL=https://api.yourdomain.com + +# Auth +JWT_SECRET=change_me +JWT_ALG=HS256 +JWT_ACCESS_TTL_MIN=240 +JWT_REFRESH_TTL_DAYS=7 +COOKIE_DOMAIN=yourdomain.com +COOKIE_SECURE=true +COOKIE_SAMESITE=Lax + +# MongoDB +MONGODB_URI=mongodb://localhost:27017/accessible_video +MONGODB_DB=accessible_video + +# Redis +REDIS_URL=redis://localhost:6379/0 + +# Celery (uses Redis) +CELERY_BROKER_URL=redis://localhost:6379/0 +CELERY_RESULT_BACKEND=redis://localhost:6379/0 + +# GCP +GCP_PROJECT_ID=... +GCS_BUCKET=accessible-video +GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp.json + +# AI +GEMINI_API_KEY=... +TRANSLATE_API_KEY=... +ELEVENLABS_API_KEY=... +GOOGLE_TTS_CREDENTIALS=/secrets/gcp_tts.json + +# Email +SENDGRID_API_KEY=... +EMAIL_FROM=support@yourdomain.com +CLIENT_BASE_URL=https://app.yourdomain.com + +# Observability +SENTRY_DSN=... +OTEL_EXPORTER_OTLP_ENDPOINT=... \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..2fa68fb --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,127 @@ +# Build stage - Install dependencies and build wheels +FROM python:3.11-slim AS builder + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Poetry +RUN pip install poetry==1.8.2 + +# Set Poetry configuration +ENV POETRY_NO_INTERACTION=1 \ + POETRY_VENV_IN_PROJECT=1 \ + POETRY_CACHE_DIR=/tmp/poetry_cache + +WORKDIR /app + +# Copy dependency files +COPY pyproject.toml poetry.lock ./ + +# Install dependencies into venv +RUN poetry config virtualenvs.in-project true && \ + poetry lock --no-update || true && \ + poetry install --only=main --no-root && \ + rm -rf $POETRY_CACHE_DIR + +# Base runtime stage +FROM python:3.11-slim AS base + +# Install runtime system dependencies +RUN apt-get update && apt-get install -y \ + ffmpeg \ + curl \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && apt-get clean + +# Create non-root user +RUN groupadd --gid 1000 app \ + && useradd --uid 1000 --gid app --shell /bin/bash --create-home app + +# Set working directory +WORKDIR /app + +# Copy virtual environment from builder stage +COPY --from=builder --chown=app:app /app/.venv /app/.venv + +# Ensure venv is in PATH +ENV PATH="/app/.venv/bin:$PATH" + +# Copy application code +COPY --chown=app:app . . + +# Switch to non-root user +USER app + +# Production API stage +FROM base AS production + +# Set environment variables for production +ENV APP_ENV=prod \ + PYTHONPATH=/app \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Expose port +EXPOSE 8000 + +# Use tini as init system for proper signal handling +ENTRYPOINT ["tini", "--"] + +# Default command for API server +CMD ["gunicorn", "-c", "gunicorn_conf.py"] + +# Worker stage for Celery workers +FROM base AS worker + +# Set environment variables for worker +ENV APP_ENV=prod \ + PYTHONPATH=/app \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + C_FORCE_ROOT=1 + +# Health check for worker (check if Celery is responding) +HEALTHCHECK --interval=60s --timeout=15s --start-period=10s --retries=3 \ + CMD python -c "from celery import Celery; app=Celery('app'); print('Worker healthy')" || exit 1 + +# Use tini as init system for proper signal handling +ENTRYPOINT ["tini", "--"] + +# Default command for Celery worker +CMD ["celery", "-A", "app.tasks", "worker", "--loglevel=info", "--concurrency=1"] + +# Development stage with dev dependencies +FROM builder AS development + +# Install all dependencies including dev +RUN poetry install --no-root && rm -rf $POETRY_CACHE_DIR + +# Install additional dev tools +RUN apt-get update && apt-get install -y \ + git \ + vim \ + && rm -rf /var/lib/apt/lists/* + +# Copy application code +COPY --chown=app:app . . + +# Switch to non-root user +USER app + +# Set environment for development +ENV APP_ENV=dev \ + PYTHONPATH=/app \ + PYTHONUNBUFFERED=1 + +EXPOSE 8000 + +# Development command with hot reload +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/backend/__pycache__/celery_worker.cpython-313.pyc b/backend/__pycache__/celery_worker.cpython-313.pyc new file mode 100644 index 0000000..11ef22b Binary files /dev/null and b/backend/__pycache__/celery_worker.cpython-313.pyc differ diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..33c65c8 Binary files /dev/null and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc new file mode 100644 index 0000000..81978ae Binary files /dev/null and b/backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc differ diff --git a/backend/app/api/v1/__pycache__/routes_auth.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_auth.cpython-313.pyc new file mode 100644 index 0000000..82e436e Binary files /dev/null and b/backend/app/api/v1/__pycache__/routes_auth.cpython-313.pyc differ diff --git a/backend/app/api/v1/__pycache__/routes_files.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_files.cpython-313.pyc new file mode 100644 index 0000000..3e238f6 Binary files /dev/null and b/backend/app/api/v1/__pycache__/routes_files.cpython-313.pyc differ diff --git a/backend/app/api/v1/__pycache__/routes_jobs.cpython-313.pyc b/backend/app/api/v1/__pycache__/routes_jobs.cpython-313.pyc new file mode 100644 index 0000000..070f4c9 Binary files /dev/null and b/backend/app/api/v1/__pycache__/routes_jobs.cpython-313.pyc differ diff --git a/backend/app/api/v1/routes_admin.py b/backend/app/api/v1/routes_admin.py new file mode 100644 index 0000000..db80287 --- /dev/null +++ b/backend/app/api/v1/routes_admin.py @@ -0,0 +1,770 @@ +from datetime import datetime, timedelta +from typing import Optional + +from bson import ObjectId +from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from motor.motor_asyncio import AsyncIOMotorDatabase + +from ...core.database import get_database +from ...core.dependencies import get_current_user, require_roles +from ...core.logging import get_logger +from ...core.security import get_password_hash, verify_password +from ...models.user import User, UserRole +from ...models.audit_log import AuditLogQuery, AuditLogResponse +from ...schemas.auth import ( + AdminStatsResponse, + ChangePasswordRequest, + CreateUserRequest, + ResetPasswordRequest, + UpdateUserRequest, + UserListResponse, + UserResponse, +) +from ...services.audit_logger import audit_logger, log_user_management, log_security_event +from ...telemetry import app_metrics + +logger = get_logger(__name__) +router = APIRouter(prefix="/admin", tags=["admin"]) + + +@router.get("/users", response_model=UserListResponse) +async def list_users( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + role: Optional[str] = Query(None), + active_only: bool = Query(True), + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """List users with filtering and pagination (admin only)""" + query = {} + + if role: + query["role"] = role + + if active_only: + query["is_active"] = True + + # Get total count + total = await db.users.count_documents(query) + + # Get paginated results + skip = (page - 1) * size + cursor = db.users.find(query, {"hashed_password": 0}).sort("created_at", -1).skip(skip).limit(size) + users = await cursor.to_list(length=size) + + user_responses = [] + for user_doc in users: + user_responses.append(UserResponse( + id=str(user_doc["_id"]), + email=user_doc["email"], + full_name=user_doc["full_name"], + role=user_doc["role"], + is_active=user_doc["is_active"], + created_at=user_doc.get("created_at", datetime.utcnow()).isoformat() + )) + + return UserListResponse( + users=user_responses, + total=total, + page=page, + size=size + ) + + +@router.get("/users/{user_id}", response_model=UserResponse) +async def get_user( + user_id: str, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Get user details by ID (admin only)""" + user_doc = await db.users.find_one({"_id": user_id}, {"hashed_password": 0}) + if not user_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + return UserResponse( + id=str(user_doc["_id"]), + email=user_doc["email"], + full_name=user_doc["full_name"], + role=user_doc["role"], + is_active=user_doc["is_active"], + created_at=user_doc.get("created_at", datetime.utcnow()).isoformat() + ) + + +@router.post("/users", response_model=UserResponse, status_code=status.HTTP_201_CREATED) +async def create_user( + user_data: CreateUserRequest, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Create a new user (admin only)""" + # Check if user already exists + existing_user = await db.users.find_one({"email": user_data.email}) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="User with this email already exists" + ) + + # Create user document + user_id = str(ObjectId()) + user_doc = { + "_id": user_id, + "email": user_data.email, + "hashed_password": get_password_hash(user_data.password), + "full_name": user_data.full_name, + "role": user_data.role.value, + "is_active": True, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow() + } + + await db.users.insert_one(user_doc) + + # Record metrics + app_metrics.record_auth_attempt("user_created", user_data.role.value) + + logger.info(f"Admin {current_user.id} created user {user_id} with role {user_data.role.value}") + + return UserResponse( + id=user_id, + email=user_data.email, + full_name=user_data.full_name, + role=user_data.role, + is_active=True, + created_at=user_doc["created_at"].isoformat() + ) + + +@router.patch("/users/{user_id}", response_model=UserResponse) +async def update_user( + user_id: str, + user_update: UpdateUserRequest, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Update user details (admin only)""" + # Check if user exists + user_doc = await db.users.find_one({"_id": user_id}) + if not user_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Check if email is being changed and doesn't conflict + if user_update.email and user_update.email != user_doc["email"]: + existing_user = await db.users.find_one({"email": user_update.email, "_id": {"$ne": user_id}}) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already in use by another user" + ) + + # Build update document + update_data = {"updated_at": datetime.utcnow()} + + if user_update.email: + update_data["email"] = user_update.email + if user_update.full_name: + update_data["full_name"] = user_update.full_name + if user_update.role: + update_data["role"] = user_update.role.value + if user_update.is_active is not None: + update_data["is_active"] = user_update.is_active + + # Update user + result = await db.users.find_one_and_update( + {"_id": user_id}, + {"$set": update_data}, + return_document=True + ) + + logger.info(f"Admin {current_user.id} updated user {user_id}") + + return UserResponse( + id=str(result["_id"]), + email=result["email"], + full_name=result["full_name"], + role=result["role"], + is_active=result["is_active"], + created_at=result.get("created_at", datetime.utcnow()).isoformat() + ) + + +@router.delete("/users/{user_id}") +async def deactivate_user( + user_id: str, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Deactivate user account (admin only) - soft delete""" + if str(current_user.id) == user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot deactivate your own account" + ) + + result = await db.users.update_one( + {"_id": user_id}, + { + "$set": { + "is_active": False, + "updated_at": datetime.utcnow() + } + } + ) + + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + logger.info(f"Admin {current_user.id} deactivated user {user_id}") + + return {"message": "User deactivated successfully"} + + +@router.post("/users/{user_id}/reset-password") +async def admin_reset_password( + user_id: str, + reset_request: ResetPasswordRequest, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Reset user password (admin only)""" + # Generate temporary password + import secrets + import string + + temp_password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(12)) + hashed_password = get_password_hash(temp_password) + + result = await db.users.update_one( + {"_id": user_id}, + { + "$set": { + "hashed_password": hashed_password, + "updated_at": datetime.utcnow() + } + } + ) + + if result.matched_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + logger.info(f"Admin {current_user.id} reset password for user {user_id}") + + # In production, send email with temp password instead of returning it + return { + "message": "Password reset successfully", + "temporary_password": temp_password # Remove this in production, send via email + } + + +@router.get("/stats", response_model=AdminStatsResponse) +async def get_admin_stats( + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Get system statistics (admin only)""" + # Get user count + total_users = await db.users.count_documents({"is_active": True}) + + # Get job counts + total_jobs = await db.jobs.count_documents({}) + + # Get jobs by status + pipeline = [ + {"$group": {"_id": "$status", "count": {"$sum": 1}}} + ] + status_counts = await db.jobs.aggregate(pipeline).to_list(None) + jobs_by_status = {item["_id"]: item["count"] for item in status_counts} + + # Get jobs created today + today_start = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) + active_jobs_today = await db.jobs.count_documents({ + "created_at": {"$gte": today_start} + }) + + # Calculate average processing time for completed jobs + avg_processing_pipeline = [ + {"$match": {"status": "completed", "created_at": {"$exists": True}, "updated_at": {"$exists": True}}}, + { + "$project": { + "processing_time_hours": { + "$divide": [ + {"$subtract": ["$updated_at", "$created_at"]}, + 3600000 # Convert milliseconds to hours + ] + } + } + }, + { + "$group": { + "_id": None, + "avg_processing_time": {"$avg": "$processing_time_hours"} + } + } + ] + + avg_result = await db.jobs.aggregate(avg_processing_pipeline).to_list(None) + avg_processing_time = avg_result[0]["avg_processing_time"] if avg_result else 0.0 + + return AdminStatsResponse( + total_users=total_users, + total_jobs=total_jobs, + jobs_by_status=jobs_by_status, + active_jobs_today=active_jobs_today, + avg_processing_time_hours=round(avg_processing_time, 2) + ) + + +@router.get("/health/detailed") +async def detailed_health_check( + current_user: User = Depends(require_roles(UserRole.ADMIN, UserRole.REVIEWER)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Detailed health check with system component status (admin/reviewer only)""" + health_status = { + "status": "healthy", + "timestamp": datetime.utcnow().isoformat(), + "components": {} + } + + # Check MongoDB + try: + await db.command("ping") + health_status["components"]["mongodb"] = {"status": "healthy"} + except Exception as e: + health_status["components"]["mongodb"] = {"status": "unhealthy", "error": str(e)} + health_status["status"] = "degraded" + + # Check Redis (via import to avoid circular dependency) + try: + from ...core.redis import redis_client + if redis_client: + await redis_client.ping() + health_status["components"]["redis"] = {"status": "healthy"} + else: + health_status["components"]["redis"] = {"status": "not_configured"} + except Exception as e: + health_status["components"]["redis"] = {"status": "unhealthy", "error": str(e)} + health_status["status"] = "degraded" + + # Check GCS (basic check) + try: + from ...services.gcs import gcs_service + # Simple check to see if bucket is accessible + bucket_exists = await gcs_service.file_exists("health_check_dummy") # This will return False but won't error if bucket accessible + health_status["components"]["gcs"] = {"status": "healthy"} + except Exception as e: + health_status["components"]["gcs"] = {"status": "unhealthy", "error": str(e)} + health_status["status"] = "degraded" + + # Check job queue health + try: + from ...tasks import celery_app + inspect = celery_app.control.inspect() + active_tasks = inspect.active() + + if active_tasks: + total_active = sum(len(tasks) for tasks in active_tasks.values()) + health_status["components"]["celery"] = { + "status": "healthy", + "active_tasks": total_active, + "workers": len(active_tasks) + } + else: + health_status["components"]["celery"] = { + "status": "no_workers", + "active_tasks": 0, + "workers": 0 + } + except Exception as e: + health_status["components"]["celery"] = {"status": "unhealthy", "error": str(e)} + health_status["status"] = "degraded" + + return health_status + + +@router.get("/jobs/stats") +async def get_job_statistics( + days: int = Query(7, ge=1, le=90), + current_user: User = Depends(require_roles(UserRole.ADMIN, UserRole.REVIEWER)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Get job processing statistics (admin/reviewer only)""" + since_date = datetime.utcnow() - timedelta(days=days) + + # Jobs created in period + jobs_in_period = await db.jobs.count_documents({ + "created_at": {"$gte": since_date} + }) + + # Jobs completed in period + jobs_completed = await db.jobs.count_documents({ + "status": "completed", + "updated_at": {"$gte": since_date} + }) + + # Average processing time for completed jobs + avg_pipeline = [ + { + "$match": { + "status": "completed", + "created_at": {"$gte": since_date}, + "updated_at": {"$exists": True} + } + }, + { + "$project": { + "processing_time_hours": { + "$divide": [ + {"$subtract": ["$updated_at", "$created_at"]}, + 3600000 + ] + } + } + }, + { + "$group": { + "_id": None, + "avg_time": {"$avg": "$processing_time_hours"}, + "min_time": {"$min": "$processing_time_hours"}, + "max_time": {"$max": "$processing_time_hours"} + } + } + ] + + avg_result = await db.jobs.aggregate(avg_pipeline).to_list(None) + processing_stats = avg_result[0] if avg_result else { + "avg_time": 0, "min_time": 0, "max_time": 0 + } + + # Current queue status + current_queue_stats = {} + pipeline = [ + {"$group": {"_id": "$status", "count": {"$sum": 1}}} + ] + status_counts = await db.jobs.aggregate(pipeline).to_list(None) + for item in status_counts: + current_queue_stats[item["_id"]] = item["count"] + + return { + "period_days": days, + "jobs_created": jobs_in_period, + "jobs_completed": jobs_completed, + "completion_rate": round(jobs_completed / max(jobs_in_period, 1) * 100, 2), + "avg_processing_time_hours": round(processing_stats["avg_time"], 2), + "min_processing_time_hours": round(processing_stats["min_time"], 2), + "max_processing_time_hours": round(processing_stats["max_time"], 2), + "current_queue_status": current_queue_stats + } + + +@router.post("/users/{user_id}/password/reset") +async def admin_force_password_reset( + user_id: str, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Force password reset for user (admin only)""" + if str(current_user.id) == user_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Cannot reset your own password this way" + ) + + # Check if user exists + user_doc = await db.users.find_one({"_id": user_id}) + if not user_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + # Generate secure temporary password + import secrets + import string + + temp_password = ''.join(secrets.choice( + string.ascii_letters + string.digits + "!@#$%" + ) for _ in range(16)) + + # Update password + await db.users.update_one( + {"_id": user_id}, + { + "$set": { + "hashed_password": get_password_hash(temp_password), + "updated_at": datetime.utcnow() + } + } + ) + + # TODO: In production, send via secure email instead of returning password + logger.info(f"Admin {current_user.id} reset password for user {user_id}") + + return { + "message": "Password reset successfully", + "temporary_password": temp_password, + "note": "User should change this password immediately" + } + + +@router.get("/audit-logs") +async def get_audit_logs( + job_id: Optional[str] = Query(None), + action: Optional[str] = Query(None), + days: int = Query(7, ge=1, le=90), + page: int = Query(1, ge=1), + size: int = Query(50, ge=1, le=200), + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Get audit logs with filtering (admin only)""" + query = { + "when": {"$gte": datetime.utcnow() - timedelta(days=days)} + } + + if job_id: + query["job_id"] = job_id + if action: + query["action"] = action + + # Get total count + total = await db.audit_logs.count_documents(query) + + # Get paginated results + skip = (page - 1) * size + cursor = ( + db.audit_logs.find(query) + .sort("when", -1) + .skip(skip) + .limit(size) + ) + logs = await cursor.to_list(length=size) + + return { + "logs": logs, + "total": total, + "page": page, + "size": size, + "period_days": days + } + + +@router.post("/maintenance/reprocess-job/{job_id}") +async def reprocess_job( + job_id: str, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Force reprocessing of a job (admin emergency function)""" + # Check if job exists + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Reset job to created status for reprocessing + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": "created", + "error": None, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": "reprocessing", + "by": str(current_user.id), + "notes": "Admin-triggered reprocessing" + } + } + } + ) + + # Trigger ingestion task + from ...tasks.ingest_and_ai import ingest_and_ai_task + ingest_and_ai_task.delay(job_id) + + logger.warning(f"Admin {current_user.id} triggered reprocessing for job {job_id}") + + return {"message": f"Job {job_id} queued for reprocessing"} + + +@router.get("/audit-logs", response_model=AuditLogResponse) +async def get_audit_logs( + # Time range + start_date: Optional[datetime] = Query(None, description="Start date for audit logs"), + end_date: Optional[datetime] = Query(None, description="End date for audit logs"), + + # Filters + action: Optional[str] = Query(None, description="Filter by action type"), + severity: Optional[str] = Query(None, description="Filter by severity level"), + user_email: Optional[str] = Query(None, description="Filter by user email"), + resource_type: Optional[str] = Query(None, description="Filter by resource type"), + resource_id: Optional[str] = Query(None, description="Filter by resource ID"), + success: Optional[bool] = Query(None, description="Filter by success status"), + + # Search + search: Optional[str] = Query(None, description="Search in description and details"), + + # Pagination + page: int = Query(1, ge=1, description="Page number"), + size: int = Query(50, ge=1, le=500, description="Page size"), + + # Sorting + sort_by: str = Query("timestamp", description="Field to sort by"), + sort_order: int = Query(-1, ge=-1, le=1, description="Sort order (-1 desc, 1 asc)"), + + current_user: User = Depends(require_roles(UserRole.ADMIN)), + request: Request = None, +): + """Get audit logs with filtering and pagination (admin only)""" + + # Log audit log access + await audit_logger.log_action( + action="admin.audit.access", + description=f"Admin {current_user.email} accessed audit logs", + user=current_user, + request=request, + details={ + "filters": { + "start_date": start_date.isoformat() if start_date else None, + "end_date": end_date.isoformat() if end_date else None, + "action": action, + "severity": severity, + "user_email": user_email, + "resource_type": resource_type, + "search": search + } + } + ) + + # Build query + query = AuditLogQuery( + start_date=start_date, + end_date=end_date, + action=action, + severity=severity, + user_email=user_email, + resource_type=resource_type, + resource_id=resource_id, + success=success, + search=search, + skip=(page - 1) * size, + limit=size, + sort_by=sort_by, + sort_order=sort_order + ) + + return await audit_logger.query_logs(query) + + +@router.get("/audit-logs/user/{user_id}") +async def get_user_audit_logs( + user_id: str, + days: int = Query(30, ge=1, le=365, description="Number of days to look back"), + current_user: User = Depends(require_roles(UserRole.ADMIN)), + request: Request = None, +): + """Get audit logs for a specific user (admin only)""" + + # Validate user_id + try: + ObjectId(user_id) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid user ID format" + ) + + # Log access to user audit logs + await audit_logger.log_action( + action="admin.audit.access", + description=f"Admin {current_user.email} accessed user audit logs for {user_id}", + user=current_user, + request=request, + resource_type="user", + resource_id=user_id, + details={"days_requested": days} + ) + + logs = await audit_logger.get_user_activity(user_id, days) + return {"logs": logs, "user_id": user_id, "days": days} + + +@router.get("/audit-logs/security") +async def get_security_events( + hours: int = Query(24, ge=1, le=168, description="Number of hours to look back"), + current_user: User = Depends(require_roles(UserRole.ADMIN)), + request: Request = None, +): + """Get recent security events (admin only)""" + + # Log access to security events + await audit_logger.log_action( + action="admin.audit.access", + description=f"Admin {current_user.email} accessed security events", + user=current_user, + request=request, + details={"hours_requested": hours} + ) + + logs = await audit_logger.get_security_events(hours) + return {"logs": logs, "hours": hours} + + +@router.delete("/audit-logs/cleanup") +async def cleanup_audit_logs( + retention_days: int = Query(365, ge=30, le=2555, description="Retention period in days"), + current_user: User = Depends(require_roles(UserRole.ADMIN)), + request: Request = None, +): + """Clean up old audit logs (admin only)""" + + # Log audit cleanup action + await audit_logger.log_action( + action="admin.system.action", + description=f"Admin {current_user.email} initiated audit log cleanup", + user=current_user, + request=request, + details={"retention_days": retention_days}, + severity="warning" + ) + + deleted_count = await audit_logger.cleanup_old_logs(retention_days) + + # Log cleanup completion + await audit_logger.log_action( + action="admin.system.action", + description=f"Audit log cleanup completed: {deleted_count} logs deleted", + user=current_user, + request=request, + details={ + "retention_days": retention_days, + "deleted_count": deleted_count + } + ) + + return { + "message": f"Deleted {deleted_count} audit logs older than {retention_days} days", + "deleted_count": deleted_count, + "retention_days": retention_days + } \ No newline at end of file diff --git a/backend/app/api/v1/routes_auth.py b/backend/app/api/v1/routes_auth.py new file mode 100644 index 0000000..d3e9191 --- /dev/null +++ b/backend/app/api/v1/routes_auth.py @@ -0,0 +1,161 @@ +from fastapi import APIRouter, Depends, HTTPException, Request, Response, status +from fastapi.security import HTTPBearer +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase + +from ...core.config import settings +from ...core.database import get_database +from ...core.security import ( + create_access_token, + create_refresh_token, + decode_token, + verify_password, +) +from ...models.user import User +from ...schemas.auth import LoginRequest, LoginResponse, LogoutResponse, RefreshResponse + +router = APIRouter(prefix="/auth", tags=["auth"]) +security = HTTPBearer() + + +@router.post("/login", response_model=LoginResponse) +async def login( + login_data: LoginRequest, + response: Response, +): + print(f"LOGIN: Starting login for {login_data.email}") + # Create database connection directly (bypass dependency injection issues) + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + print("LOGIN: Database connection created") + # Find user by email + print("LOGIN: Looking up user in database") + user_doc = await db.users.find_one({"email": login_data.email}) + print(f"LOGIN: User lookup complete, found: {user_doc is not None}") + if not user_doc: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + ) + + user = User(**user_doc) + + # Verify password + if not verify_password(login_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User account is disabled", + ) + + # Create tokens + access_token = create_access_token(subject=str(user.id)) + refresh_token = create_refresh_token(subject=str(user.id)) + + # Set refresh token as HttpOnly cookie + response.set_cookie( + key="refresh_token", + value=refresh_token, + httponly=True, + secure=settings.cookie_secure, + samesite=settings.cookie_samesite, + domain=settings.cookie_domain if settings.app_env == "prod" else None, + max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60, + ) + + return LoginResponse( + access_token=access_token, + user_id=str(user.id), + role=user.role, + ) + + finally: + # Close database connection + client.close() + + +@router.post("/refresh", response_model=RefreshResponse) +async def refresh_token( + request: Request, + response: Response, + db: AsyncIOMotorDatabase = Depends(get_database), +): + refresh_token = request.cookies.get("refresh_token") + if not refresh_token: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Refresh token not found", + ) + + try: + payload = decode_token(refresh_token) + if payload.get("type") != "refresh": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token type", + ) + + user_id = payload.get("sub") + if not user_id: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token", + ) + + # Verify user still exists and is active + user_doc = await db.users.find_one({"_id": user_id}) + if not user_doc: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + ) + + user = User(**user_doc) + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User account is disabled", + ) + + # Create new tokens + new_access_token = create_access_token(subject=user_id) + new_refresh_token = create_refresh_token(subject=user_id) + + # Update refresh token cookie + response.set_cookie( + key="refresh_token", + value=new_refresh_token, + httponly=True, + secure=settings.cookie_secure, + samesite=settings.cookie_samesite, + domain=settings.cookie_domain if settings.app_env == "prod" else None, + max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60, + ) + + return RefreshResponse(access_token=new_access_token) + + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid refresh token", + ) + + +@router.post("/logout", response_model=LogoutResponse) +async def logout(response: Response): + # Clear refresh token cookie + response.delete_cookie( + key="refresh_token", + httponly=True, + secure=settings.cookie_secure, + samesite=settings.cookie_samesite, + domain=settings.cookie_domain if settings.app_env == "prod" else None, + ) + + return LogoutResponse() diff --git a/backend/app/api/v1/routes_files.py b/backend/app/api/v1/routes_files.py new file mode 100644 index 0000000..5021655 --- /dev/null +++ b/backend/app/api/v1/routes_files.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from motor.motor_asyncio import AsyncIOMotorDatabase + +from ...core.database import get_database +from ...core.dependencies import get_current_user +from ...models.user import User +from ...schemas.file import SignedUploadRequest, SignedUploadResponse +from ...services.gcs import generate_signed_upload_url + +router = APIRouter(prefix="/files", tags=["files"]) + + +@router.post("/signed-upload", response_model=SignedUploadResponse) +async def get_signed_upload_url( + request: SignedUploadRequest, + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """ + Generate a signed URL for direct browser-to-GCS upload + This optimizes large file uploads by bypassing the API server + """ + if not request.content_type.startswith("video/"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only video files are supported" + ) + + # Generate unique blob path + from bson import ObjectId + blob_path = f"temp/{ObjectId()}/{request.filename}" + + try: + # Generate signed upload URL with form fields + signed_data = await generate_signed_upload_url( + blob_path=blob_path, + content_type=request.content_type, + max_size=request.max_size or 1024 * 1024 * 1024 # 1GB default + ) + + return SignedUploadResponse( + upload_url=signed_data["url"], + fields=signed_data["fields"], + blob_path=blob_path + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to generate signed upload URL: {str(e)}" + ) \ No newline at end of file diff --git a/backend/app/api/v1/routes_jobs.py b/backend/app/api/v1/routes_jobs.py new file mode 100644 index 0000000..457079f --- /dev/null +++ b/backend/app/api/v1/routes_jobs.py @@ -0,0 +1,1033 @@ +from datetime import datetime +from typing import Optional + +from bson import ObjectId +from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status +from motor.motor_asyncio import AsyncIOMotorDatabase + +from ...core.config import settings +from ...core.database import get_database +from ...core.dependencies import get_current_user, require_roles +from ...core.logging import get_logger +from ...lib.vtt import VTTEditor +from ...models.job import JobStatus, RequestedOutputs +from ...models.user import User, UserRole +from ...schemas.job import ( + ApproveEnglishRequest, + AssetValidationResponse, + BulkDeleteRequest, + BulkDeleteResponse, + CompleteJobRequest, + JobDeleteResponse, + JobDownloadsResponse, + JobListResponse, + JobResponse, + RejectJobRequest, + VttContentResponse, + VttTimingAdjustRequest, + VttUpdateRequest, +) +from ...services.gcs import ( + gcs_service, + upload_file_to_gcs, + upload_vtt_to_gcs, +) +from ...services.validation import asset_validation_service +from ...tasks import celery_app +from ...tasks.ingest_and_ai import ingest_and_ai_task +from ...tasks.translate_and_synthesize import translate_and_synthesize_task + +logger = get_logger(__name__) +router = APIRouter(prefix="/jobs", tags=["jobs"]) + + +@router.post("", response_model=JobResponse, status_code=status.HTTP_201_CREATED) +async def create_job( + title: str = Form(...), + language: str = Form("en"), + requested_outputs: str = Form(...), # JSON string + file: UploadFile = File(...), + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + # Validate file type + if not file.content_type or not file.content_type.startswith("video/"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="File must be a video" + ) + + # Parse requested outputs + import json + try: + outputs_data = json.loads(requested_outputs) + outputs = RequestedOutputs(**outputs_data) + except Exception: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Invalid requested_outputs format" + ) + + # Generate job ID and upload file + job_id = str(ObjectId()) + gcs_uri = await upload_file_to_gcs( + file, f"{job_id}/source.mp4" + ) + + # Create job document + job_data = { + "_id": job_id, + "client_id": str(current_user.id), + "title": title, + "source": { + "filename": f"{job_id}/source.mp4", + "original_filename": file.filename, + "gcs_uri": gcs_uri, + "language": language + }, + "requested_outputs": outputs.dict(), + "status": JobStatus.CREATED.value, + "review": { + "notes": "", + "history": [{ + "at": datetime.utcnow(), + "status": JobStatus.CREATED.value, + "by": "system" + }] + }, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow() + } + + await db.jobs.insert_one(job_data) + + # Enqueue processing task + logger.info(f"Dispatching ingest_and_ai_task for job {job_id}") + logger.info(f"Using Celery app: {celery_app}") + logger.info(f"Task object: {ingest_and_ai_task}") + logger.info(f"Task routing config: {celery_app.conf.task_routes}") + try: + # Use apply_async for more control and debugging + task = ingest_and_ai_task.apply_async(args=[job_id], queue='ingest') + logger.info(f"Task dispatched successfully: {task.id} for job {job_id} to queue 'ingest'") + logger.info(f"Task state: {task.state}") + logger.info(f"Task backend: {task.backend}") + + # Try to get the task result to see if it was actually queued + try: + # This should timeout quickly since task just started + import time + time.sleep(1) # Give it a moment + task_info = task.state + logger.info(f"Task state after 1 second: {task_info}") + + # Check celery inspect to see active/scheduled tasks + from celery import current_app + i = current_app.control.inspect() + active_tasks = i.active() + scheduled_tasks = i.scheduled() + logger.info(f"Active tasks across all workers: {active_tasks}") + logger.info(f"Scheduled tasks across all workers: {scheduled_tasks}") + + except Exception as e: + logger.warning(f"Could not inspect task status: {e}") + + # Store task ID in job document for monitoring + await db.jobs.update_one( + {"_id": job_id}, + {"$set": {"task_id": task.id}} + ) + + except Exception as e: + logger.error(f"Failed to dispatch task for job {job_id}: {e} (type: {e.__class__.__name__})") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to start processing: {e}" + ) + + return JobResponse( + id=job_id, + title=title, + status=JobStatus.CREATED, + source=job_data["source"], + requested_outputs=outputs, + review=job_data["review"], + created_at=job_data["created_at"].isoformat(), + updated_at=job_data["updated_at"].isoformat() + ) + + +@router.delete("/bulk", response_model=BulkDeleteResponse) +async def bulk_delete_jobs( + request: BulkDeleteRequest, + current_user: User = Depends(require_roles(UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Bulk delete jobs (admin only)""" + job_ids = request.job_ids + logger.info(f"Bulk deleting {len(job_ids)} jobs requested by {current_user.email}") + + deleted_count = 0 + errors = [] + + for job_id in job_ids: + try: + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + errors.append(f"Job {job_id}: not found") + continue + + # Cancel task if exists + task_id = job_doc.get("task_id") + if task_id: + try: + celery_app.control.revoke(task_id, terminate=True) + except Exception as e: + logger.warning(f"Could not cancel task {task_id} for job {job_id}: {e}") + + # Delete GCS assets + await _delete_job_gcs_assets(job_id, job_doc) + + # Delete from database + result = await db.jobs.delete_one({"_id": job_id}) + if result.deleted_count > 0: + deleted_count += 1 + logger.info(f"Deleted job {job_id}") + else: + errors.append(f"Job {job_id}: database deletion failed") + + except Exception as e: + errors.append(f"Job {job_id}: {str(e)}") + logger.error(f"Failed to delete job {job_id}: {e}") + + return { + "deleted_count": deleted_count, + "total_requested": len(job_ids), + "errors": errors + } + + +@router.get("", response_model=JobListResponse) +async def list_jobs( + status: Optional[str] = None, + mine: bool = False, + page: int = 1, + size: int = 20, + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + query = {} + + if status: + if ',' in status: + # Handle comma-separated status values + status_list = [s.strip() for s in status.split(',')] + query["status"] = {"$in": status_list} + else: + query["status"] = status + + if mine or current_user.role == UserRole.CLIENT: + query["client_id"] = str(current_user.id) + + # Get total count + total = await db.jobs.count_documents(query) + + # Get paginated results + skip = (page - 1) * size + cursor = db.jobs.find(query).sort("created_at", -1).skip(skip).limit(size) + jobs = await cursor.to_list(length=size) + + job_responses = [] + for job_doc in jobs: + job_responses.append(JobResponse( + id=str(job_doc["_id"]), + title=job_doc["title"], + status=job_doc["status"], + source=job_doc["source"], + requested_outputs=RequestedOutputs(**job_doc["requested_outputs"]), + review=job_doc.get("review", {"notes": "", "history": []}), + outputs=job_doc.get("outputs"), + created_at=job_doc["created_at"].isoformat(), + updated_at=job_doc["updated_at"].isoformat() + )) + + return JobListResponse( + jobs=job_responses, + total=total, + page=page, + size=size + ) + + +@router.get("/{job_id}", response_model=JobResponse) +async def get_job( + job_id: str, + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Check access permissions + if (current_user.role == UserRole.CLIENT and + job_doc["client_id"] != str(current_user.id)): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied" + ) + + # Check task status if task_id exists + task_id = job_doc.get("task_id") + if task_id and job_doc["status"] == JobStatus.CREATED.value: + try: + task_result = celery_app.AsyncResult(task_id) + if task_result.failed(): + logger.error(f"Task {task_id} failed for job {job_id} - State: {task_result.state}, Error: {task_result.result}") + # Update job status to reflect task failure + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "error": { + "type": "task_failure", + "message": str(task_result.result) if task_result.result else "Unknown task failure", + "timestamp": datetime.utcnow().isoformat() + }, + "updated_at": datetime.utcnow() + } + } + ) + except Exception as e: + logger.warning(f"Could not check task status for job {job_id}: {e}") + + return JobResponse( + id=str(job_doc["_id"]), + title=job_doc["title"], + status=job_doc["status"], + source=job_doc["source"], + requested_outputs=RequestedOutputs(**job_doc["requested_outputs"]), + review=job_doc.get("review", {"notes": "", "history": []}), + outputs=job_doc.get("outputs"), + created_at=job_doc["created_at"].isoformat(), + updated_at=job_doc["updated_at"].isoformat() + ) + + +@router.post("/{job_id}/actions/approve_english", response_model=JobResponse) +async def approve_english( + job_id: str, + request: ApproveEnglishRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + result = await db.jobs.find_one_and_update( + {"_id": job_id, "status": JobStatus.PENDING_QC.value}, + { + "$set": { + "status": JobStatus.APPROVED_ENGLISH.value, + "review.notes": request.notes or "", + "review.reviewer_id": str(current_user.id), + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.APPROVED_ENGLISH.value, + "by": str(current_user.id), + "notes": request.notes or "" + } + } + }, + return_document=True + ) + + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found or not in pending QC status" + ) + + # Trigger translation and synthesis pipeline immediately + try: + translate_and_synthesize_task.delay(job_id) + logger.info(f"Triggered translation task for approved job {job_id}") + except Exception as e: + logger.error(f"Failed to trigger translation task for job {job_id}: {e}") + # Don't fail the approval, just log the error + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.post("/{job_id}/actions/reject", response_model=JobResponse) +async def reject_job( + job_id: str, + request: RejectJobRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + result = await db.jobs.find_one_and_update( + {"_id": job_id, "status": JobStatus.PENDING_QC.value}, + { + "$set": { + "status": JobStatus.REJECTED.value, + "review.notes": request.notes, + "review.reviewer_id": str(current_user.id), + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.REJECTED.value, + "by": str(current_user.id), + "notes": request.notes + } + } + }, + return_document=True + ) + + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found or not in pending QC status" + ) + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.post("/{job_id}/actions/complete", response_model=JobResponse) +async def complete_job( + job_id: str, + request: CompleteJobRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + # Get job for validation + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + if job_doc["status"] != JobStatus.PENDING_FINAL_REVIEW.value: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Job not ready for completion" + ) + + # Validate all assets before completion + is_valid, validation_errors = await asset_validation_service.validate_job_assets(job_doc) + if not is_valid: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Asset validation failed: {'; '.join(validation_errors)}" + ) + + result = await db.jobs.find_one_and_update( + {"_id": job_id, "status": JobStatus.PENDING_FINAL_REVIEW.value}, + { + "$set": { + "status": JobStatus.COMPLETED.value, + "review.notes": request.notes or "", + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.COMPLETED.value, + "by": str(current_user.id), + "notes": request.notes or "" + } + } + }, + return_document=True + ) + + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found or not in pending final review status" + ) + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.post("/{job_id}/actions/reject_final", response_model=JobResponse) +async def reject_final_review( + job_id: str, + request: RejectJobRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + result = await db.jobs.find_one_and_update( + {"_id": job_id, "status": JobStatus.PENDING_FINAL_REVIEW.value}, + { + "$set": { + "status": JobStatus.QC_FEEDBACK.value, + "review.notes": request.notes, + "review.reviewer_id": str(current_user.id), + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.QC_FEEDBACK.value, + "by": str(current_user.id), + "notes": request.notes + } + } + }, + return_document=True + ) + + if not result: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found or not in pending final review status" + ) + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.get("/{job_id}/downloads", response_model=JobDownloadsResponse) +async def get_job_downloads( + job_id: str, + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + from ...services.gcs import get_signed_download_url + + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Check access permissions (only client or admin/reviewer can download) + if (current_user.role == UserRole.CLIENT and + job_doc["client_id"] != str(current_user.id)): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied" + ) + + # Allow downloads for jobs that have outputs available + # (PENDING_QC, APPROVED_ENGLISH, TRANSLATING, COMPLETED, etc.) + if job_doc["status"] in [JobStatus.CREATED.value, JobStatus.INGESTING.value, JobStatus.AI_PROCESSING.value]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Job is still being processed" + ) + + # Check if job has outputs + if not job_doc.get("outputs"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No outputs available for this job" + ) + + # Generate signed URLs for all outputs + downloads = {} + + # Add source video URL + if job_doc.get("source", {}).get("gcs_uri"): + source_blob_path = job_doc["source"]["gcs_uri"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + source_signed_url = await get_signed_download_url(source_blob_path, 24) + downloads["source_video"] = source_signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for source video: {e}") + + outputs = job_doc.get("outputs", {}) + + for language, lang_output in outputs.items(): + if not isinstance(lang_output, dict): + continue + + lang_downloads = {} + + # Captions VTT + if "captions_vtt_gcs" in lang_output: + blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["captions_vtt"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for captions {language}: {e}") + + # Audio Description VTT + if "ad_vtt_gcs" in lang_output: + blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["audio_description_vtt"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for AD VTT {language}: {e}") + + # Audio Description MP3 + if "ad_mp3_gcs" in lang_output: + blob_path = lang_output["ad_mp3_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["audio_description_mp3"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for AD MP3 {language}: {e}") + + if lang_downloads: + downloads[language] = lang_downloads + + return JobDownloadsResponse(downloads=downloads) + + +@router.get("/{job_id}/vtt", response_model=VttContentResponse) +async def get_job_vtt_content( + job_id: str, + language: str = "en", + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Get VTT content for editing""" + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Check access permissions + if (current_user.role == UserRole.CLIENT and + job_doc["client_id"] != str(current_user.id)): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied" + ) + + outputs = job_doc.get("outputs", {}) + lang_output = outputs.get(language, {}) + + response = VttContentResponse() + + # Fetch captions VTT if available + if "captions_vtt_gcs" in lang_output: + blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + blob = gcs_service.bucket.blob(blob_path) + response.captions_vtt = blob.download_as_text() + except Exception as e: + logger.warning(f"Failed to fetch captions VTT: {e}") + + # Fetch audio description VTT if available + if "ad_vtt_gcs" in lang_output: + blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + blob = gcs_service.bucket.blob(blob_path) + response.audio_description_vtt = blob.download_as_text() + except Exception as e: + logger.warning(f"Failed to fetch AD VTT: {e}") + + return response + + +@router.patch("/{job_id}/vtt", response_model=JobResponse) +async def update_job_vtt_content( + job_id: str, + request: VttUpdateRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Update VTT content for a job""" + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Only allow editing during QC phase + if job_doc["status"] not in [JobStatus.PENDING_QC.value, JobStatus.QC_FEEDBACK.value]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="VTT content can only be edited during QC phase" + ) + + outputs = job_doc.get("outputs", {}) + lang_output = outputs.get(request.language, {}) + + # Validate and update captions VTT + if request.captions_vtt is not None: + # Validate VTT format + is_valid, errors = VTTEditor.validate_vtt(request.captions_vtt) + if not is_valid: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid captions VTT: {'; '.join(errors)}" + ) + + # Upload updated VTT + new_captions_uri = await upload_vtt_to_gcs( + request.captions_vtt, + f"{job_id}/{request.language}/captions.vtt" + ) + lang_output["captions_vtt_gcs"] = new_captions_uri + + # Validate and update audio description VTT + if request.audio_description_vtt is not None: + # Validate VTT format + is_valid, errors = VTTEditor.validate_vtt(request.audio_description_vtt) + if not is_valid: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Invalid audio description VTT: {'; '.join(errors)}" + ) + + # Upload updated VTT + new_ad_uri = await upload_vtt_to_gcs( + request.audio_description_vtt, + f"{job_id}/{request.language}/ad.vtt" + ) + lang_output["ad_vtt_gcs"] = new_ad_uri + + # Update job with new VTT content + outputs[request.language] = lang_output + + result = await db.jobs.find_one_and_update( + {"_id": job_id}, + { + "$set": { + "outputs": outputs, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": "vtt_updated", + "by": str(current_user.id), + "notes": f"Updated VTT content for {request.language}" + } + } + }, + return_document=True + ) + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.post("/{job_id}/vtt/adjust-timing", response_model=JobResponse) +async def adjust_vtt_timing( + job_id: str, + request: VttTimingAdjustRequest, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Adjust timing of VTT content by a specified offset""" + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Only allow timing adjustment during QC phase + if job_doc["status"] not in [JobStatus.PENDING_QC.value, JobStatus.QC_FEEDBACK.value]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="VTT timing can only be adjusted during QC phase" + ) + + # Get current VTT content from GCS + lang_key = request.language + outputs = job_doc.get("outputs", {}).get(lang_key, {}) + + if not outputs: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"No VTT content found for language: {request.language}" + ) + + update_operations = {} + adjusted_content = {} + + # Adjust captions VTT if requested and exists + if request.adjust_captions and "captions_vtt_gcs" in outputs: + try: + # Download current captions VTT + captions_blob = gcs_service.bucket.blob( + outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + ) + current_captions_vtt = captions_blob.download_as_text() + + # Apply timing adjustment + adjusted_captions_vtt = VTTEditor.adjust_timing_offset( + current_captions_vtt, + request.offset_seconds + ) + + # Upload adjusted content back to GCS + new_captions_gcs_uri = await upload_vtt_to_gcs( + adjusted_captions_vtt, + f"{job_id}/{request.language}/captions.vtt" + ) + + update_operations[f"outputs.{lang_key}.captions_vtt_gcs"] = new_captions_gcs_uri + adjusted_content["captions"] = True + + except Exception as e: + logger.error(f"Failed to adjust captions timing: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to adjust captions timing" + ) + + # Adjust audio description VTT if requested and exists + if request.adjust_audio_description and "ad_vtt_gcs" in outputs: + try: + # Download current AD VTT + ad_blob = gcs_service.bucket.blob( + outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + ) + current_ad_vtt = ad_blob.download_as_text() + + # Apply timing adjustment + adjusted_ad_vtt = VTTEditor.adjust_timing_offset( + current_ad_vtt, + request.offset_seconds + ) + + # Upload adjusted content back to GCS + new_ad_gcs_uri = await upload_vtt_to_gcs( + adjusted_ad_vtt, + f"{job_id}/{request.language}/ad.vtt" + ) + + update_operations[f"outputs.{lang_key}.ad_vtt_gcs"] = new_ad_gcs_uri + adjusted_content["audio_description"] = True + + except Exception as e: + logger.error(f"Failed to adjust audio description timing: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to adjust audio description timing" + ) + + if not update_operations: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="No VTT content to adjust" + ) + + # Update job document + result = await db.jobs.find_one_and_update( + {"_id": job_id}, + { + "$set": { + **update_operations, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": "timing_adjusted", + "by": str(current_user.id), + "notes": f"Adjusted timing by {request.offset_seconds:+.1f}s for {', '.join(adjusted_content.keys())}" + } + } + }, + return_document=True + ) + + return JobResponse( + id=str(result["_id"]), + title=result["title"], + status=result["status"], + source=result["source"], + requested_outputs=RequestedOutputs(**result["requested_outputs"]), + review=result.get("review", {"notes": "", "history": []}), + outputs=result.get("outputs"), + created_at=result["created_at"].isoformat(), + updated_at=result["updated_at"].isoformat() + ) + + +@router.delete("/{job_id}", response_model=JobDeleteResponse) +async def delete_job( + job_id: str, + current_user: User = Depends(get_current_user), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Delete a job and all associated assets""" + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + # Check permissions: clients can only delete their own jobs, admins/reviewers can delete any + if (current_user.role == UserRole.CLIENT and + job_doc["client_id"] != str(current_user.id)): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied" + ) + + logger.info(f"Deleting job {job_id} requested by {current_user.email}") + + try: + # 1. Cancel running Celery task if exists + task_id = job_doc.get("task_id") + if task_id: + try: + celery_app.control.revoke(task_id, terminate=True) + logger.info(f"Cancelled Celery task {task_id} for job {job_id}") + except Exception as e: + logger.warning(f"Could not cancel task {task_id}: {e}") + + # 2. Delete GCS assets + await _delete_job_gcs_assets(job_id, job_doc) + + # 3. Delete from database + result = await db.jobs.delete_one({"_id": job_id}) + + if result.deleted_count == 0: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + logger.info(f"Successfully deleted job {job_id}") + return {"message": f"Job {job_id} deleted successfully"} + + except Exception as e: + logger.error(f"Failed to delete job {job_id}: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete job: {str(e)}" + ) + + +async def _delete_job_gcs_assets(job_id: str, job_doc: dict): + """Delete all GCS assets for a job""" + try: + # Delete source file + source_gcs_uri = job_doc.get("source", {}).get("gcs_uri") + if source_gcs_uri: + blob_path = source_gcs_uri.replace(f"gs://{settings.gcs_bucket}/", "") + try: + blob = gcs_service.bucket.blob(blob_path) + blob.delete() + logger.info(f"Deleted source file: {blob_path}") + except Exception as e: + logger.warning(f"Could not delete source file {blob_path}: {e}") + + # Delete output files + outputs = job_doc.get("outputs", {}) + for lang, lang_outputs in outputs.items(): + if not isinstance(lang_outputs, dict): + continue + + # Delete VTT files + for key in ["captions_vtt_gcs", "ad_vtt_gcs", "ad_mp3_gcs"]: + if key in lang_outputs: + gcs_uri = lang_outputs[key] + blob_path = gcs_uri.replace(f"gs://{settings.gcs_bucket}/", "") + try: + blob = gcs_service.bucket.blob(blob_path) + blob.delete() + logger.info(f"Deleted output file: {blob_path}") + except Exception as e: + logger.warning(f"Could not delete output file {blob_path}: {e}") + + # Delete entire job folder if it exists + try: + blobs = gcs_service.bucket.list_blobs(prefix=f"{job_id}/") + for blob in blobs: + try: + blob.delete() + logger.info(f"Deleted remaining file: {blob.name}") + except Exception as e: + logger.warning(f"Could not delete {blob.name}: {e}") + except Exception as e: + logger.warning(f"Could not list/delete job folder {job_id}/: {e}") + + except Exception as e: + logger.error(f"Error deleting GCS assets for job {job_id}: {e}") + raise + + +@router.get("/{job_id}/validate", response_model=AssetValidationResponse) +async def validate_job_assets( + job_id: str, + current_user: User = Depends(require_roles(UserRole.REVIEWER, UserRole.ADMIN)), + db: AsyncIOMotorDatabase = Depends(get_database), +): + """Validate job assets before completion""" + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Job not found" + ) + + is_valid, errors = await asset_validation_service.validate_job_assets(job_doc) + + return AssetValidationResponse( + is_valid=is_valid, + errors=errors, + warnings=[] # Can be extended for non-blocking warnings + ) diff --git a/backend/app/core/__pycache__/config.cpython-313.pyc b/backend/app/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..d7c16c0 Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/database.cpython-313.pyc b/backend/app/core/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..6a99968 Binary files /dev/null and b/backend/app/core/__pycache__/database.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/dependencies.cpython-313.pyc b/backend/app/core/__pycache__/dependencies.cpython-313.pyc new file mode 100644 index 0000000..4d97949 Binary files /dev/null and b/backend/app/core/__pycache__/dependencies.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/logging.cpython-313.pyc b/backend/app/core/__pycache__/logging.cpython-313.pyc new file mode 100644 index 0000000..3a514bf Binary files /dev/null and b/backend/app/core/__pycache__/logging.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/redis.cpython-313.pyc b/backend/app/core/__pycache__/redis.cpython-313.pyc new file mode 100644 index 0000000..ac3564d Binary files /dev/null and b/backend/app/core/__pycache__/redis.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/secrets_config.cpython-313.pyc b/backend/app/core/__pycache__/secrets_config.cpython-313.pyc new file mode 100644 index 0000000..804a1fd Binary files /dev/null and b/backend/app/core/__pycache__/secrets_config.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-313.pyc b/backend/app/core/__pycache__/security.cpython-313.pyc new file mode 100644 index 0000000..e4f5cc4 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-313.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..fb1fc91 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,77 @@ + +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # App + app_env: str = "dev" + api_base_url: str = "http://localhost:8000" + + # Auth + jwt_secret: str + jwt_alg: str = "HS256" + jwt_access_ttl_min: int = 15 + jwt_refresh_ttl_days: int = 7 + cookie_domain: str = "localhost" + cookie_secure: bool = False + cookie_samesite: str = "Lax" + + # Database + mongodb_uri: str + mongodb_db: str = "accessible_video" + + # Redis + redis_url: str + + # Celery + celery_broker_url: str = "" + celery_result_backend: str = "" + + # GCP + gcp_project_id: str + gcs_bucket: str = "accessible-video" + google_application_credentials: str = "" + + # AI Services + gemini_api_key: str + translate_api_key: str = "" + elevenlabs_api_key: str = "" + google_tts_credentials: str = "" + + # TTS Voice Configuration + tts_provider: str = "google" # "google" or "elevenlabs" + google_tts_voices: dict[str, str] = { + "en-US": "en-US-Neural2-D", + "es-ES": "es-ES-Neural2-A", + "fr-FR": "fr-FR-Neural2-A", + "de-DE": "de-DE-Neural2-B" + } + elevenlabs_voices: dict[str, str] = { + "en-US": "21m00Tcm4TlvDq8ikWAM", + "es-ES": "VR6AewLTigWG4xSOukaG", + "fr-FR": "TxGEqnHWrfWFTfGW9XjX", + "de-DE": "pNInz6obpgDQGcFmaJgB" + } + + # Email + sendgrid_api_key: str + email_from: str + client_base_url: str + + # Observability + sentry_dsn: str = "" + otel_exporter_otlp_endpoint: str = "" + + # CORS + cors_origins: list[str] = ["http://localhost:5173", "http://localhost:3000"] + + class Config: + env_file = ".env" + + +settings = Settings() + + +def get_settings(): + """Get settings instance - for dependency injection""" + return settings diff --git a/backend/app/core/database.py b/backend/app/core/database.py new file mode 100644 index 0000000..ae32351 --- /dev/null +++ b/backend/app/core/database.py @@ -0,0 +1,67 @@ +from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorDatabase + +from ..core.logging import get_logger +from .config import settings + +logger = get_logger(__name__) + +class MongoDB: + client: AsyncIOMotorClient = None + database: AsyncIOMotorDatabase = None + + +mongodb = MongoDB() + + +async def connect_to_mongo(): + logger.info("Connecting to MongoDB...") + mongodb.client = AsyncIOMotorClient(settings.mongodb_uri) + mongodb.database = mongodb.client[settings.mongodb_db] + + # Test connection + try: + await mongodb.client.admin.command('ping') + logger.info("Successfully connected to MongoDB") + except Exception as e: + logger.error(f"Failed to connect to MongoDB: {e}") + raise + + +async def close_mongo_connection(): + logger.info("Closing MongoDB connection...") + if mongodb.client: + mongodb.client.close() + + +async def get_database() -> AsyncIOMotorDatabase: + return mongodb.database + + +async def create_indexes(): + """Create database indexes as specified in the development plan""" + db = mongodb.database + + # Jobs collection indexes + await db.jobs.create_index([("status", 1), ("created_at", -1)]) + await db.jobs.create_index([("client_id", 1)]) + + # Users collection indexes + await db.users.create_index([("email", 1)], unique=True) + + # Audit logs collection indexes - comprehensive indexing for audit queries + await db.audit_logs.create_index([("timestamp", -1)]) # Primary sort field + await db.audit_logs.create_index([("action", 1), ("timestamp", -1)]) # Filter by action + await db.audit_logs.create_index([("user_id", 1), ("timestamp", -1)]) # User activity + await db.audit_logs.create_index([("severity", 1), ("timestamp", -1)]) # Security events + await db.audit_logs.create_index([("resource_type", 1), ("resource_id", 1)]) # Resource tracking + await db.audit_logs.create_index([("ip_address", 1), ("timestamp", -1)]) # IP-based analysis + await db.audit_logs.create_index([("success", 1), ("timestamp", -1)]) # Failed operations + + # Text search index for description and details + await db.audit_logs.create_index([ + ("description", "text"), + ("details", "text"), + ("error_message", "text") + ]) + + logger.info("Database indexes created successfully") diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py new file mode 100644 index 0000000..7ff36c3 --- /dev/null +++ b/backend/app/core/dependencies.py @@ -0,0 +1,88 @@ +from typing import Optional + +from bson import ObjectId +from fastapi import Depends, HTTPException, Request, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from motor.motor_asyncio import AsyncIOMotorDatabase + +from ..models.user import User, UserRole +from .database import get_database +from .security import decode_token + +security = HTTPBearer() + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: AsyncIOMotorDatabase = Depends(get_database), +) -> User: + token = credentials.credentials + payload = decode_token(token) + user_id: str = payload.get("sub") + + if user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + ) + + user_doc = await db.users.find_one({"_id": ObjectId(user_id)}) + if user_doc is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found", + ) + + return User(**user_doc) + + +def require_role(required_role: UserRole): + async def role_checker(current_user: User = Depends(get_current_user)) -> User: + if current_user.role != required_role and current_user.role != UserRole.ADMIN: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + return current_user + + return role_checker + + +def require_roles(*required_roles: UserRole): + async def roles_checker(current_user: User = Depends(get_current_user)) -> User: + if current_user.role not in required_roles and current_user.role != UserRole.ADMIN: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Insufficient permissions", + ) + return current_user + + return roles_checker + + +async def get_current_user_optional( + request: Request, + db: AsyncIOMotorDatabase = Depends(get_database), +) -> Optional[User]: + authorization: str = request.headers.get("Authorization") + if not authorization: + return None + + try: + scheme, token = authorization.split() + if scheme.lower() != "bearer": + return None + + payload = decode_token(token) + user_id: str = payload.get("sub") + + if user_id is None: + return None + + user_doc = await db.users.find_one({"_id": ObjectId(user_id)}) + if user_doc is None: + return None + + return User(**user_doc) + except Exception: + return None diff --git a/backend/app/core/logging.py b/backend/app/core/logging.py new file mode 100644 index 0000000..997beea --- /dev/null +++ b/backend/app/core/logging.py @@ -0,0 +1,65 @@ +import logging +import sys +from typing import Any + + +class StructuredFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + log_entry = { + "timestamp": self.formatTime(record), + "level": record.levelname, + "logger": record.name, + "message": record.getMessage(), + } + + if hasattr(record, "extra_fields"): + log_entry.update(record.extra_fields) + + if record.exc_info: + log_entry["exception"] = self.formatException(record.exc_info) + + return str(log_entry) + + +def setup_logging() -> None: + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + # Remove default handlers + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + # Add structured handler + handler = logging.StreamHandler(sys.stdout) + handler.setFormatter(StructuredFormatter()) + root_logger.addHandler(handler) + + # Set levels for third-party loggers + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + logging.getLogger("httpx").setLevel(logging.WARNING) + + +def get_logger(name: str) -> logging.Logger: + return logging.getLogger(name) + + +class LogContext: + def __init__(self, logger: logging.Logger, **context: Any): + self.logger = logger + self.context = context + + def info(self, message: str, **extra: Any) -> None: + self._log(logging.INFO, message, **extra) + + def warning(self, message: str, **extra: Any) -> None: + self._log(logging.WARNING, message, **extra) + + def error(self, message: str, **extra: Any) -> None: + self._log(logging.ERROR, message, **extra) + + def _log(self, level: int, message: str, **extra: Any) -> None: + combined_extra = {**self.context, **extra} + record = self.logger.makeRecord( + self.logger.name, level, "", 0, message, (), None, extra_fields=combined_extra + ) + self.logger.handle(record) diff --git a/backend/app/core/redis.py b/backend/app/core/redis.py new file mode 100644 index 0000000..5eab205 --- /dev/null +++ b/backend/app/core/redis.py @@ -0,0 +1,49 @@ +import redis.asyncio as redis + +from .config import settings +from .logging import get_logger + +logger = get_logger(__name__) + +class RedisConnection: + pool: redis.ConnectionPool = None + client: redis.Redis = None + + +redis_conn = RedisConnection() + + +async def connect_to_redis(): + logger.info("Connecting to Redis...") + redis_conn.pool = redis.ConnectionPool.from_url( + settings.redis_url, + encoding="utf-8", + decode_responses=True, + max_connections=20, + ) + redis_conn.client = redis.Redis(connection_pool=redis_conn.pool) + + # Test connection + try: + await redis_conn.client.ping() + logger.info("Successfully connected to Redis") + except Exception as e: + logger.error(f"Failed to connect to Redis: {e}") + raise + + +async def close_redis_connection(): + logger.info("Closing Redis connection...") + if redis_conn.client: + await redis_conn.client.close() + if redis_conn.pool: + await redis_conn.pool.disconnect() + + +async def get_redis() -> redis.Redis: + return redis_conn.client + + +def get_redis_client() -> redis.Redis: + """Get the Redis client synchronously (for middleware setup).""" + return redis_conn.client diff --git a/backend/app/core/secrets_config.py b/backend/app/core/secrets_config.py new file mode 100644 index 0000000..d05153e --- /dev/null +++ b/backend/app/core/secrets_config.py @@ -0,0 +1,145 @@ +"""Enhanced configuration system with Secret Manager integration.""" + +import os +import asyncio +from typing import Dict, Optional, Any +from functools import lru_cache +from pydantic_settings import BaseSettings + +from .config import Settings as BaseConfig +from .logging import get_logger + +logger = get_logger(__name__) + + +class SecretsConfig(BaseConfig): + """Enhanced configuration that loads secrets from GCP Secret Manager.""" + + def __init__(self, **kwargs): + # Initialize with base configuration first + super().__init__(**kwargs) + + # Flag to track if secrets have been loaded + self._secrets_loaded = False + self._secret_values: Dict[str, str] = {} + + async def load_secrets(self) -> None: + """Load secrets from Secret Manager asynchronously.""" + if self._secrets_loaded: + return + + try: + # Only import here to avoid circular imports + from app.services.secrets_manager import secrets_manager + + # Define which config fields should be loaded from secrets + secret_mappings = { + # Config field -> Secret Manager name + "jwt_secret": "jwt-secret", + "jwt_refresh_secret": "jwt-refresh-secret", + "mongodb_uri": "mongodb-url", + "redis_url": "redis-url", + "gemini_api_key": "gemini-api-key", + "sendgrid_api_key": "sendgrid-api-key", + "elevenlabs_api_key": "elevenlabs-api-key", + "sentry_dsn": "sentry-dsn" + } + + # Get all secrets in batch + secret_names = list(secret_mappings.values()) + retrieved_secrets = await secrets_manager.get_secrets_batch(secret_names) + + # Map secrets back to config fields + for config_field, secret_name in secret_mappings.items(): + if secret_name in retrieved_secrets: + self._secret_values[config_field] = retrieved_secrets[secret_name] + # Override the config value + setattr(self, config_field, retrieved_secrets[secret_name]) + logger.debug(f"Loaded secret for {config_field}") + else: + logger.warning(f"Secret {secret_name} not available, using environment/default") + + self._secrets_loaded = True + logger.info(f"Successfully loaded {len(retrieved_secrets)} secrets from Secret Manager") + + except Exception as e: + logger.warning(f"Failed to load secrets from Secret Manager: {e}") + logger.warning("Falling back to environment variables") + self._secrets_loaded = True # Mark as loaded to prevent retries + + def get_secret_value(self, field_name: str) -> Optional[str]: + """Get a secret value if it was loaded from Secret Manager.""" + return self._secret_values.get(field_name) + + async def refresh_secrets(self) -> None: + """Force refresh secrets from Secret Manager.""" + self._secrets_loaded = False + self._secret_values.clear() + + # Clear the secrets manager cache + from app.services.secrets_manager import secrets_manager + secrets_manager.clear_cache() + + await self.load_secrets() + + @property + def is_production(self) -> bool: + """Check if running in production environment.""" + return self.app_env == "prod" + + @property + def is_development(self) -> bool: + """Check if running in development environment.""" + return self.app_env == "dev" + + @property + def google_cloud_project(self) -> str: + """Get Google Cloud Project ID.""" + return self.gcp_project_id + + @property + def jwt_refresh_secret(self) -> str: + """Get JWT refresh secret (fallback to main secret if not set).""" + return getattr(self, '_jwt_refresh_secret', self.jwt_secret) + + @jwt_refresh_secret.setter + def jwt_refresh_secret(self, value: str) -> None: + """Set JWT refresh secret.""" + self._jwt_refresh_secret = value + + +# Global configuration instance +_config_instance: Optional[SecretsConfig] = None + + +async def initialize_config() -> SecretsConfig: + """Initialize configuration with secrets loading.""" + global _config_instance + + if _config_instance is None: + _config_instance = SecretsConfig() + await _config_instance.load_secrets() + + return _config_instance + + +def get_settings() -> SecretsConfig: + """Get settings instance (synchronous).""" + global _config_instance + + if _config_instance is None: + # Initialize without secrets for backwards compatibility + _config_instance = SecretsConfig() + logger.warning("Settings accessed before async initialization - secrets not loaded") + + return _config_instance + + +@lru_cache() +def get_settings_cached() -> SecretsConfig: + """Get cached settings instance.""" + return get_settings() + + +# Backwards compatibility +settings = get_settings() \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..960c33b --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,55 @@ +from datetime import datetime, timedelta +from typing import Any, Optional, Union + +from fastapi import HTTPException, status +from jose import JWTError, jwt +from passlib.context import CryptContext + +from .config import settings + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def create_access_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None +) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.jwt_access_ttl_min) + + to_encode = {"exp": expire, "sub": str(subject)} + encoded_jwt = jwt.encode(to_encode, settings.jwt_secret, algorithm=settings.jwt_alg) + return encoded_jwt + + +def create_refresh_token( + subject: Union[str, Any], expires_delta: Optional[timedelta] = None +) -> str: + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(days=settings.jwt_refresh_ttl_days) + + to_encode = {"exp": expire, "sub": str(subject), "type": "refresh"} + encoded_jwt = jwt.encode(to_encode, settings.jwt_secret, algorithm=settings.jwt_alg) + return encoded_jwt + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + + +def decode_token(token: str) -> dict[str, Any]: + try: + payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_alg]) + return payload + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + ) diff --git a/backend/app/lib/__pycache__/vtt.cpython-313.pyc b/backend/app/lib/__pycache__/vtt.cpython-313.pyc new file mode 100644 index 0000000..767c208 Binary files /dev/null and b/backend/app/lib/__pycache__/vtt.cpython-313.pyc differ diff --git a/backend/app/lib/vtt.py b/backend/app/lib/vtt.py new file mode 100644 index 0000000..97dfe2e --- /dev/null +++ b/backend/app/lib/vtt.py @@ -0,0 +1,222 @@ +import re +from dataclasses import dataclass + + +@dataclass +class VTTCue: + start_time: float # seconds + end_time: float # seconds + text: str + identifier: str | None = None + + +class VTTParser: + """Parser and builder for WebVTT files""" + + @staticmethod + def parse(vtt_content: str) -> list[VTTCue]: + """Parse VTT content into a list of cues""" + lines = vtt_content.strip().split('\n') + cues = [] + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Skip WEBVTT header, empty lines, and NOTE lines + if line == "WEBVTT" or line == "" or line.startswith("NOTE"): + i += 1 + continue + + # Check if this line is a cue identifier (optional) + identifier = None + if " --> " not in line and i + 1 < len(lines) and " --> " in lines[i + 1]: + identifier = line + i += 1 + line = lines[i].strip() + + # Parse timing line + if " --> " in line: + timing_match = re.match(r'([\d:.,]+)\s+-->\s+([\d:.,]+)', line) + if timing_match: + start_time = VTTParser._parse_timestamp(timing_match.group(1)) + end_time = VTTParser._parse_timestamp(timing_match.group(2)) + + # Collect text lines until empty line or next cue + i += 1 + text_lines = [] + while i < len(lines) and lines[i].strip() != "": + text_lines.append(lines[i].strip()) + i += 1 + + if text_lines: + cues.append(VTTCue( + start_time=start_time, + end_time=end_time, + text="\n".join(text_lines), + identifier=identifier + )) + else: + i += 1 + + return cues + + @staticmethod + def build(cues: list[VTTCue]) -> str: + """Build VTT content from a list of cues""" + lines = ["WEBVTT", ""] + + for cue in cues: + # Add identifier if present + if cue.identifier: + lines.append(cue.identifier) + + # Add timing line + start_timestamp = VTTParser._format_timestamp(cue.start_time) + end_timestamp = VTTParser._format_timestamp(cue.end_time) + lines.append(f"{start_timestamp} --> {end_timestamp}") + + # Add text (can be multi-line) + lines.append(cue.text) + lines.append("") # Empty line between cues + + return "\n".join(lines) + + @staticmethod + def _parse_timestamp(timestamp: str) -> float: + """Convert VTT timestamp (HH:MM:SS.mmm or MM:SS.mmm) to seconds""" + # Clean up timestamp (handle both . and , as decimal separator) + timestamp = timestamp.replace(',', '.') + + # Split by colon + parts = timestamp.split(':') + + if len(parts) == 3: # HH:MM:SS.mmm + hours, minutes, seconds = parts + elif len(parts) == 2: # MM:SS.mmm + hours, minutes, seconds = "0", parts[0], parts[1] + else: + raise ValueError(f"Invalid timestamp format: {timestamp}") + + # Parse seconds and decimal part + sec_parts = seconds.split('.') + whole_seconds = int(sec_parts[0]) + decimal_part = int(sec_parts[1]) if len(sec_parts) > 1 else 0 + + # Convert to total seconds + total_seconds = ( + int(hours) * 3600 + + int(minutes) * 60 + + whole_seconds + + decimal_part / 1000.0 + ) + + return total_seconds + + @staticmethod + def _format_timestamp(seconds: float) -> str: + """Convert seconds to VTT timestamp format (HH:MM:SS.mmm)""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = seconds % 60 + + whole_secs = int(secs) + milliseconds = int((secs - whole_secs) * 1000) + + return f"{hours:02d}:{minutes:02d}:{whole_secs:02d}.{milliseconds:03d}" + + +class VTTEditor: + """Utility class for editing VTT content while preserving timing""" + + @staticmethod + def translate_preserving_timing( + vtt_content: str, + translated_texts: list[str] + ) -> str: + """Replace text in VTT cues while preserving all timing information""" + cues = VTTParser.parse(vtt_content) + + if len(translated_texts) != len(cues): + raise ValueError( + f"Text count mismatch: {len(translated_texts)} texts for {len(cues)} cues" + ) + + # Update cue texts + for i, translated_text in enumerate(translated_texts): + cues[i].text = translated_text + + return VTTParser.build(cues) + + @staticmethod + def update_cue_text(vtt_content: str, cue_index: int, new_text: str) -> str: + """Update text for a specific cue by index""" + cues = VTTParser.parse(vtt_content) + + if cue_index < 0 or cue_index >= len(cues): + raise ValueError(f"Invalid cue index: {cue_index}") + + cues[cue_index].text = new_text + return VTTParser.build(cues) + + @staticmethod + def validate_vtt(vtt_content: str) -> tuple[bool, list[str]]: + """Validate VTT content and return errors if any""" + errors = [] + + if not vtt_content.strip().startswith("WEBVTT"): + errors.append("VTT must start with 'WEBVTT'") + + try: + cues = VTTParser.parse(vtt_content) + + # Check timing consistency + for i, cue in enumerate(cues): + if cue.start_time >= cue.end_time: + errors.append(f"Cue {i + 1}: Start time must be before end time") + + if i > 0 and cue.start_time < cues[i - 1].end_time: + errors.append(f"Cue {i + 1}: Overlapping with previous cue") + + if not cue.text.strip(): + errors.append(f"Cue {i + 1}: Empty text content") + + except Exception as e: + errors.append(f"Parse error: {str(e)}") + + return len(errors) == 0, errors + + @staticmethod + def get_cue_count(vtt_content: str) -> int: + """Get the number of cues in VTT content""" + try: + cues = VTTParser.parse(vtt_content) + return len(cues) + except Exception: + return 0 + + @staticmethod + def get_total_duration(vtt_content: str) -> float: + """Get total duration of VTT content in seconds""" + try: + cues = VTTParser.parse(vtt_content) + if not cues: + return 0.0 + return max(cue.end_time for cue in cues) + except Exception: + return 0.0 + + @staticmethod + def adjust_timing_offset(vtt_content: str, offset_seconds: float) -> str: + """ + Adjust all VTT cue timings by a fixed offset + Positive offset moves captions later, negative moves them earlier + """ + cues = VTTParser.parse(vtt_content) + + for cue in cues: + cue.start_time = max(0.0, cue.start_time + offset_seconds) + cue.end_time = max(cue.start_time + 0.5, cue.end_time + offset_seconds) + + return VTTParser.build(cues) + diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..0b7c3cb --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,216 @@ +from contextlib import asynccontextmanager + +import sentry_sdk +from fastapi import FastAPI, Request, HTTPException +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from sentry_sdk.integrations.fastapi import FastApiIntegration +from sentry_sdk.integrations.redis import RedisIntegration +from sentry_sdk.integrations.pymongo import PyMongoIntegration +from sentry_sdk.integrations.celery import CeleryIntegration + +from .api.v1.routes_admin import router as admin_router +from .api.v1.routes_auth import router as auth_router +from .api.v1.routes_files import router as files_router +from .api.v1.routes_jobs import router as jobs_router +from .core.config import settings +from .core.secrets_config import initialize_config +from .core.database import close_mongo_connection, connect_to_mongo, create_indexes +from .core.logging import setup_logging +from .core.redis import close_redis_connection, connect_to_redis, get_redis_client +from .middleware import create_rate_limit_middleware, create_validation_middleware +from .telemetry import ( + app_metrics, + instrument_dependencies, + instrument_fastapi_app, + setup_tracing +) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + setup_logging() + + # Initialize configuration with secrets + if settings.app_env == "prod": + try: + await initialize_config() + print("✅ Configuration initialized with Secret Manager") + except Exception as e: + print(f"⚠️ Failed to load secrets from Secret Manager: {e}") + print("⚠️ Falling back to environment variables") + + # Initialize Sentry error tracking + if settings.sentry_dsn and settings.sentry_dsn.startswith(('http', 'https')): + sentry_sdk.init( + dsn=settings.sentry_dsn, + integrations=[ + FastApiIntegration(), + RedisIntegration(), + PyMongoIntegration(), + CeleryIntegration(monitor_beat_tasks=True), + ], + traces_sample_rate=0.1 if settings.app_env == "prod" else 1.0, + environment=settings.app_env, + release="1.0.0", + attach_stacktrace=True, + send_default_pii=False, # Don't send PII for privacy + ) + + # Initialize telemetry (disabled for local development) + # setup_tracing("accessible-video-api", "1.0.0") + # instrument_dependencies() + + # Start Prometheus metrics server in production + if settings.app_env == "prod": + app_metrics.start_prometheus_server(port=8001) + + await connect_to_mongo() + await connect_to_redis() + # await create_indexes() # Temporarily disabled for debugging + + # Initialize middleware with Redis client + redis_client = get_redis_client() + if redis_client: + rate_limit_middleware = await create_rate_limit_middleware(redis_client) + validation_middleware = await create_validation_middleware() + + # Store middleware in app state for access + app.state.rate_limit_middleware = rate_limit_middleware + app.state.validation_middleware = validation_middleware + + yield + # Shutdown + await close_mongo_connection() + await close_redis_connection() + + +app = FastAPI( + title="Accessible Video API", + description="API for accessible video processing platform", + version="1.0.0", + lifespan=lifespan, +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], + allow_headers=["*"], +) + +# Custom CORS error handler middleware to ensure CORS headers are added to all error responses +# This must be added BEFORE CORSMiddleware (which will be applied after due to reverse order) +@app.middleware("http") +async def cors_error_handler(request, call_next): + """Ensure CORS headers are added to all responses, including errors.""" + try: + response = await call_next(request) + except Exception as e: + # Handle any unhandled exceptions and add CORS headers + from fastapi.responses import JSONResponse + response = JSONResponse( + status_code=500, + content={"detail": "Internal server error"} + ) + + # Always add CORS headers for allowed origins + origin = request.headers.get("origin") + if origin and origin in settings.cors_origins: + response.headers["access-control-allow-origin"] = origin + response.headers["access-control-allow-credentials"] = "true" + # Add other necessary CORS headers for error responses + if response.status_code >= 400: + response.headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE" + response.headers["access-control-allow-headers"] = "*" + + return response + +# Global exception handler to ensure CORS headers on all errors +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """Handle HTTP exceptions with CORS headers""" + response = JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail} + ) + + # Add CORS headers + origin = request.headers.get("origin") + if origin and origin in settings.cors_origins: + response.headers["access-control-allow-origin"] = origin + response.headers["access-control-allow-credentials"] = "true" + response.headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE" + response.headers["access-control-allow-headers"] = "*" + + return response + +# Global exception handler for validation errors +@app.exception_handler(RequestValidationError) +async def validation_exception_handler(request: Request, exc: RequestValidationError): + """Handle request validation errors with CORS headers""" + response = JSONResponse( + status_code=422, + content={"detail": exc.errors(), "body": exc.body} + ) + + # Add CORS headers + origin = request.headers.get("origin") + if origin and origin in settings.cors_origins: + response.headers["access-control-allow-origin"] = origin + response.headers["access-control-allow-credentials"] = "true" + response.headers["access-control-allow-methods"] = "GET, POST, PUT, PATCH, DELETE" + response.headers["access-control-allow-headers"] = "*" + + return response + +# Add custom middleware (order matters - applied in reverse order) +@app.middleware("http") +async def rate_limiting_middleware(request, call_next): + """Apply rate limiting middleware.""" + # Skip middleware for auth endpoints during debugging + if request.url.path in ["/api/v1/auth/login", "/api/v1/auth/refresh"]: + return await call_next(request) + if hasattr(app.state, 'rate_limit_middleware'): + return await app.state.rate_limit_middleware(request, call_next) + return await call_next(request) + +@app.middleware("http") +async def validation_middleware(request, call_next): + """Apply request validation middleware.""" + # Skip middleware for auth endpoints during debugging + if request.url.path in ["/api/v1/auth/login", "/api/v1/auth/refresh"]: + return await call_next(request) + if hasattr(app.state, 'validation_middleware'): + return await app.state.validation_middleware(request, call_next) + return await call_next(request) + +# Instrument FastAPI app for tracing (disabled for local development) +# instrument_fastapi_app(app) + +# Include routers +app.include_router(auth_router, prefix="/api/v1") +app.include_router(files_router, prefix="/api/v1") +app.include_router(jobs_router, prefix="/api/v1") +app.include_router(admin_router, prefix="/api/v1") + + +@app.get("/health") +async def health_check(): + return {"status": "healthy", "version": "1.0.0"} + + +@app.get("/metrics") +async def metrics(): + """Prometheus metrics endpoint""" + from prometheus_client import generate_latest, CONTENT_TYPE_LATEST + from fastapi import Response + + return Response( + content=generate_latest(), + media_type=CONTENT_TYPE_LATEST + ) diff --git a/backend/app/middleware/__init__.py b/backend/app/middleware/__init__.py new file mode 100644 index 0000000..3c361b0 --- /dev/null +++ b/backend/app/middleware/__init__.py @@ -0,0 +1,12 @@ +"""Middleware package for FastAPI application.""" + +from .rate_limiting import RateLimitMiddleware, IPWhitelist, create_rate_limit_middleware +from .validation import ValidationMiddleware, create_validation_middleware + +__all__ = [ + "RateLimitMiddleware", + "IPWhitelist", + "create_rate_limit_middleware", + "ValidationMiddleware", + "create_validation_middleware" +] \ No newline at end of file diff --git a/backend/app/middleware/__pycache__/__init__.cpython-313.pyc b/backend/app/middleware/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..b547000 Binary files /dev/null and b/backend/app/middleware/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc b/backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc new file mode 100644 index 0000000..42e5913 Binary files /dev/null and b/backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc differ diff --git a/backend/app/middleware/__pycache__/validation.cpython-313.pyc b/backend/app/middleware/__pycache__/validation.cpython-313.pyc new file mode 100644 index 0000000..aa6148c Binary files /dev/null and b/backend/app/middleware/__pycache__/validation.cpython-313.pyc differ diff --git a/backend/app/middleware/rate_limiting.py b/backend/app/middleware/rate_limiting.py new file mode 100644 index 0000000..76a7cdf --- /dev/null +++ b/backend/app/middleware/rate_limiting.py @@ -0,0 +1,264 @@ +"""Rate limiting middleware for API endpoints.""" + +import time +from collections import defaultdict +from typing import Dict, Optional, Tuple +import redis.asyncio as aioredis +from fastapi import HTTPException, Request, status +from fastapi.responses import JSONResponse +import json +import asyncio +from datetime import datetime, timedelta + +from app.core.config import get_settings +from app.telemetry.metrics import track_rate_limit_metrics + + +class RateLimiter: + """Redis-based rate limiter with sliding window algorithm.""" + + def __init__(self, redis_client: aioredis.Redis): + self.redis = redis_client + + async def is_allowed( + self, + key: str, + limit: int, + window_seconds: int, + identifier: str = "" + ) -> Tuple[bool, Dict[str, int]]: + """ + Check if request is allowed under rate limit. + + Returns: + Tuple of (is_allowed, rate_limit_info) + """ + now = time.time() + pipeline = self.redis.pipeline() + + # Remove expired entries + pipeline.zremrangebyscore(key, 0, now - window_seconds) + + # Count current requests in window + pipeline.zcard(key) + + # Add current request + pipeline.zadd(key, {str(now): now}) + + # Set expiry + pipeline.expire(key, window_seconds) + + results = await pipeline.execute() + current_requests = results[1] + + rate_limit_info = { + "limit": limit, + "remaining": max(0, limit - current_requests), + "reset_time": int(now + window_seconds), + "retry_after": window_seconds if current_requests >= limit else 0 + } + + is_allowed = current_requests <= limit + + # Track metrics + track_rate_limit_metrics( + identifier=identifier, + is_allowed=is_allowed, + current_requests=current_requests, + limit=limit + ) + + return is_allowed, rate_limit_info + + +class RateLimitMiddleware: + """FastAPI middleware for rate limiting.""" + + def __init__(self, redis_client: aioredis.Redis): + self.limiter = RateLimiter(redis_client) + self.settings = get_settings() + + # Rate limit configurations by endpoint pattern + self.rate_limits = { + # Authentication endpoints + "POST:/api/v1/auth/login": (5, 300), # 5 requests per 5 minutes + "POST:/api/v1/auth/register": (3, 3600), # 3 requests per hour + "POST:/api/v1/auth/refresh": (10, 300), # 10 requests per 5 minutes + "POST:/api/v1/auth/forgot-password": (3, 3600), # 3 requests per hour + + # File upload endpoints + "POST:/api/v1/files/upload": (10, 3600), # 10 uploads per hour + "POST:/api/v1/jobs": (20, 3600), # 20 job creations per hour + + # Job management endpoints + "GET:/api/v1/jobs": (100, 300), # 100 requests per 5 minutes + "PATCH:/api/v1/jobs/*/approve": (50, 3600), # 50 approvals per hour + "PATCH:/api/v1/jobs/*/reject": (50, 3600), # 50 rejections per hour + + # VTT editing endpoints + "PATCH:/api/v1/jobs/*/vtt": (100, 3600), # 100 VTT edits per hour + + # Admin endpoints (more restrictive) + "GET:/api/v1/admin/*": (50, 300), # 50 requests per 5 minutes + "POST:/api/v1/admin/*": (20, 3600), # 20 admin actions per hour + "PATCH:/api/v1/admin/*": (20, 3600), # 20 admin updates per hour + "DELETE:/api/v1/admin/*": (10, 3600), # 10 admin deletions per hour + } + + # Default rate limits + self.default_limits = { + "authenticated": (1000, 3600), # 1000 requests per hour for authenticated users + "anonymous": (100, 3600), # 100 requests per hour for anonymous users + } + + def _get_client_identifier(self, request: Request) -> str: + """Get client identifier for rate limiting.""" + # Try to get user ID from JWT token + user = getattr(request.state, 'user', None) + if user: + return f"user:{user.id}" + + # Fall back to IP address + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + return f"ip:{forwarded_for.split(',')[0].strip()}" + + client_ip = request.client.host if request.client else "unknown" + return f"ip:{client_ip}" + + def _get_endpoint_key(self, request: Request) -> str: + """Get endpoint pattern for rate limiting.""" + method = request.method + path = request.url.path + + # Replace job IDs with wildcard for pattern matching + import re + path = re.sub(r'/jobs/[a-f0-9-]+/', '/jobs/*/', path) + path = re.sub(r'/admin/users/[a-f0-9-]+', '/admin/users/*', path) + + return f"{method}:{path}" + + def _get_rate_limit(self, request: Request) -> Tuple[int, int]: + """Get rate limit for the current request.""" + endpoint_key = self._get_endpoint_key(request) + + # Check for specific endpoint limits + if endpoint_key in self.rate_limits: + return self.rate_limits[endpoint_key] + + # Check for wildcard matches + for pattern, limits in self.rate_limits.items(): + if pattern.endswith("*") and endpoint_key.startswith(pattern[:-1]): + return limits + + # Use default limits based on authentication + user = getattr(request.state, 'user', None) + if user: + return self.default_limits["authenticated"] + else: + return self.default_limits["anonymous"] + + async def __call__(self, request: Request, call_next): + """Process rate limiting for the request.""" + + # Skip rate limiting for health checks and login (temporary for debugging) + if request.url.path in ["/health", "/metrics", "/api/v1/auth/login"]: + return await call_next(request) + + client_id = self._get_client_identifier(request) + endpoint_key = self._get_endpoint_key(request) + limit, window = self._get_rate_limit(request) + + # Create rate limit key + rate_limit_key = f"rate_limit:{client_id}:{endpoint_key}" + + try: + is_allowed, rate_info = await self.limiter.is_allowed( + key=rate_limit_key, + limit=limit, + window_seconds=window, + identifier=client_id + ) + + if not is_allowed: + # Return rate limit exceeded response + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content={ + "detail": "Rate limit exceeded", + "error_code": "RATE_LIMIT_EXCEEDED", + "rate_limit": rate_info + }, + headers={ + "X-RateLimit-Limit": str(rate_info["limit"]), + "X-RateLimit-Remaining": str(rate_info["remaining"]), + "X-RateLimit-Reset": str(rate_info["reset_time"]), + "Retry-After": str(rate_info["retry_after"]) + } + ) + + # Process the request + response = await call_next(request) + + # Add rate limit headers to response + response.headers["X-RateLimit-Limit"] = str(rate_info["limit"]) + response.headers["X-RateLimit-Remaining"] = str(rate_info["remaining"]) + response.headers["X-RateLimit-Reset"] = str(rate_info["reset_time"]) + + return response + + except Exception as e: + # Log error but don't block request if rate limiting fails + print(f"Rate limiting error: {e}") + return await call_next(request) + + +class IPWhitelist: + """IP whitelist for bypassing rate limits.""" + + def __init__(self, redis_client: aioredis.Redis): + self.redis = redis_client + self.whitelist_key = "ip_whitelist" + + # Default whitelisted IPs (health checks, monitoring) + self.default_whitelist = { + "127.0.0.1", + "::1", + "169.254.169.254", # GCP metadata server + } + + async def is_whitelisted(self, ip: str) -> bool: + """Check if IP is whitelisted.""" + if ip in self.default_whitelist: + return True + + try: + is_member = await self.redis.sismember(self.whitelist_key, ip) + return bool(is_member) + except Exception: + return False + + async def add_ip(self, ip: str, ttl_seconds: Optional[int] = None) -> bool: + """Add IP to whitelist.""" + try: + await self.redis.sadd(self.whitelist_key, ip) + if ttl_seconds: + # Create temporary whitelist entry + temp_key = f"{self.whitelist_key}:temp:{ip}" + await self.redis.setex(temp_key, ttl_seconds, "1") + return True + except Exception: + return False + + async def remove_ip(self, ip: str) -> bool: + """Remove IP from whitelist.""" + try: + await self.redis.srem(self.whitelist_key, ip) + return True + except Exception: + return False + + +async def create_rate_limit_middleware(redis_client: aioredis.Redis) -> RateLimitMiddleware: + """Factory function to create rate limit middleware.""" + return RateLimitMiddleware(redis_client) \ No newline at end of file diff --git a/backend/app/middleware/validation.py b/backend/app/middleware/validation.py new file mode 100644 index 0000000..54c629e --- /dev/null +++ b/backend/app/middleware/validation.py @@ -0,0 +1,324 @@ +"""Enhanced request validation middleware.""" + +import json +import re +import time +from typing import Any, Dict, List, Optional, Set +from fastapi import HTTPException, Request, status +from fastapi.responses import JSONResponse +from pydantic import BaseModel, ValidationError as PydanticValidationError +import magic +from urllib.parse import unquote + +from app.telemetry.metrics import track_validation_metrics + + +class ValidationError(Exception): + """Custom validation error.""" + pass + + +class SecurityValidationError(Exception): + """Raised when security validation fails.""" + pass + + +class RequestValidator: + """Enhanced request validation with security checks.""" + + def __init__(self): + # File type restrictions + self.allowed_video_types = { + "video/mp4", + "video/quicktime", + "video/x-msvideo" # AVI + } + + self.allowed_subtitle_types = { + "text/vtt", + "text/plain" + } + + # Security patterns to block + self.malicious_patterns = [ + # SQL injection patterns + r"(union|select|insert|update|delete|drop|create|alter)\s+", + r"(script|javascript|vbscript|onload|onerror|onclick)", + r"<\s*script[^>]*>", + r"javascript:", + r"data:.*base64", + + # Path traversal + r"\.\./", + r"\.\.\\", + r"%2e%2e%2f", + r"%2e%2e\\", + + # Command injection + r"[;&|`$]", + r"(rm|wget|curl|nc|bash|sh|cmd|powershell)\s+", + + # MongoDB injection + r"\$where|\$ne|\$gt|\$lt|\$regex", + ] + + self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.malicious_patterns] + + # Max file sizes (in bytes) + self.max_video_size = 2 * 1024 * 1024 * 1024 # 2GB + self.max_subtitle_size = 10 * 1024 * 1024 # 10MB + + # Request size limits + self.max_json_size = 1024 * 1024 # 1MB + self.max_form_fields = 50 + + def validate_string_content(self, content: str, field_name: str = "input") -> None: + """Validate string content for malicious patterns.""" + if not isinstance(content, str): + return + + for pattern in self.compiled_patterns: + if pattern.search(content): + raise SecurityValidationError( + f"Potentially malicious content detected in {field_name}" + ) + + def validate_filename(self, filename: str) -> str: + """Validate and sanitize filename.""" + if not filename: + raise ValidationError("Filename cannot be empty") + + # Decode URL encoding + filename = unquote(filename) + + # Check for malicious patterns + self.validate_string_content(filename, "filename") + + # Remove dangerous characters + safe_filename = re.sub(r'[^\w\-_\.]', '_', filename) + + # Prevent hidden files + if safe_filename.startswith('.'): + safe_filename = 'file_' + safe_filename[1:] + + # Limit length + if len(safe_filename) > 255: + name, ext = safe_filename.rsplit('.', 1) if '.' in safe_filename else (safe_filename, '') + safe_filename = name[:250] + ('.' + ext if ext else '') + + return safe_filename + + def validate_file_type(self, content: bytes, expected_type: str, filename: str) -> None: + """Validate file type using magic numbers.""" + try: + detected_type = magic.from_buffer(content, mime=True) + except Exception: + # Fallback to extension-based validation + ext = filename.lower().split('.')[-1] if '.' in filename else '' + video_extensions = {'mp4', 'mov', 'avi', 'mkv'} + subtitle_extensions = {'vtt', 'srt', 'txt'} + + if expected_type == "video" and ext not in video_extensions: + raise ValidationError(f"Invalid video file extension: {ext}") + elif expected_type == "subtitle" and ext not in subtitle_extensions: + raise ValidationError(f"Invalid subtitle file extension: {ext}") + return + + if expected_type == "video" and detected_type not in self.allowed_video_types: + raise ValidationError( + f"Invalid video file type: {detected_type}. " + f"Allowed types: {', '.join(self.allowed_video_types)}" + ) + elif expected_type == "subtitle" and detected_type not in self.allowed_subtitle_types: + raise ValidationError( + f"Invalid subtitle file type: {detected_type}. " + f"Allowed types: {', '.join(self.allowed_subtitle_types)}" + ) + + def validate_file_size(self, size: int, file_type: str) -> None: + """Validate file size limits.""" + if file_type == "video" and size > self.max_video_size: + raise ValidationError( + f"Video file too large: {size} bytes. " + f"Maximum allowed: {self.max_video_size} bytes" + ) + elif file_type == "subtitle" and size > self.max_subtitle_size: + raise ValidationError( + f"Subtitle file too large: {size} bytes. " + f"Maximum allowed: {self.max_subtitle_size} bytes" + ) + + async def validate_json_payload(self, request: Request) -> Optional[Dict[str, Any]]: + """Validate JSON request payload.""" + if not request.headers.get("content-type", "").startswith("application/json"): + return None + + content_length = request.headers.get("content-length") + if content_length and int(content_length) > self.max_json_size: + raise ValidationError(f"JSON payload too large: {content_length} bytes") + + try: + # Check if body has already been read + if hasattr(request, '_cached_body'): + body = request._cached_body + else: + body = await request.body() + # Cache the body so FastAPI can read it later + request._cached_body = body + + if len(body) > self.max_json_size: + raise ValidationError(f"JSON payload too large: {len(body)} bytes") + + if not body: + return {} + + payload = json.loads(body) + + # Recursively validate all string values + self._validate_json_values(payload) + + return payload + + except json.JSONDecodeError as e: + raise ValidationError(f"Invalid JSON: {e}") + + def _validate_json_values(self, obj: Any, path: str = "root") -> None: + """Recursively validate JSON values.""" + if isinstance(obj, dict): + if len(obj) > self.max_form_fields: + raise ValidationError(f"Too many fields in object at {path}") + + for key, value in obj.items(): + if isinstance(key, str): + self.validate_string_content(key, f"{path}.{key}") + self._validate_json_values(value, f"{path}.{key}") + + elif isinstance(obj, list): + if len(obj) > 1000: # Prevent large arrays + raise ValidationError(f"Array too large at {path}") + + for i, item in enumerate(obj): + self._validate_json_values(item, f"{path}[{i}]") + + elif isinstance(obj, str): + self.validate_string_content(obj, path) + + def validate_query_params(self, request: Request) -> None: + """Validate query parameters.""" + for key, value in request.query_params.items(): + self.validate_string_content(key, f"query.{key}") + self.validate_string_content(str(value), f"query.{key}") + + def validate_headers(self, request: Request) -> None: + """Validate request headers.""" + suspicious_headers = { + "x-forwarded-host", + "x-original-host", + "x-rewrite-url" + } + + for header_name, header_value in request.headers.items(): + # Check for suspicious headers + if header_name.lower() in suspicious_headers: + self.validate_string_content(header_value, f"header.{header_name}") + + # Validate user-agent length + if header_name.lower() == "user-agent" and len(header_value) > 500: + raise SecurityValidationError("User-Agent header too long") + + +class ValidationMiddleware: + """FastAPI middleware for enhanced request validation.""" + + def __init__(self): + self.validator = RequestValidator() + + async def __call__(self, request: Request, call_next): + """Process validation for the request.""" + + start_time = time.time() + validation_errors = [] + + # Skip validation for timing adjustment endpoint temporarily + if "/vtt/adjust-timing" in request.url.path: + return await call_next(request) + + try: + # Validate headers + self.validator.validate_headers(request) + + # Validate query parameters + self.validator.validate_query_params(request) + + # Validate JSON payload if present + if request.method in ["POST", "PUT", "PATCH"]: + await self.validator.validate_json_payload(request) + + # Process the request + response = await call_next(request) + + # Track successful validation + track_validation_metrics( + endpoint=request.url.path, + method=request.method, + is_valid=True, + validation_time=time.time() - start_time, + error_types=[] + ) + + return response + + except SecurityValidationError as e: + validation_errors.append("security") + track_validation_metrics( + endpoint=request.url.path, + method=request.method, + is_valid=False, + validation_time=time.time() - start_time, + error_types=validation_errors + ) + + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={ + "detail": "Security validation failed", + "error_code": "SECURITY_VALIDATION_ERROR" + } + ) + + except ValidationError as e: + validation_errors.append("format") + track_validation_metrics( + endpoint=request.url.path, + method=request.method, + is_valid=False, + validation_time=time.time() - start_time, + error_types=validation_errors + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "detail": str(e), + "error_code": "VALIDATION_ERROR" + } + ) + + except Exception as e: + validation_errors.append("unknown") + track_validation_metrics( + endpoint=request.url.path, + method=request.method, + is_valid=False, + validation_time=time.time() - start_time, + error_types=validation_errors + ) + + # Log unexpected error but continue processing + print(f"Validation middleware error: {e}") + return await call_next(request) + + +async def create_validation_middleware() -> ValidationMiddleware: + """Factory function to create validation middleware.""" + return ValidationMiddleware() \ No newline at end of file diff --git a/backend/app/migrations/__init__.py b/backend/app/migrations/__init__.py new file mode 100644 index 0000000..c8030a9 --- /dev/null +++ b/backend/app/migrations/__init__.py @@ -0,0 +1,5 @@ +"""Database migration framework for MongoDB.""" + +from .migrator import MigrationManager, Migration + +__all__ = ["MigrationManager", "Migration"] \ No newline at end of file diff --git a/backend/app/migrations/migrator.py b/backend/app/migrations/migrator.py new file mode 100644 index 0000000..70ba707 --- /dev/null +++ b/backend/app/migrations/migrator.py @@ -0,0 +1,253 @@ +"""MongoDB migration framework.""" + +import os +import importlib.util +from abc import ABC, abstractmethod +from datetime import datetime +from pathlib import Path +from typing import List, Optional +from motor.motor_asyncio import AsyncIOMotorDatabase + +from app.core.database import get_database +from app.core.logging import get_logger +from app.telemetry.tracing import trace_async_operation + +logger = get_logger(__name__) + + +class Migration(ABC): + """Base class for database migrations.""" + + def __init__(self): + self.version: str = "0000-00-00-000000" # Format: YYYY-MM-DD-HHMMSS + self.description: str = "" + self.db: Optional[AsyncIOMotorDatabase] = None + + @abstractmethod + async def up(self) -> None: + """Apply the migration.""" + pass + + @abstractmethod + async def down(self) -> None: + """Rollback the migration.""" + pass + + async def set_database(self, db: AsyncIOMotorDatabase) -> None: + """Set the database instance.""" + self.db = db + + +class MigrationRecord: + """Represents a migration record in the database.""" + + def __init__(self, version: str, description: str, applied_at: datetime): + self.version = version + self.description = description + self.applied_at = applied_at + + +class MigrationManager: + """Manages database migrations.""" + + def __init__(self): + self.db: Optional[AsyncIOMotorDatabase] = None + self.migrations_dir = Path(__file__).parent / "scripts" + self.collection_name = "migration_history" + + async def initialize(self) -> None: + """Initialize the migration manager.""" + self.db = await get_database() + await self._ensure_migration_collection() + + async def _ensure_migration_collection(self) -> None: + """Ensure the migration history collection exists with proper indexes.""" + collection = self.db[self.collection_name] + + # Create indexes for migration history + await collection.create_index([("version", 1)], unique=True) + await collection.create_index([("applied_at", -1)]) + + logger.info("Migration history collection initialized") + + def discover_migrations(self) -> List[str]: + """Discover all migration files in the migrations directory.""" + if not self.migrations_dir.exists(): + logger.warning(f"Migrations directory not found: {self.migrations_dir}") + return [] + + migration_files = [] + for file_path in self.migrations_dir.glob("*.py"): + if file_path.name.startswith("migration_") and not file_path.name.startswith("__"): + migration_files.append(file_path.stem) + + # Sort by version (filename should start with version) + migration_files.sort() + return migration_files + + async def load_migration(self, migration_name: str) -> Migration: + """Dynamically load a migration class.""" + migration_path = self.migrations_dir / f"{migration_name}.py" + + if not migration_path.exists(): + raise FileNotFoundError(f"Migration file not found: {migration_path}") + + # Load the module + spec = importlib.util.spec_from_file_location(migration_name, migration_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Get the migration class (assume it's named Migration) + if not hasattr(module, 'Migration'): + raise AttributeError(f"Migration class not found in {migration_name}") + + migration_class = getattr(module, 'Migration') + migration = migration_class() + await migration.set_database(self.db) + + return migration + + async def get_applied_migrations(self) -> List[str]: + """Get list of applied migration versions.""" + collection = self.db[self.collection_name] + cursor = collection.find({}, {"version": 1}).sort("version", 1) + + applied = [] + async for doc in cursor: + applied.append(doc["version"]) + + return applied + + async def record_migration(self, migration: Migration) -> None: + """Record a successful migration in the database.""" + collection = self.db[self.collection_name] + + record = { + "version": migration.version, + "description": migration.description, + "applied_at": datetime.utcnow() + } + + await collection.insert_one(record) + logger.info(f"Recorded migration: {migration.version} - {migration.description}") + + async def remove_migration_record(self, version: str) -> None: + """Remove a migration record (for rollback).""" + collection = self.db[self.collection_name] + await collection.delete_one({"version": version}) + logger.info(f"Removed migration record: {version}") + + @trace_async_operation("migration_manager.migrate_up") + async def migrate_up(self, target_version: Optional[str] = None) -> List[str]: + """ + Apply migrations up to the target version. + + Args: + target_version: Version to migrate to. If None, applies all pending migrations. + + Returns: + List of applied migration versions. + """ + await self.initialize() + + # Discover all migrations + all_migrations = self.discover_migrations() + applied_migrations = await self.get_applied_migrations() + + # Find pending migrations + pending_migrations = [] + for migration_name in all_migrations: + # Extract version from filename (assumes format: migration_YYYY-MM-DD-HHMMSS_description.py) + version = migration_name.replace("migration_", "").split("_")[0] + + if version not in applied_migrations: + if target_version is None or version <= target_version: + pending_migrations.append((migration_name, version)) + + # Sort by version + pending_migrations.sort(key=lambda x: x[1]) + + applied = [] + for migration_name, version in pending_migrations: + try: + logger.info(f"Applying migration: {migration_name}") + + migration = await self.load_migration(migration_name) + await migration.up() + await self.record_migration(migration) + + applied.append(version) + logger.info(f"Successfully applied migration: {version}") + + except Exception as e: + logger.error(f"Failed to apply migration {migration_name}: {e}") + raise + + return applied + + @trace_async_operation("migration_manager.migrate_down") + async def migrate_down(self, target_version: str) -> List[str]: + """ + Rollback migrations down to the target version. + + Args: + target_version: Version to rollback to. + + Returns: + List of rolled back migration versions. + """ + await self.initialize() + + applied_migrations = await self.get_applied_migrations() + + # Find migrations to rollback (newer than target) + to_rollback = [] + for version in reversed(applied_migrations): + if version > target_version: + to_rollback.append(version) + + rolled_back = [] + for version in to_rollback: + try: + # Find migration file for this version + migration_name = None + for migration_file in self.discover_migrations(): + if version in migration_file: + migration_name = migration_file + break + + if not migration_name: + logger.warning(f"Migration file not found for version {version}") + continue + + logger.info(f"Rolling back migration: {migration_name}") + + migration = await self.load_migration(migration_name) + await migration.down() + await self.remove_migration_record(version) + + rolled_back.append(version) + logger.info(f"Successfully rolled back migration: {version}") + + except Exception as e: + logger.error(f"Failed to rollback migration {version}: {e}") + raise + + return rolled_back + + async def get_migration_status(self) -> dict: + """Get current migration status.""" + await self.initialize() + + all_migrations = self.discover_migrations() + applied_migrations = await self.get_applied_migrations() + + pending_count = len(all_migrations) - len(applied_migrations) + + return { + "total_migrations": len(all_migrations), + "applied_migrations": len(applied_migrations), + "pending_migrations": pending_count, + "latest_applied": applied_migrations[-1] if applied_migrations else None, + "all_applied": applied_migrations + } \ No newline at end of file diff --git a/backend/app/migrations/scripts/migration_2025-08-17-120000_initial_schema.py b/backend/app/migrations/scripts/migration_2025-08-17-120000_initial_schema.py new file mode 100644 index 0000000..667e76c --- /dev/null +++ b/backend/app/migrations/scripts/migration_2025-08-17-120000_initial_schema.py @@ -0,0 +1,64 @@ +"""Initial database schema setup migration.""" + +from datetime import datetime +from app.migrations.migrator import Migration + + +class Migration(Migration): + """Initial schema setup with all collections and indexes.""" + + def __init__(self): + super().__init__() + self.version = "2025-08-17-120000" + self.description = "Initial database schema with users, jobs, and audit_logs collections" + + async def up(self) -> None: + """Create initial collections and indexes.""" + + # Users collection setup + await self.db.users.create_index([("email", 1)], unique=True) + await self.db.users.create_index([("role", 1)]) + await self.db.users.create_index([("is_active", 1)]) + await self.db.users.create_index([("created_at", -1)]) + + # Jobs collection setup + await self.db.jobs.create_index([("status", 1), ("created_at", -1)]) + await self.db.jobs.create_index([("client_id", 1)]) + await self.db.jobs.create_index([("updated_at", -1)]) + await self.db.jobs.create_index([("languages", 1)]) + + # Create compound index for job queries + await self.db.jobs.create_index([ + ("status", 1), + ("client_id", 1), + ("created_at", -1) + ]) + + # Audit logs collection setup + await self.db.audit_logs.create_index([("timestamp", -1)]) + await self.db.audit_logs.create_index([("action", 1), ("timestamp", -1)]) + await self.db.audit_logs.create_index([("user_id", 1), ("timestamp", -1)]) + await self.db.audit_logs.create_index([("severity", 1), ("timestamp", -1)]) + await self.db.audit_logs.create_index([("resource_type", 1), ("resource_id", 1)]) + await self.db.audit_logs.create_index([("ip_address", 1), ("timestamp", -1)]) + await self.db.audit_logs.create_index([("success", 1), ("timestamp", -1)]) + + # Text search index for audit logs + await self.db.audit_logs.create_index([ + ("description", "text"), + ("details", "text"), + ("error_message", "text") + ]) + + print(f"✅ Applied migration {self.version}: {self.description}") + + async def down(self) -> None: + """Drop all collections (destructive - use with caution).""" + + # This is a destructive operation - in production, you might want to backup first + await self.db.users.drop() + await self.db.jobs.drop() + await self.db.audit_logs.drop() + + print(f"⚠️ Rolled back migration {self.version}: {self.description}") + print("⚠️ WARNING: All data has been deleted!") \ No newline at end of file diff --git a/backend/app/migrations/scripts/migration_2025-08-17-120001_index_optimization.py b/backend/app/migrations/scripts/migration_2025-08-17-120001_index_optimization.py new file mode 100644 index 0000000..fe7b143 --- /dev/null +++ b/backend/app/migrations/scripts/migration_2025-08-17-120001_index_optimization.py @@ -0,0 +1,134 @@ +"""Index optimization migration for improved query performance.""" + +from app.migrations.migrator import Migration + + +class Migration(Migration): + """Optimize indexes for better query performance.""" + + def __init__(self): + super().__init__() + self.version = "2025-08-17-120001" + self.description = "Index optimization for query performance improvements" + + async def up(self) -> None: + """Add optimized indexes for common query patterns.""" + + # Jobs collection optimizations + + # Index for job status transitions and monitoring + await self.db.jobs.create_index([ + ("status", 1), + ("updated_at", -1), + ("client_id", 1) + ], name="jobs_status_updated_client_idx") + + # Index for queue management (pending jobs) + await self.db.jobs.create_index([ + ("status", 1), + ("created_at", 1) + ], name="jobs_queue_processing_idx") + + # Index for client job history + await self.db.jobs.create_index([ + ("client_id", 1), + ("created_at", -1), + ("status", 1) + ], name="jobs_client_history_idx") + + # Sparse index for error tracking + await self.db.jobs.create_index([ + ("status", 1), + ("error", 1) + ], sparse=True, name="jobs_error_tracking_idx") + + # Users collection optimizations + + # Index for active user queries + await self.db.users.create_index([ + ("is_active", 1), + ("role", 1), + ("last_login_at", -1) + ], name="users_active_role_login_idx") + + # Index for user search by email pattern + await self.db.users.create_index([ + ("email", "text"), + ("first_name", "text"), + ("last_name", "text") + ], name="users_search_idx") + + # Audit logs collection optimizations + + # Compound index for security monitoring + await self.db.audit_logs.create_index([ + ("severity", 1), + ("action", 1), + ("timestamp", -1) + ], name="audit_security_monitoring_idx") + + # Index for user activity analysis + await self.db.audit_logs.create_index([ + ("user_id", 1), + ("action", 1), + ("timestamp", -1) + ], name="audit_user_activity_idx") + + # Index for resource access tracking + await self.db.audit_logs.create_index([ + ("resource_type", 1), + ("resource_id", 1), + ("action", 1), + ("timestamp", -1) + ], name="audit_resource_access_idx") + + # Sparse index for failed operations + await self.db.audit_logs.create_index([ + ("success", 1), + ("timestamp", -1) + ], sparse=True, name="audit_failures_idx") + + # Add TTL index for automatic audit log cleanup (optional) + # Uncomment if you want automatic cleanup after 2 years + # await self.db.audit_logs.create_index( + # [("timestamp", 1)], + # expireAfterSeconds=63072000, # 2 years + # name="audit_ttl_idx" + # ) + + print(f"✅ Applied migration {self.version}: {self.description}") + + async def down(self) -> None: + """Remove the optimized indexes.""" + + # Drop the indexes we created + indexes_to_drop = [ + "jobs_status_updated_client_idx", + "jobs_queue_processing_idx", + "jobs_client_history_idx", + "jobs_error_tracking_idx", + "users_active_role_login_idx", + "users_search_idx", + "audit_security_monitoring_idx", + "audit_user_activity_idx", + "audit_resource_access_idx", + "audit_failures_idx" + ] + + for index_name in indexes_to_drop: + try: + await self.db.jobs.drop_index(index_name) + except Exception: + pass # Index might not exist on this collection + + try: + await self.db.users.drop_index(index_name) + except Exception: + pass + + try: + await self.db.audit_logs.drop_index(index_name) + except Exception: + pass + + print(f"⚠️ Rolled back migration {self.version}: {self.description}") \ No newline at end of file diff --git a/backend/app/migrations/scripts/migration_2025-08-17-120002_audit_log_schema_update.py b/backend/app/migrations/scripts/migration_2025-08-17-120002_audit_log_schema_update.py new file mode 100644 index 0000000..fecf7b0 --- /dev/null +++ b/backend/app/migrations/scripts/migration_2025-08-17-120002_audit_log_schema_update.py @@ -0,0 +1,155 @@ +"""Migrate audit log schema from basic to comprehensive format.""" + +from datetime import datetime +from app.migrations.migrator import Migration + + +class Migration(Migration): + """Update audit log schema to comprehensive format.""" + + def __init__(self): + super().__init__() + self.version = "2025-08-17-120002" + self.description = "Update audit log schema from basic to comprehensive format" + + async def up(self) -> None: + """Migrate existing audit logs to new schema format.""" + + # Find all existing audit logs with old schema + old_logs_cursor = self.db.audit_logs.find({ + # Look for logs that have the old schema structure + "$or": [ + {"when": {"$exists": True}}, # Old timestamp field + {"job_id": {"$exists": True}}, # Old job-specific logs + {"timestamp": {"$exists": False}} # Missing new timestamp field + ] + }) + + migration_count = 0 + + async for old_log in old_logs_cursor: + try: + # Map old fields to new schema + new_log = { + "_id": old_log["_id"], + "timestamp": old_log.get("when", old_log.get("timestamp", datetime.utcnow())), + "action": self._map_old_action(old_log.get("action", "unknown")), + "severity": "info", + "description": old_log.get("action", "Legacy action"), + "success": True, + "environment": "prod", + "service_name": "accessible-video-api", + "api_version": "v1" + } + + # Map optional fields if they exist + if "user_id" in old_log: + new_log["user_id"] = old_log["user_id"] + + if "job_id" in old_log: + new_log["resource_type"] = "job" + new_log["resource_id"] = old_log["job_id"] + + if "ip_address" in old_log: + new_log["ip_address"] = old_log["ip_address"] + + if "user_agent" in old_log: + new_log["user_agent"] = old_log["user_agent"] + + if "details" in old_log: + new_log["details"] = old_log["details"] + + # Replace the old document with the new schema + await self.db.audit_logs.replace_one( + {"_id": old_log["_id"]}, + new_log + ) + + migration_count += 1 + + except Exception as e: + print(f"Error migrating audit log {old_log.get('_id')}: {e}") + continue + + print(f"✅ Applied migration {self.version}: Migrated {migration_count} audit log records") + + def _map_old_action(self, old_action: str) -> str: + """Map old action strings to new AuditAction enum values.""" + action_mapping = { + # Job actions + "job_created": "job.create", + "job_approved": "job.approve", + "job_rejected": "job.reject", + "job_updated": "job.update", + "job_cancelled": "job.cancel", + + # Auth actions + "login": "auth.login.success", + "logout": "auth.logout", + "login_failed": "auth.login.failure", + + # File actions + "file_uploaded": "file.upload", + "file_downloaded": "file.download", + + # VTT actions + "vtt_edited": "vtt.edit", + + # Admin actions + "user_created": "user.create", + "user_updated": "user.update", + "user_deleted": "user.delete", + } + + return action_mapping.get(old_action, old_action) + + async def down(self) -> None: + """Rollback to old audit log schema format (limited).""" + + # Find all audit logs with new schema + new_logs_cursor = self.db.audit_logs.find({ + "timestamp": {"$exists": True}, + "action": {"$exists": True} + }) + + rollback_count = 0 + + async for new_log in new_logs_cursor: + try: + # Map new fields back to old schema (lossy conversion) + old_log = { + "_id": new_log["_id"], + "when": new_log["timestamp"], + "action": new_log["action"] + } + + # Map back optional fields + if "user_id" in new_log: + old_log["user_id"] = new_log["user_id"] + + if "resource_type" in new_log and new_log["resource_type"] == "job": + old_log["job_id"] = new_log.get("resource_id") + + if "ip_address" in new_log: + old_log["ip_address"] = new_log["ip_address"] + + if "user_agent" in new_log: + old_log["user_agent"] = new_log["user_agent"] + + if "details" in new_log: + old_log["details"] = new_log["details"] + + # Replace with old schema + await self.db.audit_logs.replace_one( + {"_id": new_log["_id"]}, + old_log + ) + + rollback_count += 1 + + except Exception as e: + print(f"Error rolling back audit log {new_log.get('_id')}: {e}") + continue + + print(f"⚠️ Rolled back migration {self.version}: Reverted {rollback_count} audit log records") + print("⚠️ WARNING: Some audit log data may have been lost due to schema differences") \ No newline at end of file diff --git a/backend/app/models/__pycache__/audit_log.cpython-313.pyc b/backend/app/models/__pycache__/audit_log.cpython-313.pyc new file mode 100644 index 0000000..b36f052 Binary files /dev/null and b/backend/app/models/__pycache__/audit_log.cpython-313.pyc differ diff --git a/backend/app/models/__pycache__/job.cpython-313.pyc b/backend/app/models/__pycache__/job.cpython-313.pyc new file mode 100644 index 0000000..89af50a Binary files /dev/null and b/backend/app/models/__pycache__/job.cpython-313.pyc differ diff --git a/backend/app/models/__pycache__/user.cpython-313.pyc b/backend/app/models/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000..fa0ffeb Binary files /dev/null and b/backend/app/models/__pycache__/user.cpython-313.pyc differ diff --git a/backend/app/models/audit_log.py b/backend/app/models/audit_log.py new file mode 100644 index 0000000..0f813df --- /dev/null +++ b/backend/app/models/audit_log.py @@ -0,0 +1,175 @@ +"""Audit log model for tracking sensitive operations.""" + +from datetime import datetime +from enum import Enum +from typing import Any, Dict, Optional +from bson import ObjectId +from pydantic import BaseModel, Field + +from .user import PyObjectId + + +class AuditAction(str, Enum): + """Enumeration of auditable actions.""" + + # Authentication actions + LOGIN_SUCCESS = "auth.login.success" + LOGIN_FAILURE = "auth.login.failure" + LOGOUT = "auth.logout" + TOKEN_REFRESH = "auth.token.refresh" + PASSWORD_CHANGE = "auth.password.change" + PASSWORD_RESET = "auth.password.reset" + + # User management actions + USER_CREATE = "user.create" + USER_UPDATE = "user.update" + USER_DELETE = "user.delete" + USER_ROLE_CHANGE = "user.role.change" + USER_ACTIVATE = "user.activate" + USER_DEACTIVATE = "user.deactivate" + + # Job management actions + JOB_CREATE = "job.create" + JOB_UPDATE = "job.update" + JOB_DELETE = "job.delete" + JOB_APPROVE = "job.approve" + JOB_REJECT = "job.reject" + JOB_CANCEL = "job.cancel" + JOB_STATUS_CHANGE = "job.status.change" + + # File operations + FILE_UPLOAD = "file.upload" + FILE_DOWNLOAD = "file.download" + FILE_DELETE = "file.delete" + FILE_ACCESS = "file.access" + + # VTT editing actions + VTT_EDIT = "vtt.edit" + VTT_APPROVE = "vtt.approve" + VTT_REJECT = "vtt.reject" + + # Admin actions + ADMIN_CONFIG_CHANGE = "admin.config.change" + ADMIN_SYSTEM_ACTION = "admin.system.action" + ADMIN_DATA_EXPORT = "admin.data.export" + ADMIN_AUDIT_ACCESS = "admin.audit.access" + + # Security events + RATE_LIMIT_EXCEEDED = "security.rate_limit.exceeded" + VALIDATION_FAILURE = "security.validation.failure" + UNAUTHORIZED_ACCESS = "security.unauthorized.access" + SUSPICIOUS_ACTIVITY = "security.suspicious.activity" + + +class AuditLogSeverity(str, Enum): + """Severity levels for audit events.""" + + INFO = "info" # Normal operations + WARNING = "warning" # Suspicious but not critical + ERROR = "error" # Failed operations + CRITICAL = "critical" # Security incidents + + +class AuditLog(BaseModel): + """Audit log entry model.""" + + id: Optional[PyObjectId] = Field(default_factory=PyObjectId, alias="_id") + + # Core audit fields + timestamp: datetime = Field(default_factory=datetime.utcnow) + action: AuditAction + severity: AuditLogSeverity = AuditLogSeverity.INFO + + # Actor information + user_id: Optional[PyObjectId] = None + user_email: Optional[str] = None + user_role: Optional[str] = None + + # Request context + ip_address: Optional[str] = None + user_agent: Optional[str] = None + request_id: Optional[str] = None + session_id: Optional[str] = None + + # Resource information + resource_type: Optional[str] = None # e.g., "job", "user", "file" + resource_id: Optional[str] = None + resource_name: Optional[str] = None + + # Action details + description: str + details: Dict[str, Any] = Field(default_factory=dict) + + # Outcome + success: bool = True + error_message: Optional[str] = None + + # Additional metadata + environment: str = "prod" + service_name: str = "accessible-video-api" + api_version: str = "v1" + + class Config: + populate_by_name = True + arbitrary_types_allowed = True + json_encoders = {ObjectId: str} + + +class AuditLogCreate(BaseModel): + """Schema for creating audit log entries.""" + + action: AuditAction + severity: AuditLogSeverity = AuditLogSeverity.INFO + description: str + + # Optional fields that can be provided + user_id: Optional[PyObjectId] = None + user_email: Optional[str] = None + user_role: Optional[str] = None + ip_address: Optional[str] = None + user_agent: Optional[str] = None + request_id: Optional[str] = None + resource_type: Optional[str] = None + resource_id: Optional[str] = None + resource_name: Optional[str] = None + details: Dict[str, Any] = Field(default_factory=dict) + success: bool = True + error_message: Optional[str] = None + + +class AuditLogQuery(BaseModel): + """Schema for querying audit logs.""" + + # Time range + start_date: Optional[datetime] = None + end_date: Optional[datetime] = None + + # Filters + action: Optional[AuditAction] = None + severity: Optional[AuditLogSeverity] = None + user_id: Optional[PyObjectId] = None + user_email: Optional[str] = None + resource_type: Optional[str] = None + resource_id: Optional[str] = None + success: Optional[bool] = None + + # Search + search: Optional[str] = None # Full-text search in description and details + + # Pagination + skip: int = 0 + limit: int = 100 + + # Sorting + sort_by: str = "timestamp" + sort_order: int = -1 # -1 for descending, 1 for ascending + + +class AuditLogResponse(BaseModel): + """Response schema for audit log queries.""" + + logs: list[AuditLog] + total_count: int + page: int + page_size: int + has_more: bool diff --git a/backend/app/models/job.py b/backend/app/models/job.py new file mode 100644 index 0000000..5529978 --- /dev/null +++ b/backend/app/models/job.py @@ -0,0 +1,95 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Literal, Optional + +from pydantic import BaseModel, Field, constr + + +class JobStatus(str, Enum): + CREATED = "created" + INGESTING = "ingesting" + AI_PROCESSING = "ai_processing" + PENDING_QC = "pending_qc" + APPROVED_ENGLISH = "approved_english" + REJECTED = "rejected" + QC_FEEDBACK = "qc_feedback" + TRANSLATING = "translating" + TTS_GENERATING = "tts_generating" + PENDING_FINAL_REVIEW = "pending_final_review" + COMPLETED = "completed" + + +class Source(BaseModel): + filename: str + original_filename: Optional[str] = None + gcs_uri: str + duration_s: Optional[float] = None + language: constr(min_length=2, max_length=10) = "en" + + +class RequestedOutputs(BaseModel): + captions_vtt: bool = True + audio_description_vtt: bool = True + audio_description_mp3: bool = True + languages: list[str] = [] + transcreation: list[str] = [] + + +class LangOutput(BaseModel): + captions_vtt_gcs: Optional[str] = None + ad_vtt_gcs: Optional[str] = None + ad_mp3_gcs: Optional[str] = None + origin: Optional[Literal["translate", "transcreate"]] = None + qa_notes: Optional[str] = None + + +class ReviewHistoryItem(BaseModel): + at: datetime + status: str + by: Optional[str] = None + notes: Optional[str] = None + + +class Review(BaseModel): + notes: Optional[str] = "" + reviewer_id: Optional[str] = None + history: list[ReviewHistoryItem] = [] + + +class AISection(BaseModel): + ingestion_json: Optional[dict[str, Any]] = None + confidence: Optional[float] = None + + +class Job(BaseModel): + id: Optional[str] = Field(None, alias="_id") + client_id: str + title: str + source: Source + requested_outputs: RequestedOutputs + status: JobStatus = JobStatus.CREATED + review: Review = Review() + outputs: Optional[dict[str, LangOutput]] = None + ai: Optional[AISection] = None + error: Optional[dict[str, Any]] = None + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + populate_by_name = True + use_enum_values = True + + +class JobCreate(BaseModel): + title: str + language: str = "en" + requested_outputs: RequestedOutputs + + +class JobUpdate(BaseModel): + title: Optional[str] = None + status: Optional[JobStatus] = None + review: Optional[Review] = None + outputs: Optional[dict[str, LangOutput]] = None + ai: Optional[AISection] = None + error: Optional[dict[str, Any]] = None diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..a3c6266 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,57 @@ +from datetime import datetime +from enum import Enum +from typing import Optional, Annotated + +from bson import ObjectId +from pydantic import BaseModel, EmailStr, Field, BeforeValidator + + +def validate_object_id(v) -> str: + """Convert ObjectId to string""" + if isinstance(v, ObjectId): + return str(v) + if isinstance(v, str): + return v + raise ValueError('Invalid ObjectId') + + +PyObjectId = Annotated[str, BeforeValidator(validate_object_id)] + + +class UserRole(str, Enum): + CLIENT = "client" + REVIEWER = "reviewer" + ADMIN = "admin" + + +class User(BaseModel): + id: Optional[PyObjectId] = Field(None, alias="_id") + email: EmailStr + hashed_password: str + full_name: str + role: UserRole = UserRole.CLIENT + is_active: bool = True + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + class Config: + populate_by_name = True + use_enum_values = True + + +class UserInDB(User): + pass + + +class UserCreate(BaseModel): + email: EmailStr + password: str + full_name: str + role: UserRole = UserRole.CLIENT + + +class UserUpdate(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None diff --git a/backend/app/prompts/gemini_ingestion.md b/backend/app/prompts/gemini_ingestion.md new file mode 100644 index 0000000..b40b463 --- /dev/null +++ b/backend/app/prompts/gemini_ingestion.md @@ -0,0 +1,57 @@ +SYSTEM: +You are an expert accessibility writer for film/TV and e-learning. Produce STRICT JSON only. + +USER: +You are given a video. Return a JSON object with: +- language: BCP-47 code (e.g., "en") +- confidence: 0..1 +- summary: 1–2 sentence synopsis +- transcript_plaintext: full spoken words, punctuated +- captions_vtt: a valid WebVTT file as a single string, with accurate timings and no styling +- audio_description_vtt: a valid WebVTT file as a single string, describing key visual elements (no spoilers), synchronized with the program + +Constraints: +- Output MUST be valid JSON. Do not include markdown fences or any other text. +- All JSON strings must be properly escaped (use \" for quotes within strings) +- Use detailed, descriptive audio description phrases that paint a vivid picture. Aim for rich descriptions that are 20% longer than typical AD, providing enhanced visual context without duplicating spoken dialogue. +- WebVTT must start with "WEBVTT" and follow this exact format: + - Timestamp format: HH:MM:SS.mmm --> HH:MM:SS.mmm (ALWAYS include hours, even if 00:) + - Example: "00:01:23.456 --> 00:01:27.890" + - Each cue must be separated by blank lines + - Never use MM:SS format - always include the hour component +- Escape all newlines in VTT strings as \n +- Do not include trailing commas in JSON objects or arrays + +CRITICAL TIMING REQUIREMENTS: +- Caption timing must be PRECISELY synchronized with the actual speech in the video +- Each caption cue should start exactly when the speaker begins that phrase/sentence +- Each caption cue should end exactly when the speaker finishes that phrase/sentence +- Listen carefully to detect natural speech pauses and word boundaries +- Avoid starting captions too early or ending them too late +- Ensure captions align with lip movement and speech rhythm +- For audio descriptions, time them during natural speech gaps or over non-dialogue audio +- Validate that all timestamps are monotonically increasing (each cue starts after the previous one ends) + +AUDIO DESCRIPTION GUIDELINES: +- Provide rich, detailed descriptions that include setting, characters, actions, facial expressions, body language, and visual mood +- Describe colors, lighting, camera angles, and composition when relevant to understanding +- Include environmental details like weather, time of day, architectural features, or technological elements +- Mention clothing, objects, and spatial relationships that contribute to scene understanding +- Use vivid, engaging language that creates a complete mental picture for visually impaired viewers +- Aim for descriptions that are substantive enough to fill natural pauses and reduce silence between spoken content + +CRITICAL: Return ONLY valid JSON that can be parsed by JSON.parse(). No additional text. + +Example output format: +```json +{ + "language": "en", + "confidence": 0.95, + "summary": "A tutorial video showing how to use a web application dashboard.", + "transcript_plaintext": "Hello everyone, welcome to this tutorial. Today we'll be exploring the dashboard interface. First, let's log in to the system.", + "captions_vtt": "WEBVTT\n\n00:00:01.000 --> 00:00:03.500\nHello everyone, welcome to this tutorial.\n\n00:00:04.000 --> 00:00:07.200\nToday we'll be exploring the dashboard interface.\n\n00:00:08.000 --> 00:00:10.500\nFirst, let's log in to the system.", + "audio_description_vtt": "WEBVTT\n\n00:00:00.500 --> 00:00:02.000\nA bright computer monitor displays a clean, modern login page with blue and white corporate branding. The interface features prominently positioned username and password fields.\n\n00:00:05.000 --> 00:00:07.000\nA cursor arrow hovers over the rectangular username input field, which highlights with a subtle blue border as the user prepares to type.\n\n00:00:10.000 --> 00:00:12.000\nThe screen transitions to reveal a comprehensive dashboard filled with colorful charts, data widgets, and navigation panels arranged in an organized grid layout." +} +``` + +Follow this exact structure and formatting. \ No newline at end of file diff --git a/backend/app/prompts/gemini_transcreation.md b/backend/app/prompts/gemini_transcreation.md new file mode 100644 index 0000000..13f38d4 --- /dev/null +++ b/backend/app/prompts/gemini_transcreation.md @@ -0,0 +1,20 @@ +SYSTEM: +You are a culturally-savvy accessibility writer. + +USER: +Rewrite the following English captions and audio descriptions into {TARGET_LANGUAGE}, preserving: +- meaning, tone, and accessibility intent, +- timing boundaries (same cue timestamps), +- line lengths friendly for readability (~32–40 chars). + +Input: +- captions_vtt_en: +- ad_vtt_en: +- brief: + +Output: +JSON: +{ + "captions_vtt": "", + "audio_description_vtt": "" +} \ No newline at end of file diff --git a/backend/app/schemas/__pycache__/auth.cpython-313.pyc b/backend/app/schemas/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..e25bb99 Binary files /dev/null and b/backend/app/schemas/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/app/schemas/__pycache__/file.cpython-313.pyc b/backend/app/schemas/__pycache__/file.cpython-313.pyc new file mode 100644 index 0000000..189d4b2 Binary files /dev/null and b/backend/app/schemas/__pycache__/file.cpython-313.pyc differ diff --git a/backend/app/schemas/__pycache__/job.cpython-313.pyc b/backend/app/schemas/__pycache__/job.cpython-313.pyc new file mode 100644 index 0000000..f1dfa21 Binary files /dev/null and b/backend/app/schemas/__pycache__/job.cpython-313.pyc differ diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..82b972f --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,72 @@ +from typing import Optional +from pydantic import BaseModel, EmailStr +from ..models.user import UserRole + + +class LoginRequest(BaseModel): + email: EmailStr + password: str + + +class LoginResponse(BaseModel): + access_token: str + token_type: str = "bearer" + user_id: str + role: str + + +class RefreshResponse(BaseModel): + access_token: str + token_type: str = "bearer" + + +class LogoutResponse(BaseModel): + message: str = "Successfully logged out" + + +# User management schemas for admin routes +class UserResponse(BaseModel): + id: str + email: EmailStr + full_name: str + role: UserRole + is_active: bool + created_at: Optional[str] = None + + +class UserListResponse(BaseModel): + users: list[UserResponse] + total: int + page: int + size: int + + +class CreateUserRequest(BaseModel): + email: EmailStr + password: str + full_name: str + role: UserRole = UserRole.CLIENT + + +class UpdateUserRequest(BaseModel): + email: Optional[EmailStr] = None + full_name: Optional[str] = None + role: Optional[UserRole] = None + is_active: Optional[bool] = None + + +class ChangePasswordRequest(BaseModel): + current_password: str + new_password: str + + +class ResetPasswordRequest(BaseModel): + email: EmailStr + + +class AdminStatsResponse(BaseModel): + total_users: int + total_jobs: int + jobs_by_status: dict[str, int] + active_jobs_today: int + avg_processing_time_hours: float diff --git a/backend/app/schemas/file.py b/backend/app/schemas/file.py new file mode 100644 index 0000000..f195b37 --- /dev/null +++ b/backend/app/schemas/file.py @@ -0,0 +1,15 @@ +from typing import Optional + +from pydantic import BaseModel + + +class SignedUploadRequest(BaseModel): + filename: str + content_type: str + max_size: Optional[int] = None + + +class SignedUploadResponse(BaseModel): + upload_url: str + fields: dict[str, str] + blob_path: str \ No newline at end of file diff --git a/backend/app/schemas/job.py b/backend/app/schemas/job.py new file mode 100644 index 0000000..c83007a --- /dev/null +++ b/backend/app/schemas/job.py @@ -0,0 +1,89 @@ +from typing import Any, Optional, Union + +from pydantic import BaseModel + +from ..models.job import JobStatus, LangOutput, RequestedOutputs, Review + + +class JobResponse(BaseModel): + id: str + title: str + status: JobStatus + source: dict[str, Any] + requested_outputs: RequestedOutputs + review: Review + outputs: Optional[dict[str, LangOutput]] = None + created_at: Optional[str] = None + updated_at: Optional[str] = None + + +class JobListResponse(BaseModel): + jobs: list[JobResponse] + total: int + page: int + size: int + + +class JobCreateRequest(BaseModel): + title: str + language: str = "en" + requested_outputs: RequestedOutputs + + +class JobUpdateRequest(BaseModel): + title: Optional[str] = None + review_notes: Optional[str] = None + + +class ApproveEnglishRequest(BaseModel): + notes: Optional[str] = None + + +class RejectJobRequest(BaseModel): + notes: str + + +class CompleteJobRequest(BaseModel): + notes: Optional[str] = None + + +class VttUpdateRequest(BaseModel): + captions_vtt: Optional[str] = None + audio_description_vtt: Optional[str] = None + language: str = "en" + + +class VttTimingAdjustRequest(BaseModel): + offset_seconds: float + language: str = "en" + adjust_captions: bool = True + adjust_audio_description: bool = True + + +class JobDownloadsResponse(BaseModel): + downloads: dict[str, Union[dict[str, str], str]] # language -> {file_type: signed_url} OR source_video -> signed_url + + +class VttContentResponse(BaseModel): + captions_vtt: Optional[str] = None + audio_description_vtt: Optional[str] = None + + +class AssetValidationResponse(BaseModel): + is_valid: bool + errors: list[str] + warnings: list[str] = [] + + +class JobDeleteResponse(BaseModel): + message: str + + +class BulkDeleteRequest(BaseModel): + job_ids: list[str] + + +class BulkDeleteResponse(BaseModel): + deleted_count: int + total_requested: int + errors: list[str] diff --git a/backend/app/services/__pycache__/audit_logger.cpython-313.pyc b/backend/app/services/__pycache__/audit_logger.cpython-313.pyc new file mode 100644 index 0000000..ce004b2 Binary files /dev/null and b/backend/app/services/__pycache__/audit_logger.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/emailer.cpython-313.pyc b/backend/app/services/__pycache__/emailer.cpython-313.pyc new file mode 100644 index 0000000..79f2152 Binary files /dev/null and b/backend/app/services/__pycache__/emailer.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/gcs.cpython-313.pyc b/backend/app/services/__pycache__/gcs.cpython-313.pyc new file mode 100644 index 0000000..8c720cf Binary files /dev/null and b/backend/app/services/__pycache__/gcs.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/gemini.cpython-313.pyc b/backend/app/services/__pycache__/gemini.cpython-313.pyc new file mode 100644 index 0000000..d960f56 Binary files /dev/null and b/backend/app/services/__pycache__/gemini.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/translate.cpython-313.pyc b/backend/app/services/__pycache__/translate.cpython-313.pyc new file mode 100644 index 0000000..f1dfdf3 Binary files /dev/null and b/backend/app/services/__pycache__/translate.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/tts.cpython-313.pyc b/backend/app/services/__pycache__/tts.cpython-313.pyc new file mode 100644 index 0000000..4d1c00b Binary files /dev/null and b/backend/app/services/__pycache__/tts.cpython-313.pyc differ diff --git a/backend/app/services/__pycache__/validation.cpython-313.pyc b/backend/app/services/__pycache__/validation.cpython-313.pyc new file mode 100644 index 0000000..c462791 Binary files /dev/null and b/backend/app/services/__pycache__/validation.cpython-313.pyc differ diff --git a/backend/app/services/audit_logger.py b/backend/app/services/audit_logger.py new file mode 100644 index 0000000..63fd4bb --- /dev/null +++ b/backend/app/services/audit_logger.py @@ -0,0 +1,331 @@ +"""Audit logging service for tracking sensitive operations.""" + +import uuid +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional +from fastapi import Request +from motor.motor_asyncio import AsyncIOMotorCollection + +from app.core.database import get_database +from app.core.config import get_settings +from app.models.audit_log import ( + AuditLog, + AuditLogCreate, + AuditLogQuery, + AuditLogResponse, + AuditAction, + AuditLogSeverity +) +from app.models.user import User +from app.telemetry.tracing import trace_async_operation + + +class AuditLogger: + """Service for managing audit logs.""" + + def __init__(self): + self.settings = get_settings() + self.collection: Optional[AsyncIOMotorCollection] = None + + async def _get_collection(self) -> AsyncIOMotorCollection: + """Get the audit logs collection.""" + if not self.collection: + db = await get_database() + self.collection = db.audit_logs + return self.collection + + @trace_async_operation("audit_logger.log_action") + async def log_action( + self, + action: AuditAction, + description: str, + user: Optional[User] = None, + request: Optional[Request] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None, + resource_name: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + severity: AuditLogSeverity = AuditLogSeverity.INFO, + success: bool = True, + error_message: Optional[str] = None + ) -> str: + """ + Log an audit event. + + Returns: + The ID of the created audit log entry. + """ + + # Extract request context + ip_address = None + user_agent = None + request_id = None + + if request: + # Get IP address (handle forwarded headers) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + ip_address = forwarded_for.split(',')[0].strip() + elif request.client: + ip_address = request.client.host + + user_agent = request.headers.get("User-Agent") + request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) + + # Create audit log entry + audit_log = AuditLog( + action=action, + severity=severity, + description=description, + user_id=user.id if user else None, + user_email=user.email if user else None, + user_role=user.role.value if user else None, + ip_address=ip_address, + user_agent=user_agent, + request_id=request_id, + resource_type=resource_type, + resource_id=resource_id, + resource_name=resource_name, + details=details or {}, + success=success, + error_message=error_message, + environment=self.settings.app_env, + service_name="accessible-video-api", + api_version="v1" + ) + + # Save to database + collection = await self._get_collection() + result = await collection.insert_one(audit_log.dict(by_alias=True)) + + return str(result.inserted_id) + + @trace_async_operation("audit_logger.query_logs") + async def query_logs(self, query: AuditLogQuery) -> AuditLogResponse: + """Query audit logs with filtering and pagination.""" + + collection = await self._get_collection() + + # Build MongoDB query + mongo_query = {} + + # Time range filter + if query.start_date or query.end_date: + timestamp_filter = {} + if query.start_date: + timestamp_filter["$gte"] = query.start_date + if query.end_date: + timestamp_filter["$lte"] = query.end_date + mongo_query["timestamp"] = timestamp_filter + + # Exact match filters + if query.action: + mongo_query["action"] = query.action + if query.severity: + mongo_query["severity"] = query.severity + if query.user_id: + mongo_query["user_id"] = query.user_id + if query.user_email: + mongo_query["user_email"] = query.user_email + if query.resource_type: + mongo_query["resource_type"] = query.resource_type + if query.resource_id: + mongo_query["resource_id"] = query.resource_id + if query.success is not None: + mongo_query["success"] = query.success + + # Text search + if query.search: + mongo_query["$or"] = [ + {"description": {"$regex": query.search, "$options": "i"}}, + {"details": {"$regex": query.search, "$options": "i"}}, + {"error_message": {"$regex": query.search, "$options": "i"}} + ] + + # Get total count + total_count = await collection.count_documents(mongo_query) + + # Execute query with pagination and sorting + cursor = collection.find(mongo_query) + + # Apply sorting + sort_direction = query.sort_order + cursor = cursor.sort(query.sort_by, sort_direction) + + # Apply pagination + cursor = cursor.skip(query.skip).limit(query.limit) + + # Execute query + documents = await cursor.to_list(length=query.limit) + + # Convert to Pydantic models + logs = [] + for doc in documents: + try: + logs.append(AuditLog(**doc)) + except Exception as e: + # Log conversion error but continue + print(f"Error converting audit log document: {e}") + continue + + # Calculate pagination info + page = (query.skip // query.limit) + 1 + has_more = (query.skip + len(logs)) < total_count + + return AuditLogResponse( + logs=logs, + total_count=total_count, + page=page, + page_size=len(logs), + has_more=has_more + ) + + async def get_user_activity(self, user_id: str, days: int = 30) -> List[AuditLog]: + """Get recent activity for a specific user.""" + + from_date = datetime.utcnow().replace( + hour=0, minute=0, second=0, microsecond=0 + ) - timedelta(days=days) + + query = AuditLogQuery( + user_id=user_id, + start_date=from_date, + limit=1000, + sort_by="timestamp", + sort_order=-1 + ) + + response = await self.query_logs(query) + return response.logs + + async def get_security_events(self, hours: int = 24) -> List[AuditLog]: + """Get recent security-related events.""" + + from_date = datetime.utcnow() - timedelta(hours=hours) + + security_actions = [ + AuditAction.LOGIN_FAILURE, + AuditAction.RATE_LIMIT_EXCEEDED, + AuditAction.VALIDATION_FAILURE, + AuditAction.UNAUTHORIZED_ACCESS, + AuditAction.SUSPICIOUS_ACTIVITY + ] + + collection = await self._get_collection() + + query = { + "timestamp": {"$gte": from_date}, + "action": {"$in": security_actions} + } + + cursor = collection.find(query).sort("timestamp", -1).limit(1000) + documents = await cursor.to_list(length=1000) + + logs = [] + for doc in documents: + try: + logs.append(AuditLog(**doc)) + except Exception: + continue + + return logs + + async def cleanup_old_logs(self, retention_days: int = 365) -> int: + """Clean up audit logs older than retention period.""" + + cutoff_date = datetime.utcnow().replace( + hour=0, minute=0, second=0, microsecond=0 + ) - timedelta(days=retention_days) + + collection = await self._get_collection() + result = await collection.delete_many({ + "timestamp": {"$lt": cutoff_date} + }) + + return result.deleted_count + + +# Global audit logger instance +audit_logger = AuditLogger() + + +# Convenience functions for common audit operations +async def log_auth_success(user: User, request: Request): + """Log successful authentication.""" + await audit_logger.log_action( + action=AuditAction.LOGIN_SUCCESS, + description=f"User {user.email} logged in successfully", + user=user, + request=request, + severity=AuditLogSeverity.INFO + ) + + +async def log_auth_failure(email: str, request: Request, reason: str): + """Log failed authentication attempt.""" + await audit_logger.log_action( + action=AuditAction.LOGIN_FAILURE, + description=f"Failed login attempt for {email}: {reason}", + request=request, + severity=AuditLogSeverity.WARNING, + success=False, + error_message=reason, + details={"attempted_email": email} + ) + + +async def log_job_action(action: AuditAction, job_id: str, user: User, request: Request, details: Optional[Dict] = None): + """Log job-related actions.""" + action_descriptions = { + AuditAction.JOB_CREATE: "Job created", + AuditAction.JOB_APPROVE: "Job approved", + AuditAction.JOB_REJECT: "Job rejected", + AuditAction.JOB_CANCEL: "Job cancelled", + AuditAction.JOB_UPDATE: "Job updated" + } + + await audit_logger.log_action( + action=action, + description=f"{action_descriptions.get(action, str(action))} by {user.email}", + user=user, + request=request, + resource_type="job", + resource_id=job_id, + details=details + ) + + +async def log_user_management(action: AuditAction, target_user_id: str, admin_user: User, request: Request, details: Optional[Dict] = None): + """Log user management actions.""" + action_descriptions = { + AuditAction.USER_CREATE: "User created", + AuditAction.USER_UPDATE: "User updated", + AuditAction.USER_DELETE: "User deleted", + AuditAction.USER_ROLE_CHANGE: "User role changed", + AuditAction.USER_ACTIVATE: "User activated", + AuditAction.USER_DEACTIVATE: "User deactivated" + } + + await audit_logger.log_action( + action=action, + description=f"{action_descriptions.get(action, str(action))} by admin {admin_user.email}", + user=admin_user, + request=request, + resource_type="user", + resource_id=target_user_id, + details=details, + severity=AuditLogSeverity.INFO + ) + + +async def log_security_event(action: AuditAction, description: str, request: Request, user: Optional[User] = None, details: Optional[Dict] = None): + """Log security-related events.""" + await audit_logger.log_action( + action=action, + description=description, + user=user, + request=request, + severity=AuditLogSeverity.WARNING if action != AuditAction.SUSPICIOUS_ACTIVITY else AuditLogSeverity.CRITICAL, + success=False, + details=details + ) \ No newline at end of file diff --git a/backend/app/services/emailer.py b/backend/app/services/emailer.py new file mode 100644 index 0000000..b89c37a --- /dev/null +++ b/backend/app/services/emailer.py @@ -0,0 +1,123 @@ + +from jinja2 import Template +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Content, From, Mail, Subject, To + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +class EmailService: + def __init__(self): + if settings.sendgrid_api_key: + self.client = SendGridAPIClient(api_key=settings.sendgrid_api_key) + else: + logger.warning("SendGrid API key not configured") + self.client = None + + async def send_completion_email( + self, + recipient_email: str, + job_title: str, + download_links: dict[str, dict[str, str]] + ) -> bool: + """Send job completion email with download links""" + if not self.client: + logger.error("SendGrid not configured, cannot send email") + return False + + try: + # Render email template + html_content = self._render_completion_template( + job_title=job_title, + download_links=download_links + ) + + message = Mail( + from_email=From(settings.email_from, "Accessible Video Platform"), + to_emails=To(recipient_email), + subject=Subject(f"Your accessible video assets are ready: {job_title}"), + html_content=Content("text/html", html_content) + ) + + response = self.client.send(message) + + if response.status_code == 202: + logger.info(f"Completion email sent successfully to {recipient_email}") + return True + else: + logger.error(f"Failed to send email, status code: {response.status_code}") + return False + + except Exception as e: + logger.error(f"Email sending failed: {e}") + return False + + def _render_completion_template( + self, + job_title: str, + download_links: dict[str, dict[str, str]] + ) -> str: + """Render the completion email HTML template""" + template_str = """ + + + + + Your Accessible Video Assets Are Ready + + + +
+
+

Your Accessible Video Assets Are Ready!

+
+ +
+

{{ job_title }}

+ +

Great news! Your video accessibility assets have been processed and are ready for download.

+ + {% for language, files in download_links.items() %} +
+

{{ language.upper() }} Assets

+ {% for file_type, url in files.items() %} + + Download {{ file_type|replace('_', ' ')|title }} + + {% endfor %} +
+ {% endfor %} + +

Important: These download links will expire in 24 hours for security purposes.

+ +

If you need assistance or have questions about your accessible video assets, please don't hesitate to contact our support team.

+
+ + +
+ + + """ + + template = Template(template_str) + return template.render( + job_title=job_title, + download_links=download_links + ) + + +# Global service instance +email_service = EmailService() diff --git a/backend/app/services/gcs.py b/backend/app/services/gcs.py new file mode 100644 index 0000000..8b03f96 --- /dev/null +++ b/backend/app/services/gcs.py @@ -0,0 +1,168 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor +from datetime import datetime, timedelta +from typing import Optional + +from fastapi import HTTPException, UploadFile +from google.cloud import storage +from google.cloud.exceptions import NotFound + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +class GCSService: + def __init__(self): + self.client = storage.Client(project=settings.gcp_project_id) + self.bucket = self.client.bucket(settings.gcs_bucket) + self.executor = ThreadPoolExecutor(max_workers=4) + + async def upload_file_to_gcs( + self, + file: UploadFile, + destination_path: str, + content_type: Optional[str] = None + ) -> str: + """Upload file to GCS and return the GCS URI""" + def _upload(): + blob = self.bucket.blob(destination_path) + + # Set content type + if content_type: + blob.content_type = content_type + elif file.content_type: + blob.content_type = file.content_type + + # Upload file + file.file.seek(0) # Reset file pointer + blob.upload_from_file(file.file) + + return f"gs://{settings.gcs_bucket}/{destination_path}" + + loop = asyncio.get_event_loop() + try: + return await loop.run_in_executor(self.executor, _upload) + except Exception as e: + logger.error(f"Failed to upload file to GCS: {e}") + raise HTTPException(status_code=500, detail="File upload failed") + + async def upload_text_to_gcs( + self, + content: str, + destination_path: str, + content_type: str = "text/plain" + ) -> str: + """Upload text content to GCS and return the GCS URI""" + def _upload(): + blob = self.bucket.blob(destination_path) + blob.content_type = content_type + blob.upload_from_string(content, content_type=content_type) + + return f"gs://{settings.gcs_bucket}/{destination_path}" + + loop = asyncio.get_event_loop() + try: + return await loop.run_in_executor(self.executor, _upload) + except Exception as e: + logger.error(f"Failed to upload text to GCS: {e}") + raise HTTPException(status_code=500, detail="Text upload failed") + + async def get_signed_url( + self, + blob_path: str, + expiration_hours: int = 24, + method: str = "GET" + ) -> str: + """Generate a signed URL for downloading a file""" + def _get_signed_url(): + blob = self.bucket.blob(blob_path) + + # Check if blob exists + if not blob.exists(): + raise NotFound(f"File not found: {blob_path}") + + expiration = datetime.utcnow() + timedelta(hours=expiration_hours) + + return blob.generate_signed_url( + expiration=expiration, + method=method, + version="v4" + ) + + loop = asyncio.get_event_loop() + try: + return await loop.run_in_executor(self.executor, _get_signed_url) + except NotFound: + raise HTTPException(status_code=404, detail="File not found") + except Exception as e: + logger.error(f"Failed to generate signed URL: {e}") + raise HTTPException(status_code=500, detail="Failed to generate download URL") + + async def delete_file(self, blob_path: str) -> bool: + """Delete a file from GCS""" + def _delete(): + blob = self.bucket.blob(blob_path) + blob.delete() + return True + + loop = asyncio.get_event_loop() + try: + return await loop.run_in_executor(self.executor, _delete) + except NotFound: + return False + except Exception as e: + logger.error(f"Failed to delete file from GCS: {e}") + raise HTTPException(status_code=500, detail="File deletion failed") + + async def file_exists(self, blob_path: str) -> bool: + """Check if a file exists in GCS""" + def _exists(): + blob = self.bucket.blob(blob_path) + return blob.exists() + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, _exists) + + +# Global GCS service instance +gcs_service = GCSService() + +# Convenience functions +async def upload_file_to_gcs(file: UploadFile, destination_path: str) -> str: + return await gcs_service.upload_file_to_gcs(file, destination_path) + +async def upload_vtt_to_gcs(content: str, destination_path: str) -> str: + return await gcs_service.upload_text_to_gcs(content, destination_path, "text/vtt") + +async def upload_json_to_gcs(content: str, destination_path: str) -> str: + return await gcs_service.upload_text_to_gcs(content, destination_path, "application/json") + +async def get_signed_download_url(blob_path: str, expiration_hours: int = 24) -> str: + return await gcs_service.get_signed_url(blob_path, expiration_hours) + +async def generate_signed_upload_url( + blob_path: str, + content_type: str, + max_size: int = 1024 * 1024 * 1024 # 1GB +) -> dict: + """Generate a signed URL for direct browser-to-GCS upload""" + def _generate(): + blob = gcs_service.bucket.blob(blob_path) + + # Generate signed POST URL + url, fields = blob.generate_signed_post_policy_v4( + expiration=timedelta(hours=1), + conditions=[ + ["content-length-range", 1, max_size], + ["starts-with", "$Content-Type", content_type.split("/")[0]] + ], + fields={ + "Content-Type": content_type + } + ) + + return {"url": url, "fields": fields} + + loop = asyncio.get_event_loop() + return await loop.run_in_executor(gcs_service.executor, _generate) diff --git a/backend/app/services/gemini.py b/backend/app/services/gemini.py new file mode 100644 index 0000000..a9d01e8 --- /dev/null +++ b/backend/app/services/gemini.py @@ -0,0 +1,350 @@ +import json +import asyncio +from pathlib import Path +from typing import Any, Optional + +import google.genai as genai + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +# Configure Gemini client +client = genai.Client(api_key=settings.gemini_api_key) + +class GeminiService: + def __init__(self): + self.model_name = 'gemini-2.5-pro' # Stable production model + self.prompts_dir = Path(__file__).parent.parent / "prompts" + + def _load_prompt(self, prompt_file: str) -> str: + """Load prompt template from prompts directory""" + prompt_path = self.prompts_dir / prompt_file + try: + return prompt_path.read_text() + except FileNotFoundError: + logger.error(f"Prompt file not found: {prompt_file}") + raise + + async def _wait_for_file_active(self, file_name: str, max_wait_seconds: int = 300) -> bool: + """Wait for uploaded file to become ACTIVE state""" + wait_time = 1 # Start with 1 second + total_waited = 0 + + while total_waited < max_wait_seconds: + try: + # Get file status + file_info = client.files.get(name=file_name) + logger.info(f"File {file_name} status: {file_info.state} (waited {total_waited}s)") + + if file_info.state == "ACTIVE": + logger.info(f"File {file_name} is now ACTIVE!") + return True + elif file_info.state == "FAILED": + logger.error(f"File {file_name} processing FAILED") + return False + + # Wait with exponential backoff (max 30s) + logger.info(f"File not ready, waiting {wait_time}s...") + await asyncio.sleep(wait_time) + total_waited += wait_time + wait_time = min(wait_time * 1.5, 30) # Exponential backoff, max 30s + + except Exception as e: + logger.error(f"Error checking file status: {e}") + await asyncio.sleep(5) # Wait 5s on error + total_waited += 5 + + logger.error(f"File {file_name} did not become ACTIVE within {max_wait_seconds}s") + return False + + async def extract_accessibility(self, video_file_path: str) -> dict[str, Any]: + """ + Extract captions and audio descriptions from video using Gemini 2.0 + Returns structured JSON with transcript, captions VTT, and audio description VTT + """ + prompt = self._load_prompt("gemini_ingestion.md") + + try: + logger.info(f"Starting Gemini processing for video: {video_file_path}") + + # Upload video file to Gemini using new API + logger.info("Uploading video file to Gemini API...") + uploaded_file = client.files.upload( + file=video_file_path, + config={ + "display_name": f"video_processing_{Path(video_file_path).name}", + "mime_type": "video/mp4" + } + ) + logger.info(f"Successfully uploaded file: {uploaded_file.name} (URI: {uploaded_file.uri})") + + # Wait for file to become ACTIVE before using it + logger.info("Waiting for file to become ACTIVE...") + file_ready = await self._wait_for_file_active(uploaded_file.name) + if not file_ready: + raise Exception("File failed to become ACTIVE within timeout") + + # Generate content using new API + logger.info("Generating content with Gemini model...") + response = client.models.generate_content( + model=self.model_name, + contents=[ + genai.types.Part.from_text(text=prompt), + genai.types.Part.from_uri( + file_uri=uploaded_file.uri, + mime_type=uploaded_file.mime_type + ) + ] + ) + + # Parse JSON response + response_text = response.text.strip() + logger.info(f"Received Gemini response (first 200 chars): {response_text[:200]}...") + + # Handle potential markdown formatting + if response_text.startswith("```json"): + response_text = response_text.replace("```json", "").replace("```", "").strip() + logger.info("Cleaned markdown formatting from response") + + # Additional cleanup for common JSON issues + response_text = response_text.strip() + + logger.info("Parsing JSON response...") + try: + result = json.loads(response_text) + except json.JSONDecodeError as e: + logger.error(f"JSON parse error at position {e.pos}: {e.msg}") + # Log the problematic area + start = max(0, e.pos - 100) + end = min(len(response_text), e.pos + 100) + problematic_text = response_text[start:end] + logger.error(f"Problematic JSON area: ...{problematic_text}...") + raise + + # Validate required fields + required_fields = [ + "language", "confidence", "summary", + "transcript_plaintext", "captions_vtt", "audio_description_vtt" + ] + + for field in required_fields: + if field not in result: + raise ValueError(f"Missing required field: {field}") + + # Validate VTT format + if not result["captions_vtt"].startswith("WEBVTT"): + raise ValueError("Invalid captions VTT format") + + if not result["audio_description_vtt"].startswith("WEBVTT"): + raise ValueError("Invalid audio description VTT format") + + logger.info( + f"Successfully extracted accessibility content with confidence: {result['confidence']}" + ) + + # Clean up uploaded file + try: + client.files.delete(name=uploaded_file.name) + except Exception as e: + logger.warning(f"Failed to cleanup uploaded file: {e}") + + return result + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse Gemini JSON response: {e}") + logger.error(f"Raw response that failed to parse: {response_text}") + # Attempt self-healing + return await self._self_heal_response(video_file_path, response_text) + except Exception as e: + logger.error(f"Gemini extraction failed with exception: {type(e).__name__}: {str(e)}") + logger.error(f"Video file path: {video_file_path}") + # Print to stdout for immediate visibility + print(f"🚨 GEMINI ERROR: {type(e).__name__}: {str(e)}") + raise + + async def _self_heal_response(self, video_file_path: str, invalid_response: str) -> dict[str, Any]: + """Attempt to self-heal invalid JSON response from Gemini""" + logger.info("Attempting to self-heal JSON response without re-uploading video") + + # Try to fix common JSON issues first + try: + fixed_response = self._attempt_json_fix(invalid_response) + if fixed_response: + logger.info("Successfully fixed JSON without re-processing") + return fixed_response + except Exception as e: + logger.warning(f"JSON fix attempt failed: {e}") + + # If simple fixes don't work, try a text-only self-heal prompt with more context + self_heal_prompt = f""" +SYSTEM: You are a JSON repair service. Fix the malformed JSON below and return ONLY the corrected JSON. + +CRITICAL REQUIREMENTS: +- The JSON MUST contain these exact fields: language, confidence, summary, transcript_plaintext, captions_vtt, audio_description_vtt +- If audio_description_vtt is truncated or missing, reconstruct it as a valid WebVTT with at least basic descriptions +- All VTT content must start with "WEBVTT" and have proper timestamp format (HH:MM:SS.mmm --> HH:MM:SS.mmm) +- Properly escape all quotes within strings using \" +- Fix unterminated strings by adding closing quotes +- Remove trailing commas +- Ensure all JSON is properly closed with }} + +Fix the JSON and return it: + +{invalid_response} + """ + + try: + response = client.models.generate_content( + model=self.model_name, + contents=[genai.types.Part.from_text(text=self_heal_prompt)] + ) + + response_text = response.text.strip() + + # Handle potential markdown formatting + if response_text.startswith("```json"): + response_text = response_text.replace("```json", "").replace("```", "").strip() + + result = json.loads(response_text) + + # Validate that all required fields are present after healing + required_fields = [ + "language", "confidence", "summary", + "transcript_plaintext", "captions_vtt", "audio_description_vtt" + ] + + missing_fields = [field for field in required_fields if field not in result] + if missing_fields: + logger.error(f"Self-heal lost required fields: {missing_fields}") + # If audio_description_vtt is missing, create a basic one + if "audio_description_vtt" in missing_fields: + logger.info("Creating fallback audio_description_vtt") + result["audio_description_vtt"] = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nVideo content with visual elements described." + + # If other critical fields are missing, raise an error + remaining_missing = [f for f in missing_fields if f != "audio_description_vtt"] + if remaining_missing: + raise ValueError(f"Self-heal failed to preserve required fields: {remaining_missing}") + + logger.info("Successfully self-healed Gemini response with all required fields") + return result + + except Exception as e: + logger.error(f"Self-heal attempt failed: {e}") + raise ValueError("Failed to get valid JSON from Gemini after self-heal attempt") + + def _attempt_json_fix(self, json_text: str) -> dict[str, Any] | None: + """Attempt to fix common JSON syntax issues""" + # Try to identify and fix common issues + fixes_tried = [] + fixed_text = json_text + import re + + # Fix 1: Remove trailing commas + fixed_text = re.sub(r',(\s*[}\]])', r'\1', fixed_text) + fixes_tried.append("removed trailing commas") + + # Fix 2: Try to fix unterminated strings by adding closing quote and brace + if fixed_text.count('"') % 2 != 0: # Odd number of quotes suggests unterminated string + # Find the last quote and see if we need to close the JSON + last_quote_pos = fixed_text.rfind('"') + remainder = fixed_text[last_quote_pos + 1:].strip() + + # If there's no closing brace after the last quote, try to fix it + if remainder and not remainder.endswith('}'): + # Try to intelligently close the JSON + if 'audio_description_vtt' in fixed_text[max(0, last_quote_pos - 100):]: + # This appears to be in the audio_description_vtt field + fixed_text += '"\n}' + fixes_tried.append("closed unterminated audio_description_vtt string") + else: + fixed_text += '"' + fixes_tried.append("closed unterminated string") + + # Fix 3: Ensure JSON ends with closing brace + if not fixed_text.rstrip().endswith('}'): + fixed_text = fixed_text.rstrip() + '\n}' + fixes_tried.append("added closing brace") + + try: + result = json.loads(fixed_text) + logger.info(f"JSON fixed with: {', '.join(fixes_tried)}") + + # Validate that we have the required fields + required_fields = [ + "language", "confidence", "summary", + "transcript_plaintext", "captions_vtt", "audio_description_vtt" + ] + + missing_fields = [field for field in required_fields if field not in result] + if missing_fields: + logger.warning(f"Fixed JSON is missing required fields: {missing_fields}") + return None # Let the more advanced self-healing handle this + + return result + except json.JSONDecodeError as e: + logger.debug(f"JSON fix attempt failed: {e}") + return None + + async def transcreate_content( + self, + captions_vtt: str, + ad_vtt: str, + target_language: str, + brief: Optional[str] = None + ) -> dict[str, str]: + """ + Transcreate English VTT content to target language with cultural adaptation + """ + prompt_template = self._load_prompt("gemini_transcreation.md") + + # Format prompt with actual content + prompt = prompt_template.format( + TARGET_LANGUAGE=target_language + ) + + user_prompt = f""" +Input: +- captions_vtt_en: {captions_vtt} +- ad_vtt_en: {ad_vtt} +- brief: {brief or "No specific brand guidelines provided"} + +Output: +JSON: +""" + + try: + response = client.models.generate_content( + model=self.model_name, + contents=[ + genai.types.Part.from_text(text=prompt + "\n\n" + user_prompt) + ] + ) + + response_text = response.text.strip() + + # Handle potential markdown formatting + if response_text.startswith("```json"): + response_text = response_text.replace("```json", "").replace("```", "").strip() + + result = json.loads(response_text) + + # Validate required fields + if "captions_vtt" not in result or "audio_description_vtt" not in result: + raise ValueError("Missing required VTT fields in transcreation response") + + logger.info(f"Successfully transcreated content to {target_language}") + return result + + except json.JSONDecodeError as e: + logger.error(f"Failed to parse transcreation JSON response: {e}") + raise ValueError("Invalid JSON response from transcreation") + except Exception as e: + logger.error(f"Transcreation failed: {e}") + raise + + +# Global service instance +gemini_service = GeminiService() diff --git a/backend/app/services/secrets_manager.py b/backend/app/services/secrets_manager.py new file mode 100644 index 0000000..c337f88 --- /dev/null +++ b/backend/app/services/secrets_manager.py @@ -0,0 +1,284 @@ +"""Google Cloud Secret Manager integration service.""" + +import os +import asyncio +from typing import Dict, List, Optional, Any +from functools import lru_cache +from google.cloud import secretmanager +from google.api_core import exceptions as gcp_exceptions + +from app.core.config import get_settings +from app.core.logging import get_logger +from app.telemetry.tracing import trace_async_operation + +logger = get_logger(__name__) + + +class SecretManagerError(Exception): + """Custom exception for Secret Manager operations.""" + pass + + +class SecretsManager: + """Service for managing secrets via Google Cloud Secret Manager.""" + + def __init__(self): + self.settings = get_settings() + self.client: Optional[secretmanager.SecretManagerServiceClient] = None + self.project_id = self.settings.google_cloud_project + self._cache: Dict[str, str] = {} + self._cache_ttl = 300 # 5 minutes cache + + def _get_client(self) -> secretmanager.SecretManagerServiceClient: + """Get or create Secret Manager client.""" + if not self.client: + try: + self.client = secretmanager.SecretManagerServiceClient() + logger.info("Secret Manager client initialized") + except Exception as e: + logger.error(f"Failed to initialize Secret Manager client: {e}") + raise SecretManagerError(f"Failed to initialize Secret Manager: {e}") + + return self.client + + @trace_async_operation("secrets_manager.get_secret") + async def get_secret(self, secret_name: str, version: str = "latest") -> str: + """ + Retrieve a secret from Google Cloud Secret Manager. + + Args: + secret_name: Name of the secret + version: Version of the secret (default: "latest") + + Returns: + The secret value as a string + + Raises: + SecretManagerError: If secret cannot be retrieved + """ + + cache_key = f"{secret_name}:{version}" + + # Check cache first + if cache_key in self._cache: + logger.debug(f"Secret {secret_name} retrieved from cache") + return self._cache[cache_key] + + try: + # Build the secret name + name = f"projects/{self.project_id}/secrets/{secret_name}/versions/{version}" + + # Get the secret + client = self._get_client() + + # Run in thread pool since Secret Manager client is synchronous + loop = asyncio.get_event_loop() + response = await loop.run_in_executor( + None, + client.access_secret_version, + {"name": name} + ) + + secret_value = response.payload.data.decode("UTF-8") + + # Cache the secret (with TTL handled by application restart) + self._cache[cache_key] = secret_value + + logger.info(f"Successfully retrieved secret: {secret_name}") + return secret_value + + except gcp_exceptions.NotFound: + error_msg = f"Secret not found: {secret_name}" + logger.error(error_msg) + raise SecretManagerError(error_msg) + + except gcp_exceptions.PermissionDenied: + error_msg = f"Permission denied accessing secret: {secret_name}" + logger.error(error_msg) + raise SecretManagerError(error_msg) + + except Exception as e: + error_msg = f"Failed to retrieve secret {secret_name}: {e}" + logger.error(error_msg) + raise SecretManagerError(error_msg) + + @trace_async_operation("secrets_manager.get_secrets_batch") + async def get_secrets_batch(self, secret_names: List[str]) -> Dict[str, str]: + """ + Retrieve multiple secrets efficiently. + + Args: + secret_names: List of secret names to retrieve + + Returns: + Dictionary mapping secret names to their values + """ + + secrets = {} + tasks = [] + + for secret_name in secret_names: + task = asyncio.create_task( + self.get_secret(secret_name), + name=f"get_secret_{secret_name}" + ) + tasks.append((secret_name, task)) + + # Wait for all tasks to complete + for secret_name, task in tasks: + try: + secrets[secret_name] = await task + except SecretManagerError as e: + logger.warning(f"Failed to retrieve secret {secret_name}: {e}") + # Continue with other secrets + continue + + return secrets + + async def create_secret(self, secret_name: str, secret_value: str, labels: Optional[Dict[str, str]] = None) -> str: + """ + Create a new secret in Secret Manager. + + Args: + secret_name: Name of the secret + secret_value: Value to store + labels: Optional labels for the secret + + Returns: + The full secret resource name + """ + + try: + client = self._get_client() + parent = f"projects/{self.project_id}" + + # Create the secret + secret = { + "labels": labels or {}, + "replication": {"automatic": {}} + } + + loop = asyncio.get_event_loop() + + # Create secret resource + create_response = await loop.run_in_executor( + None, + client.create_secret, + { + "parent": parent, + "secret_id": secret_name, + "secret": secret + } + ) + + # Add secret version with the actual value + version_response = await loop.run_in_executor( + None, + client.add_secret_version, + { + "parent": create_response.name, + "payload": {"data": secret_value.encode("UTF-8")} + } + ) + + logger.info(f"Successfully created secret: {secret_name}") + return version_response.name + + except gcp_exceptions.AlreadyExists: + error_msg = f"Secret already exists: {secret_name}" + logger.error(error_msg) + raise SecretManagerError(error_msg) + + except Exception as e: + error_msg = f"Failed to create secret {secret_name}: {e}" + logger.error(error_msg) + raise SecretManagerError(error_msg) + + def clear_cache(self) -> None: + """Clear the secrets cache.""" + self._cache.clear() + logger.info("Secrets cache cleared") + + +# Global secrets manager instance +secrets_manager = SecretsManager() + + +# Convenience functions for common operations +async def get_secret(secret_name: str, version: str = "latest") -> str: + """Get a secret value.""" + return await secrets_manager.get_secret(secret_name, version) + + +async def get_database_url() -> str: + """Get MongoDB connection URL from Secret Manager.""" + try: + return await secrets_manager.get_secret("mongodb-url") + except SecretManagerError: + # Fallback to environment variable + url = os.getenv("MONGODB_URL") + if not url: + raise SecretManagerError("MongoDB URL not available in secrets or environment") + return url + + +async def get_redis_url() -> str: + """Get Redis connection URL from Secret Manager.""" + try: + return await secrets_manager.get_secret("redis-url") + except SecretManagerError: + # Fallback to environment variable + url = os.getenv("REDIS_URL") + if not url: + raise SecretManagerError("Redis URL not available in secrets or environment") + return url + + +async def get_jwt_secrets() -> Dict[str, str]: + """Get JWT secrets from Secret Manager.""" + try: + return await secrets_manager.get_secrets_batch([ + "jwt-secret", + "jwt-refresh-secret" + ]) + except SecretManagerError: + # Fallback to environment variables + return { + "jwt-secret": os.getenv("JWT_SECRET_KEY", "dev-secret-change-in-production"), + "jwt-refresh-secret": os.getenv("JWT_REFRESH_SECRET_KEY", "dev-refresh-secret-change-in-production") + } + + +async def get_api_keys() -> Dict[str, str]: + """Get all API keys from Secret Manager.""" + api_keys = {} + + secret_names = [ + "gemini-api-key", + "sendgrid-api-key", + "elevenlabs-api-key", + "sentry-dsn" + ] + + try: + api_keys = await secrets_manager.get_secrets_batch(secret_names) + except SecretManagerError: + logger.warning("Failed to retrieve some API keys from Secret Manager, using environment fallback") + + # Fallback to environment variables for missing keys + env_mapping = { + "gemini-api-key": "GEMINI_API_KEY", + "sendgrid-api-key": "SENDGRID_API_KEY", + "elevenlabs-api-key": "ELEVENLABS_API_KEY", + "sentry-dsn": "SENTRY_DSN" + } + + for secret_name, env_var in env_mapping.items(): + if secret_name not in api_keys: + env_value = os.getenv(env_var) + if env_value: + api_keys[secret_name] = env_value + else: + logger.warning(f"API key {secret_name} not available in secrets or environment") + + return api_keys \ No newline at end of file diff --git a/backend/app/services/translate.py b/backend/app/services/translate.py new file mode 100644 index 0000000..be08fda --- /dev/null +++ b/backend/app/services/translate.py @@ -0,0 +1,110 @@ + +from google.cloud import translate_v2 as translate + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +class TranslateService: + def __init__(self): + if settings.translate_api_key: + self.client = translate.Client() + else: + logger.warning("Google Translate API key not configured") + self.client = None + + async def translate_vtt(self, vtt_content: str, target_language: str) -> str: + """ + Translate VTT content while preserving timing and structure + """ + if not self.client: + raise ValueError("Google Translate not configured") + + # Parse VTT to extract cues + cues = self._parse_vtt_cues(vtt_content) + + # Extract text for translation + texts_to_translate = [cue["text"] for cue in cues] + + if not texts_to_translate: + return vtt_content + + try: + # Translate all texts in batch + results = self.client.translate( + texts_to_translate, + target_language=target_language, + source_language="en" + ) + + # Rebuild VTT with translated text + translated_cues = [] + for i, cue in enumerate(cues): + translated_text = results[i]["translatedText"] if isinstance(results, list) else results["translatedText"] + translated_cues.append({ + "start": cue["start"], + "end": cue["end"], + "text": translated_text + }) + + return self._build_vtt(translated_cues) + + except Exception as e: + logger.error(f"Translation failed: {e}") + raise + + def _parse_vtt_cues(self, vtt_content: str) -> list[dict[str, str]]: + """Parse VTT content and extract timing and text cues""" + lines = vtt_content.strip().split('\n') + cues = [] + current_cue = {} + + for line in lines: + line = line.strip() + + # Skip WEBVTT header and empty lines + if line == "WEBVTT" or line == "" or line.startswith("NOTE"): + continue + + # Check if line contains timing + if " --> " in line: + timing_parts = line.split(" --> ") + current_cue = { + "start": timing_parts[0].strip(), + "end": timing_parts[1].strip(), + "text": "" + } + elif current_cue and line: + # This is subtitle text + if current_cue.get("text"): + current_cue["text"] += " " + line + else: + current_cue["text"] = line + + # If next line is empty or timing, cue is complete + # For simplicity, we'll add the cue here and handle multi-line in a more robust way + if current_cue["text"]: + cues.append(current_cue.copy()) + current_cue = {} + + # Add final cue if exists + if current_cue and current_cue.get("text"): + cues.append(current_cue) + + return cues + + def _build_vtt(self, cues: list[dict[str, str]]) -> str: + """Build VTT content from cues""" + vtt_lines = ["WEBVTT", ""] + + for cue in cues: + vtt_lines.append(f"{cue['start']} --> {cue['end']}") + vtt_lines.append(cue["text"]) + vtt_lines.append("") # Empty line between cues + + return "\n".join(vtt_lines) + + +# Global service instance +translate_service = TranslateService() diff --git a/backend/app/services/tts.py b/backend/app/services/tts.py new file mode 100644 index 0000000..377b7c5 --- /dev/null +++ b/backend/app/services/tts.py @@ -0,0 +1,301 @@ +import io +from typing import Optional + +import aiohttp +from google.cloud import texttospeech +from pydub import AudioSegment + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +class TTSService: + def __init__(self): + # Initialize Google TTS + if settings.google_tts_credentials: + self.google_client = texttospeech.TextToSpeechClient() + else: + logger.warning("Google TTS credentials not configured") + self.google_client = None + + # Check ElevenLabs availability + self.elevenlabs_available = bool(settings.elevenlabs_api_key) + + async def synthesize_audio_description( + self, + ad_vtt_content: str, + language_code: str = "en-US", + voice_name: Optional[str] = None + ) -> bytes: + """ + Generate MP3 audio from audio description VTT content + Synthesizes each cue separately and stitches them together with timing + Uses Google TTS with ElevenLabs fallback + """ + # Try Google TTS first, fallback to ElevenLabs + try: + if self.google_client: + return await self._synthesize_with_google(ad_vtt_content, language_code, voice_name) + elif self.elevenlabs_available: + return await self._synthesize_with_elevenlabs(ad_vtt_content, language_code, voice_name) + else: + raise ValueError("No TTS service configured") + except Exception as e: + if self.elevenlabs_available and self.google_client: + logger.warning(f"Google TTS failed, trying ElevenLabs: {e}") + return await self._synthesize_with_elevenlabs(ad_vtt_content, language_code, voice_name) + raise + + async def _synthesize_with_google( + self, + ad_vtt_content: str, + language_code: str = "en-US", + voice_name: Optional[str] = None + ) -> bytes: + """Generate MP3 using Google TTS with 2-second pauses between passages""" + + # Parse VTT cues + cues = self._parse_ad_cues(ad_vtt_content) + + if not cues: + raise ValueError("No audio description cues found") + + # Synthesize each cue separately with precise timing anchoring + audio_segments = [] + current_audio_position = 0.0 # Track actual audio timeline position + + for i, cue in enumerate(cues): + # Calculate where this cue should start (anchored to VTT timing) + target_start_time = cue["start_time"] + + # Add silence to reach the exact VTT start time + if target_start_time > current_audio_position: + silence_duration = target_start_time - current_audio_position + silence = AudioSegment.silent(duration=int(silence_duration * 1000)) + audio_segments.append(silence) + current_audio_position = target_start_time + + # Synthesize this cue's text + text = cue["text"].strip() + if text: + # Ensure proper punctuation for natural TTS flow + if not text.endswith(('.', '!', '?')): + text += "." + + # Synthesize this individual cue + audio_data = await self._synthesize_text_google( + text, language_code, voice_name + ) + + # Convert to AudioSegment and get actual duration + audio_segment = AudioSegment.from_file(io.BytesIO(audio_data), format="mp3") + audio_segments.append(audio_segment) + + # Update current position based on actual audio duration (not VTT end time) + actual_audio_duration = len(audio_segment) / 1000.0 # Convert ms to seconds + current_audio_position += actual_audio_duration + + # Combine all segments + if audio_segments: + final_audio = sum(audio_segments, AudioSegment.empty()) + else: + # Fallback to empty audio if no segments + final_audio = AudioSegment.silent(duration=1000) + + # Export to MP3 + output_buffer = io.BytesIO() + final_audio.export(output_buffer, format="mp3", bitrate="128k") + + return output_buffer.getvalue() + + async def _synthesize_with_elevenlabs( + self, + ad_vtt_content: str, + language_code: str = "en-US", + voice_name: Optional[str] = None + ) -> bytes: + """Generate MP3 using ElevenLabs TTS""" + # Parse VTT cues + cues = self._parse_ad_cues(ad_vtt_content) + + if not cues: + raise ValueError("No audio description cues found") + + # Get voice ID for language + voice_id = self._get_elevenlabs_voice(language_code, voice_name) + + # Synthesize each cue with precise timing anchoring + audio_segments = [] + current_audio_position = 0.0 # Track actual audio timeline position + + for i, cue in enumerate(cues): + # Calculate where this cue should start (anchored to VTT timing) + target_start_time = cue["start_time"] + + # Add silence to reach the exact VTT start time + if target_start_time > current_audio_position: + silence_duration = target_start_time - current_audio_position + silence = AudioSegment.silent(duration=int(silence_duration * 1000)) + audio_segments.append(silence) + current_audio_position = target_start_time + + # Synthesize this cue with ElevenLabs + text = cue["text"].strip() + if text: + audio_data = await self._synthesize_text_elevenlabs(text, voice_id) + + # Convert to AudioSegment and get actual duration + audio_segment = AudioSegment.from_file(io.BytesIO(audio_data), format="mp3") + audio_segments.append(audio_segment) + + # Update current position based on actual audio duration (not VTT end time) + actual_audio_duration = len(audio_segment) / 1000.0 # Convert ms to seconds + current_audio_position += actual_audio_duration + + # Combine all segments + final_audio = sum(audio_segments, AudioSegment.empty()) + + # Export to MP3 + output_buffer = io.BytesIO() + final_audio.export(output_buffer, format="mp3", bitrate="128k") + + return output_buffer.getvalue() + + async def _synthesize_text_google( + self, + text: str, + language_code: str, + voice_name: Optional[str] = None + ) -> bytes: + """Synthesize a single text string to audio using Google TTS""" + # Configure voice + if not voice_name: + voice_name = settings.google_tts_voices.get(language_code, "en-US-Neural2-D") + + voice = texttospeech.VoiceSelectionParams( + language_code=language_code, + name=voice_name + ) + + # Configure audio + audio_config = texttospeech.AudioConfig( + audio_encoding=texttospeech.AudioEncoding.MP3, + speaking_rate=1.2, # Faster cadence for better flow + pitch=0.0 + ) + + # Synthesize + synthesis_input = texttospeech.SynthesisInput(text=text) + + response = self.google_client.synthesize_speech( + input=synthesis_input, + voice=voice, + audio_config=audio_config + ) + + return response.audio_content + + async def _synthesize_text_elevenlabs(self, text: str, voice_id: str) -> bytes: + """Synthesize text using ElevenLabs API""" + url = f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}" + + headers = { + "Accept": "audio/mpeg", + "Content-Type": "application/json", + "xi-api-key": settings.elevenlabs_api_key + } + + data = { + "text": text, + "model_id": "eleven_multilingual_v2", + "voice_settings": { + "stability": 0.5, + "similarity_boost": 0.5, + "style": 0.0, + "use_speaker_boost": True + } + } + + async with aiohttp.ClientSession() as session: + async with session.post(url, json=data, headers=headers) as response: + if response.status == 200: + return await response.read() + else: + error_text = await response.text() + raise ValueError(f"ElevenLabs TTS failed: {response.status} - {error_text}") + + def _get_elevenlabs_voice(self, language_code: str, voice_name: Optional[str] = None) -> str: + """Get ElevenLabs voice ID for language""" + if voice_name: + return voice_name + + return settings.elevenlabs_voices.get(language_code, "21m00Tcm4TlvDq8ikWAM") + + def _parse_ad_cues(self, vtt_content: str) -> list[dict]: + """Parse audio description VTT and extract timing + text""" + lines = vtt_content.strip().split('\n') + cues = [] + + i = 0 + while i < len(lines): + line = lines[i].strip() + + # Skip header and empty lines + if line == "WEBVTT" or line == "" or line.startswith("NOTE"): + i += 1 + continue + + # Check for timing line + if " --> " in line: + timing_parts = line.split(" --> ") + start_time = self._parse_timestamp(timing_parts[0].strip()) + end_time = self._parse_timestamp(timing_parts[1].strip()) + + # Get text from next line(s) + i += 1 + text_lines = [] + while i < len(lines) and lines[i].strip() != "": + text_lines.append(lines[i].strip()) + i += 1 + + if text_lines: + cues.append({ + "start_time": start_time, + "end_time": end_time, + "text": " ".join(text_lines) + }) + else: + i += 1 + + return cues + + def _parse_timestamp(self, timestamp: str) -> float: + """Convert VTT timestamp to seconds""" + # Format: HH:MM:SS.mmm or MM:SS.mmm + parts = timestamp.split(":") + + if len(parts) == 3: # HH:MM:SS.mmm + hours, minutes, seconds = parts + elif len(parts) == 2: # MM:SS.mmm + hours, minutes, seconds = "0", parts[0], parts[1] + else: + raise ValueError(f"Invalid timestamp format: {timestamp}") + + # Parse seconds and milliseconds + sec_parts = seconds.split(".") + seconds = int(sec_parts[0]) + milliseconds = int(sec_parts[1]) if len(sec_parts) > 1 else 0 + + total_seconds = ( + int(hours) * 3600 + + int(minutes) * 60 + + seconds + + milliseconds / 1000.0 + ) + + return total_seconds + + +# Global service instance +tts_service = TTSService() diff --git a/backend/app/services/validation.py b/backend/app/services/validation.py new file mode 100644 index 0000000..174d6af --- /dev/null +++ b/backend/app/services/validation.py @@ -0,0 +1,130 @@ +from typing import Dict, List, Any + +from ..core.logging import get_logger +from ..lib.vtt import VTTEditor +from ..services.gcs import gcs_service + +logger = get_logger(__name__) + + +class AssetValidationService: + """Service for validating job assets before completion""" + + @staticmethod + async def validate_job_assets(job_doc: Dict[str, Any]) -> tuple[bool, List[str]]: + """ + Validate all assets for a job before allowing completion + Returns (is_valid, list_of_errors) + """ + errors = [] + outputs = job_doc.get("outputs", {}) + requested_outputs = job_doc.get("requested_outputs", {}) + + if not outputs: + errors.append("No outputs generated for this job") + return False, errors + + # Validate each language + for language in requested_outputs.get("languages", ["en"]): + lang_output = outputs.get(language) + if not lang_output: + errors.append(f"Missing outputs for language: {language}") + continue + + # Validate captions VTT if requested + if requested_outputs.get("captions_vtt"): + captions_error = await AssetValidationService._validate_vtt_asset( + lang_output.get("captions_vtt_gcs"), + f"{language} captions VTT" + ) + if captions_error: + errors.append(captions_error) + + # Validate audio description VTT if requested + if requested_outputs.get("audio_description_vtt"): + ad_vtt_error = await AssetValidationService._validate_vtt_asset( + lang_output.get("ad_vtt_gcs"), + f"{language} audio description VTT" + ) + if ad_vtt_error: + errors.append(ad_vtt_error) + + # Validate MP3 if requested + if requested_outputs.get("audio_description_mp3"): + mp3_error = await AssetValidationService._validate_mp3_asset( + lang_output.get("ad_mp3_gcs"), + f"{language} audio description MP3" + ) + if mp3_error: + errors.append(mp3_error) + + # Check minimum quality requirements + ai_confidence = job_doc.get("ai", {}).get("confidence", 0) + if ai_confidence < 0.7: + errors.append(f"AI confidence too low: {ai_confidence:.1%} (minimum: 70%)") + + return len(errors) == 0, errors + + @staticmethod + async def _validate_vtt_asset(gcs_uri: str, asset_name: str) -> str | None: + """Validate a VTT asset exists and is properly formatted""" + if not gcs_uri: + return f"Missing {asset_name}" + + try: + # Download and validate VTT content + blob_path = gcs_uri.replace(f"gs://{gcs_service.bucket.name}/", "") + blob = gcs_service.bucket.blob(blob_path) + + if not blob.exists(): + return f"{asset_name} file not found in storage" + + vtt_content = blob.download_as_text() + is_valid, vtt_errors = VTTEditor.validate_vtt(vtt_content) + + if not is_valid: + return f"{asset_name} validation failed: {'; '.join(vtt_errors[:3])}" + + # Check minimum content requirements + cue_count = VTTEditor.get_cue_count(vtt_content) + if cue_count == 0: + return f"{asset_name} contains no cues" + + except Exception as e: + logger.error(f"Failed to validate {asset_name}: {e}") + return f"{asset_name} validation error: {str(e)}" + + return None + + @staticmethod + async def _validate_mp3_asset(gcs_uri: str, asset_name: str) -> str | None: + """Validate an MP3 asset exists and has reasonable properties""" + if not gcs_uri: + return f"Missing {asset_name}" + + try: + blob_path = gcs_uri.replace(f"gs://{gcs_service.bucket.name}/", "") + blob = gcs_service.bucket.blob(blob_path) + + if not blob.exists(): + return f"{asset_name} file not found in storage" + + # Reload blob to get metadata (including size) + blob.reload() + + # Check file size (should be reasonable for audio) + size_mb = blob.size / (1024 * 1024) if blob.size else 0 + if size_mb < 0.01: # Less than 10KB + return f"{asset_name} file too small (likely empty)" + elif size_mb > 500: # More than 500MB + return f"{asset_name} file too large ({size_mb:.1f}MB)" + + except Exception as e: + logger.error(f"Failed to validate {asset_name}: {e}") + return f"{asset_name} validation error: {str(e)}" + + return None + + +# Global service instance +asset_validation_service = AssetValidationService() \ No newline at end of file diff --git a/backend/app/tasks/__init__.py b/backend/app/tasks/__init__.py new file mode 100644 index 0000000..cfee0d7 --- /dev/null +++ b/backend/app/tasks/__init__.py @@ -0,0 +1,158 @@ +from celery import Celery +from celery.signals import task_failure, task_success, task_retry + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + +celery_app = Celery( + "accessible-video-tasks", + broker=settings.redis_url, + backend=settings.redis_url, +) + +celery_app.conf.update( + task_serializer="json", + accept_content=["json"], + result_serializer="json", + timezone="UTC", + enable_utc=True, + task_track_started=True, + task_time_limit=30 * 60, # 30 minutes default + task_soft_time_limit=25 * 60, # 25 minutes default + worker_prefetch_multiplier=1, + task_acks_late=True, + worker_max_tasks_per_child=1000, + task_routes={ + "app.tasks.ingest_and_ai.*": {"queue": "ingest"}, + "app.tasks.translate_and_synthesize.*": {"queue": "default"}, + "app.tasks.notify.*": {"queue": "notify"}, + "app.tasks.watchers.*": {"queue": "default"}, + }, + task_default_queue="default", + task_create_missing_queues=True, + # Task-specific timeout overrides + task_annotations={ + 'app.tasks.watchers.start_change_stream_watcher': { + 'time_limit': None, + 'soft_time_limit': None, + }, + 'app.tasks.watchers.ensure_watcher_running': { + 'time_limit': 300, # 5 minutes + 'soft_time_limit': 240, # 4 minutes + }, + }, +) + +# Add a simple test task for debugging +@celery_app.task +def test_task(message="test"): + """Simple test task to verify worker connectivity""" + logger.info(f"🧪 TEST TASK EXECUTED: {message}") + print(f"🧪 TEST TASK EXECUTED: {message}") + return f"Test task completed: {message}" + + +# Add task received handler for debugging +from celery.signals import task_received, task_prerun, worker_ready +import threading +import time + +@worker_ready.connect +def worker_ready_handler(sender=None, **kwargs): + """Log when worker is ready and start heartbeat""" + logger.info(f"🟢 WORKER READY: {sender}") + print(f"🟢 WORKER READY: {sender} - Worker is online and listening!") + + # Start MongoDB change stream watcher + # Note: The main job progression is handled by immediate triggering in approve_english endpoint + # This watcher provides redundancy for status change detection + if _watchers_available and 'app.tasks.watchers.ensure_watcher_running' in celery_app.tasks: + try: + from .watchers import ensure_watcher_running + ensure_watcher_running.apply_async(countdown=3) # Start after 3 seconds + logger.info("Scheduled MongoDB change stream watcher to start") + except Exception as e: + logger.error(f"Failed to schedule change stream watcher: {e}") + else: + logger.info("Watcher not available or not registered, using primary job progression via approve_english endpoint") + + +@task_received.connect +def task_received_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, retries=None, eta=None, **kwds): + """Log when a task is received by the worker""" + logger.info(f"🎯 TASK RECEIVED: {task} [{task_id}] with args: {args}") + print(f"🎯 TASK RECEIVED: {task} [{task_id}] - Worker is picking up the task!") + +@task_prerun.connect +def task_prerun_handler(sender=None, task_id=None, task=None, args=None, kwargs=None, **kwds): + """Log when a task starts executing""" + logger.info(f"🚀 TASK STARTING: {task} [{task_id}]") + print(f"🚀 TASK STARTING: {task} [{task_id}] - About to execute!") + +# Celery signal handlers for centralized logging +@task_failure.connect +def task_failure_handler(sender=None, task_id=None, exception=None, traceback=None, einfo=None, **kwargs): + """Log task failures to centralized logging""" + exception_type = exception.__class__.__name__ if exception else "Unknown" + exception_msg = str(exception) if exception else "No details" + + # Log comprehensive error details + error_details = f""" +=== CELERY TASK FAILURE === +Task: {sender} +Task ID: {task_id} +Exception Type: {exception_type} +Exception Message: {exception_msg} +Full Traceback: +{traceback} +Additional Info: {einfo} +============================= +""" + logger.error(error_details) + + # Also log to stdout for immediate visibility + print(f"🚨 TASK FAILURE: {sender} [{task_id}] - {exception_type}: {exception_msg}") + if traceback: + print(f"Full traceback:\n{traceback}") + + +@task_success.connect +def task_success_handler(sender=None, result=None, **kwargs): + """Log task success""" + result_str = str(result)[:100] if result else "No result" + logger.info(f"Celery task completed: {sender} - Result: {result_str}") + + +@task_retry.connect +def task_retry_handler(sender=None, task_id=None, reason=None, einfo=None, **kwargs): + """Log task retries""" + reason_str = str(reason) if reason else "No reason provided" + logger.warning(f"Celery task retry: {sender} [{task_id}] - Reason: {reason_str}") + + +def import_task_modules(): + """Import all task modules to register them with Celery""" + try: + from . import ingest_and_ai # noqa: E402, F401 + from . import translate_and_synthesize # noqa: E402, F401 + from . import notify # noqa: E402, F401 + logger.info("Successfully imported core task modules") + except Exception as e: + logger.error(f"Error importing core task modules: {e}") + + # Import watchers module conditionally to handle import errors gracefully + try: + from . import watchers # noqa: E402, F401 + logger.info("Successfully imported watchers module") + return True + except ImportError as e: + logger.warning(f"Could not import watchers module: {e}") + return False + except Exception as e: + logger.error(f"Error importing watchers module: {e}") + return False + +# Import task modules at startup +_watchers_available = import_task_modules() diff --git a/backend/app/tasks/__pycache__/__init__.cpython-313.pyc b/backend/app/tasks/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..346a0ca Binary files /dev/null and b/backend/app/tasks/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/app/tasks/__pycache__/ingest_and_ai.cpython-313.pyc b/backend/app/tasks/__pycache__/ingest_and_ai.cpython-313.pyc new file mode 100644 index 0000000..3e9230b Binary files /dev/null and b/backend/app/tasks/__pycache__/ingest_and_ai.cpython-313.pyc differ diff --git a/backend/app/tasks/__pycache__/notify.cpython-313.pyc b/backend/app/tasks/__pycache__/notify.cpython-313.pyc new file mode 100644 index 0000000..901e6fa Binary files /dev/null and b/backend/app/tasks/__pycache__/notify.cpython-313.pyc differ diff --git a/backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc b/backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc new file mode 100644 index 0000000..8049562 Binary files /dev/null and b/backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc differ diff --git a/backend/app/tasks/__pycache__/watchers.cpython-313.pyc b/backend/app/tasks/__pycache__/watchers.cpython-313.pyc new file mode 100644 index 0000000..f2d74f8 Binary files /dev/null and b/backend/app/tasks/__pycache__/watchers.cpython-313.pyc differ diff --git a/backend/app/tasks/ingest_and_ai.py b/backend/app/tasks/ingest_and_ai.py new file mode 100644 index 0000000..c59a879 --- /dev/null +++ b/backend/app/tasks/ingest_and_ai.py @@ -0,0 +1,213 @@ +import asyncio +import os +import tempfile +from datetime import datetime + +import ffmpeg +from celery import Task +from motor.motor_asyncio import AsyncIOMotorClient + +from ..core.config import settings +from ..core.logging import get_logger +from ..models.job import JobStatus +from ..services.gcs import gcs_service, upload_vtt_to_gcs +from ..services.gemini import gemini_service +from . import celery_app + +logger = get_logger(__name__) + + +class AsyncTask(Task): + """Base task class that supports async execution""" + def __call__(self, *args, **kwargs): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(self.run_async(*args, **kwargs)) + finally: + loop.close() + + async def run_async(self, *args, **kwargs): + raise NotImplementedError + + +class IngestAndAITask(AsyncTask): + async def run_async(self, job_id: str): + return await ingest_and_ai_task_impl(job_id) + + +@celery_app.task(bind=True, base=IngestAndAITask) +def ingest_and_ai_task(self, job_id: str): + """ + Pipeline 1: Ingestion & AI Processing + Task wrapper that delegates to async implementation + """ + # This method is called by AsyncTask.__call__ + pass + + +async def ingest_and_ai_task_impl(job_id: str): + """ + Pipeline 1: Ingestion & AI Processing + 1. Update status to 'ingesting' + 2. Probe video for metadata (duration, codec) + 3. Process with Gemini 2.5 Pro + 4. Generate VTT files + 5. Update status to 'pending_qc' + """ + logger.info(f"Starting ingestion and AI processing for job {job_id}") + + # Connect to MongoDB + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + # Update status to ingesting + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.INGESTING.value, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.INGESTING.value, + "by": "system" + } + } + } + ) + + # Get job details + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise ValueError(f"Job {job_id} not found") + + # Download video file temporarily for processing + source_blob_path = job_doc["source"]["gcs_uri"].replace(f"gs://{settings.gcs_bucket}/", "") + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp_file: + temp_path = temp_file.name + + # Download from GCS + blob = gcs_service.bucket.blob(source_blob_path) + blob.download_to_filename(temp_path) + + try: + # Update status to AI processing + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.AI_PROCESSING.value, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.AI_PROCESSING.value, + "by": "system" + } + } + } + ) + + # Probe video for metadata + duration = await _get_video_duration(temp_path) + + # Update source with duration + await db.jobs.update_one( + {"_id": job_id}, + {"$set": {"source.duration_s": duration}} + ) + + # Process with Gemini + ai_result = await gemini_service.extract_accessibility(temp_path) + + # Final safety check for required fields + required_fields = ["captions_vtt", "audio_description_vtt"] + missing_fields = [field for field in required_fields if field not in ai_result] + + if missing_fields: + logger.error(f"Missing required fields after AI processing: {missing_fields}") + # Create fallback content for missing fields + if "audio_description_vtt" in missing_fields: + ai_result["audio_description_vtt"] = "WEBVTT\n\n00:00:00.000 --> 00:00:05.000\nVideo content with visual elements." + logger.info("Created fallback audio_description_vtt") + + # Upload VTT files to GCS + captions_gcs_uri = await upload_vtt_to_gcs( + ai_result["captions_vtt"], + f"{job_id}/en/captions.vtt" + ) + + ad_gcs_uri = await upload_vtt_to_gcs( + ai_result["audio_description_vtt"], + f"{job_id}/en/ad.vtt" + ) + + # Update job with AI results and outputs + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.PENDING_QC.value, + "ai.ingestion_json": ai_result, + "ai.confidence": ai_result["confidence"], + "outputs.en": { + "captions_vtt_gcs": captions_gcs_uri, + "ad_vtt_gcs": ad_gcs_uri + }, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.PENDING_QC.value, + "by": "system" + } + } + } + ) + + logger.info(f"Successfully completed ingestion and AI processing for job {job_id}") + + finally: + # Clean up temp file + os.unlink(temp_path) + + except Exception as e: + logger.error(f"Ingestion and AI processing failed for job {job_id}: {e}") + + # Update job with error + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "error": { + "type": "ingestion_failure", + "message": str(e), + "timestamp": datetime.utcnow().isoformat() + }, + "updated_at": datetime.utcnow() + } + } + ) + + raise + + finally: + client.close() + + +async def _get_video_duration(video_path: str) -> float: + """Get video duration using ffprobe""" + try: + probe = ffmpeg.probe(video_path) + duration = float(probe['streams'][0]['duration']) + return duration + except Exception as e: + logger.warning(f"Could not determine video duration: {e}") + return 0.0 diff --git a/backend/app/tasks/notify.py b/backend/app/tasks/notify.py new file mode 100644 index 0000000..ac0ab4a --- /dev/null +++ b/backend/app/tasks/notify.py @@ -0,0 +1,142 @@ +import asyncio +from datetime import datetime + +from celery import Task +from motor.motor_asyncio import AsyncIOMotorClient + +from ..core.config import settings +from ..core.logging import get_logger +from ..models.audit_log import AuditLogCreate +from ..services.emailer import email_service +from ..services.gcs import get_signed_download_url +from . import celery_app + +logger = get_logger(__name__) + + +class AsyncTask(Task): + """Base task class that supports async execution""" + def __call__(self, *args, **kwargs): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(self.run_async(*args, **kwargs)) + finally: + loop.close() + + async def run_async(self, *args, **kwargs): + raise NotImplementedError + + +@celery_app.task(bind=True, base=AsyncTask) +async def notify_client_task(self, job_id: str): + """ + Pipeline 3: Client Notification + Triggered when job status changes to 'completed' + """ + logger.info(f"Starting client notification for job {job_id}") + + # Connect to MongoDB + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + # Get job and client details + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + raise ValueError(f"Job {job_id} not found") + + if job_doc["status"] != "completed": + logger.warning(f"Job {job_id} not in completed status, skipping notification") + return + + # Get client email + client_doc = await db.users.find_one({"_id": job_doc["client_id"]}) + if not client_doc: + raise ValueError(f"Client {job_doc['client_id']} not found") + + # Generate signed URLs for all outputs + download_links = {} + outputs = job_doc.get("outputs", {}) + + for language, lang_output in outputs.items(): + if not isinstance(lang_output, dict): + continue + + lang_downloads = {} + + # Captions VTT + if "captions_vtt_gcs" in lang_output: + blob_path = lang_output["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["captions_vtt"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for captions {language}: {e}") + + # Audio Description VTT + if "ad_vtt_gcs" in lang_output: + blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["audio_description_vtt"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for AD VTT {language}: {e}") + + # Audio Description MP3 + if "ad_mp3_gcs" in lang_output: + blob_path = lang_output["ad_mp3_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + try: + signed_url = await get_signed_download_url(blob_path, 24) + lang_downloads["audio_description_mp3"] = signed_url + except Exception as e: + logger.warning(f"Failed to generate signed URL for AD MP3 {language}: {e}") + + if lang_downloads: + download_links[language] = lang_downloads + + # Send completion email + success = await email_service.send_completion_email( + recipient_email=client_doc["email"], + job_title=job_doc["title"], + download_links=download_links + ) + + if success: + # Log audit entry + audit_log = AuditLogCreate( + job_id=job_id, + action="client_notified", + details={ + "email": client_doc["email"], + "download_count": sum(len(files) for files in download_links.values()) + } + ) + await db.audit_logs.insert_one(audit_log.dict()) + + logger.info(f"Successfully notified client for job {job_id}") + else: + raise ValueError("Failed to send completion email") + + except Exception as e: + logger.error(f"Client notification failed for job {job_id}: {e}") + + # Update job with error + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "error": { + "type": "notification_failure", + "message": str(e), + "timestamp": datetime.utcnow().isoformat() + }, + "updated_at": datetime.utcnow() + } + } + ) + + raise + + finally: + client.close() diff --git a/backend/app/tasks/translate_and_synthesize.py b/backend/app/tasks/translate_and_synthesize.py new file mode 100644 index 0000000..b90fe67 --- /dev/null +++ b/backend/app/tasks/translate_and_synthesize.py @@ -0,0 +1,317 @@ +import asyncio +from datetime import datetime +from typing import Any +import time +import random + +from celery import Task +from motor.motor_asyncio import AsyncIOMotorClient + +from ..core.config import settings +from ..core.logging import get_logger +from ..models.job import JobStatus +from ..services.gcs import gcs_service, upload_vtt_to_gcs +from ..services.gemini import gemini_service +from ..services.translate import translate_service +from ..services.tts import tts_service +from . import celery_app + +logger = get_logger(__name__) + + +async def retry_with_backoff(func, max_retries=3, base_delay=1): + """Retry a function with exponential backoff""" + last_exception = None + + for attempt in range(max_retries): + try: + return await func() + except Exception as e: + last_exception = e + if attempt == max_retries - 1: + break + + # Exponential backoff with jitter + delay = base_delay * (2 ** attempt) + random.uniform(0, 1) + logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay:.2f}s: {e}") + await asyncio.sleep(delay) + + raise last_exception + + +@celery_app.task(bind=True) +def translate_and_synthesize_task(self, job_id: str): + """ + Pipeline 2: Translation & MP3 Generation + Triggered when job status changes to 'approved_english' + """ + logger.info(f"🚀 CELERY TASK STARTED: translate_and_synthesize_task for job {job_id}") + + try: + logger.info(f"📝 About to call asyncio.run for job {job_id}") + result = asyncio.run(_async_translate_and_synthesize(job_id)) + logger.info(f"✅ CELERY TASK COMPLETED successfully for job {job_id}") + return result + except Exception as e: + logger.error(f"❌ CELERY TASK FAILED for job {job_id}: {str(e)}") + logger.error(f"❌ Exception type: {type(e).__name__}") + logger.error(f"❌ Exception args: {e.args}") + import traceback + logger.error(f"❌ Full traceback: {traceback.format_exc()}") + raise + + +async def _async_translate_and_synthesize(job_id: str): + """Async implementation of translation and synthesis""" + logger.info(f"🔄 ASYNC FUNCTION STARTED: _async_translate_and_synthesize for job {job_id}") + + # Connect to MongoDB + logger.info(f"📡 Connecting to MongoDB for job {job_id}") + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + logger.info(f"📡 MongoDB connection established for job {job_id}") + + try: + # Get job details + logger.info(f"🔍 Looking up job document for job {job_id}") + job_doc = await db.jobs.find_one({"_id": job_id}) + if not job_doc: + logger.error(f"❌ Job {job_id} not found in database!") + raise ValueError(f"Job {job_id} not found") + + logger.info(f"✅ Found job document for {job_id}, status: {job_doc.get('status', 'UNKNOWN')}") + + if job_doc["status"] != JobStatus.APPROVED_ENGLISH.value: + logger.warning(f"⚠️ Job {job_id} not in approved_english status (current: {job_doc['status']}), skipping") + return + + logger.info(f"✅ Job {job_id} is in correct status, proceeding with translation") + + # Update status to translating + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.TRANSLATING.value, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.TRANSLATING.value, + "by": "system" + } + } + } + ) + + # Get English VTT content + en_outputs = job_doc["outputs"]["en"] + + # Download English VTT files + captions_blob_path = en_outputs["captions_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + ad_blob_path = en_outputs["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + + captions_blob = gcs_service.bucket.blob(captions_blob_path) + ad_blob = gcs_service.bucket.blob(ad_blob_path) + + en_captions_vtt = captions_blob.download_as_text() + en_ad_vtt = ad_blob.download_as_text() + + # Process each requested language + requested_languages = job_doc["requested_outputs"]["languages"] + transcreation_languages = job_doc["requested_outputs"]["transcreation"] + + updated_outputs = job_doc.get("outputs", {}) + + for language in requested_languages: + if language == "en": + continue # Skip English as it's already processed + + logger.info(f"Processing language: {language}") + + try: + if language in transcreation_languages: + # Use transcreation for cultural adaptation with retry + async def transcreate(): + return await gemini_service.transcreate_content( + en_captions_vtt, + en_ad_vtt, + language, + brief="Standard accessibility content" + ) + + result = await retry_with_backoff(transcreate, max_retries=3) + translated_captions = result["captions_vtt"] + translated_ad = result["audio_description_vtt"] + origin = "transcreate" + + else: + # Use standard translation with retry + async def translate_captions(): + return await translate_service.translate_vtt(en_captions_vtt, language) + + async def translate_ad(): + return await translate_service.translate_vtt(en_ad_vtt, language) + + translated_captions = await retry_with_backoff(translate_captions, max_retries=3) + translated_ad = await retry_with_backoff(translate_ad, max_retries=3) + origin = "translate" + + # Upload translated VTT files + captions_gcs_uri = await upload_vtt_to_gcs( + translated_captions, + f"{job_id}/{language}/captions.vtt" + ) + + ad_gcs_uri = await upload_vtt_to_gcs( + translated_ad, + f"{job_id}/{language}/ad.vtt" + ) + + # Store language outputs + updated_outputs[language] = { + "captions_vtt_gcs": captions_gcs_uri, + "ad_vtt_gcs": ad_gcs_uri, + "origin": origin + } + + logger.info(f"Successfully processed VTT files for language: {language}") + + except Exception as e: + logger.error(f"Failed to process language {language}: {e}") + updated_outputs[language] = { + "origin": "translate" if language not in transcreation_languages else "transcreate", + "qa_notes": f"Translation failed: {str(e)}" + } + + # Update status to TTS generating + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.TTS_GENERATING.value, + "outputs": updated_outputs, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.TTS_GENERATING.value, + "by": "system" + } + } + } + ) + + # Generate TTS for languages that need MP3 + if job_doc["requested_outputs"]["audio_description_mp3"]: + await _generate_tts_for_languages(job_id, updated_outputs, db) + + # Update final status + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "status": JobStatus.PENDING_FINAL_REVIEW.value, + "updated_at": datetime.utcnow() + }, + "$push": { + "review.history": { + "at": datetime.utcnow(), + "status": JobStatus.PENDING_FINAL_REVIEW.value, + "by": "system" + } + } + } + ) + + logger.info(f"Successfully completed translation and synthesis for job {job_id}") + + except Exception as e: + logger.error(f"Translation and synthesis failed for job {job_id}: {e}") + + # Update job with error + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + "error": { + "type": "translation_failure", + "message": str(e), + "timestamp": datetime.utcnow().isoformat() + }, + "updated_at": datetime.utcnow() + } + } + ) + + raise + + finally: + client.close() + + +async def _generate_tts_for_languages(job_id: str, outputs: dict[str, Any], db): + """Generate TTS audio for each language's audio description""" + + # Always generate English MP3 + if "en" in outputs: + await _generate_language_tts(job_id, "en", outputs["en"], db) + + # Generate for other languages + for language, lang_output in outputs.items(): + if language != "en" and "ad_vtt_gcs" in lang_output: + await _generate_language_tts(job_id, language, lang_output, db) + + +async def _generate_language_tts(job_id: str, language: str, lang_output: dict, db): + """Generate TTS for a specific language""" + try: + # Download AD VTT content + ad_blob_path = lang_output["ad_vtt_gcs"].replace(f"gs://{settings.gcs_bucket}/", "") + ad_blob = gcs_service.bucket.blob(ad_blob_path) + ad_vtt_content = ad_blob.download_as_text() + + # Generate MP3 with retry + language_code = f"{language}-US" if language == "en" else f"{language}-{language.upper()}" + + async def synthesize(): + return await tts_service.synthesize_audio_description(ad_vtt_content, language_code) + + mp3_data = await retry_with_backoff(synthesize, max_retries=3) + + # Upload MP3 to GCS + mp3_blob_path = f"{job_id}/{language}/ad.mp3" + mp3_blob = gcs_service.bucket.blob(mp3_blob_path) + mp3_blob.content_type = "audio/mpeg" + mp3_blob.upload_from_string(mp3_data, content_type="audio/mpeg") + + mp3_gcs_uri = f"gs://{settings.gcs_bucket}/{mp3_blob_path}" + + # Update job outputs + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + f"outputs.{language}.ad_mp3_gcs": mp3_gcs_uri, + "updated_at": datetime.utcnow() + } + } + ) + + logger.info(f"Successfully generated TTS for {language}") + + except Exception as e: + logger.error(f"TTS generation failed for {language}: {e}") + + # Update with error note + await db.jobs.update_one( + {"_id": job_id}, + { + "$set": { + f"outputs.{language}.qa_notes": f"TTS generation failed: {str(e)}", + "updated_at": datetime.utcnow() + } + } + ) \ No newline at end of file diff --git a/backend/app/tasks/watchers.py b/backend/app/tasks/watchers.py new file mode 100644 index 0000000..14ac323 --- /dev/null +++ b/backend/app/tasks/watchers.py @@ -0,0 +1,136 @@ +import asyncio + +from motor.motor_asyncio import AsyncIOMotorClient + +from ..core.config import settings +from ..core.logging import get_logger +from ..models.job import JobStatus +from . import celery_app + +logger = get_logger(__name__) + + +@celery_app.task( + bind=True, + acks_late=True, # Acknowledge task only after completion + reject_on_worker_lost=True, # Retry if worker crashes + autoretry_for=(Exception,), # Auto-retry on any exception + retry_kwargs={'max_retries': None, 'countdown': 60}, # Infinite retries with 60s delay + retry_backoff=True, # Exponential backoff +) +def start_change_stream_watcher(self): + """Start MongoDB change stream watcher for job status changes""" + try: + asyncio.run(_watch_job_changes()) + except Exception as e: + logger.error(f"Change stream watcher failed: {e}") + # Task will auto-retry due to configuration + raise + + +async def _watch_job_changes(): + """Watch MongoDB change streams for job status updates""" + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + logger.info("Starting MongoDB change stream watcher") + + try: + # Add a heartbeat mechanism to ensure the connection stays alive + await client.admin.command('ping') + logger.info("MongoDB connection verified") + # Watch for changes to the jobs collection + pipeline = [ + { + "$match": { + "operationType": "update", + "fullDocument.status": { + "$in": [ + JobStatus.APPROVED_ENGLISH.value, + JobStatus.COMPLETED.value + ] + } + } + } + ] + + async with db.jobs.watch( + pipeline, + full_document="updateLookup", + max_await_time_ms=30000, # 30 second timeout for getMore operations + batch_size=10 # Process changes in small batches + ) as stream: + logger.info("Change stream watcher active, waiting for job status changes...") + + async for change in stream: + try: + job_doc = change["fullDocument"] + if not job_doc: + logger.warning("Received change event without fullDocument") + continue + + job_id = str(job_doc["_id"]) + status = job_doc["status"] + + logger.info(f"Job {job_id} status changed to {status}") + + if status == JobStatus.APPROVED_ENGLISH.value: + # Trigger translation and synthesis + from .translate_and_synthesize import translate_and_synthesize_task + translate_and_synthesize_task.delay(job_id) + logger.info(f"Enqueued translation task for job {job_id}") + + elif status == JobStatus.COMPLETED.value: + # Trigger client notification + from .notify import notify_client_task + notify_client_task.delay(job_id) + logger.info(f"Enqueued notification task for job {job_id}") + + except Exception as e: + logger.error(f"Error processing change stream event: {e}") + # Continue processing other events + continue + + except Exception as e: + error_msg = str(e) + if "replica sets" in error_msg: + logger.warning("Change stream watcher not available - MongoDB not configured as replica set") + logger.info("This is normal in development. Job progression works via immediate triggering in approval endpoint.") + else: + logger.error(f"Change stream watcher failed: {e}") + # Don't re-raise in development to prevent worker crashes + + finally: + client.close() + + +# Auto-start the watcher when the worker starts +@celery_app.task( + bind=True, + autoretry_for=(Exception,), + retry_kwargs={'max_retries': 3, 'countdown': 30} +) +def ensure_watcher_running(self): + """Ensure the change stream watcher is running""" + try: + # Check if watcher is already running + active_tasks = celery_app.control.inspect().active() + + if not active_tasks: + logger.warning("Could not inspect active tasks - starting watcher anyway") + else: + # Look for running watcher + for worker, tasks in active_tasks.items(): + if tasks: # Check if tasks list is not None + for task in tasks: + if task.get("name") == "app.tasks.watchers.start_change_stream_watcher": + logger.info(f"Change stream watcher already running on worker {worker}") + return + + # Start the watcher + result = start_change_stream_watcher.delay() + logger.info(f"Started change stream watcher with task ID: {result.id}") + + except Exception as e: + logger.error(f"Failed to ensure watcher is running: {e}") + raise # Will trigger retry diff --git a/backend/app/telemetry/__init__.py b/backend/app/telemetry/__init__.py new file mode 100644 index 0000000..1a7ca5f --- /dev/null +++ b/backend/app/telemetry/__init__.py @@ -0,0 +1,33 @@ +"""Telemetry package for OpenTelemetry tracing and metrics collection""" + +from .metrics import app_metrics, time_ai_request, time_job_processing, time_storage_operation, time_celery_task +from .tracing import ( + get_tracer, + instrument_dependencies, + instrument_fastapi_app, + setup_tracing, + trace_ai_operation, + trace_job_pipeline, + trace_storage_operation, + TracingContext, + trace_api_request, + trace_celery_task, +) + +__all__ = [ + "app_metrics", + "time_ai_request", + "time_job_processing", + "time_storage_operation", + "time_celery_task", + "get_tracer", + "instrument_dependencies", + "instrument_fastapi_app", + "setup_tracing", + "trace_ai_operation", + "trace_job_pipeline", + "trace_storage_operation", + "TracingContext", + "trace_api_request", + "trace_celery_task", +] \ No newline at end of file diff --git a/backend/app/telemetry/__pycache__/__init__.cpython-313.pyc b/backend/app/telemetry/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..2f62cdb Binary files /dev/null and b/backend/app/telemetry/__pycache__/__init__.cpython-313.pyc differ diff --git a/backend/app/telemetry/__pycache__/metrics.cpython-313.pyc b/backend/app/telemetry/__pycache__/metrics.cpython-313.pyc new file mode 100644 index 0000000..433fbd8 Binary files /dev/null and b/backend/app/telemetry/__pycache__/metrics.cpython-313.pyc differ diff --git a/backend/app/telemetry/__pycache__/tracing.cpython-313.pyc b/backend/app/telemetry/__pycache__/tracing.cpython-313.pyc new file mode 100644 index 0000000..c463124 Binary files /dev/null and b/backend/app/telemetry/__pycache__/tracing.cpython-313.pyc differ diff --git a/backend/app/telemetry/metrics.py b/backend/app/telemetry/metrics.py new file mode 100644 index 0000000..566a0b7 --- /dev/null +++ b/backend/app/telemetry/metrics.py @@ -0,0 +1,359 @@ +import time +from typing import Optional + +from opentelemetry import metrics +# from opentelemetry.exporter.prometheus import PrometheusMetricReader # Disabled for local dev +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.resources import Resource +from prometheus_client import start_http_server + +from ..core.config import settings +from ..core.logging import get_logger + +logger = get_logger(__name__) + + +class ApplicationMetrics: + """Central metrics collection for the accessible video platform""" + + def __init__(self): + self.setup_metrics() + + # Job processing metrics + self.job_total_counter = self.meter.create_counter( + name="jobs_total", + description="Total number of jobs created", + unit="1" + ) + + self.job_status_gauge = self.meter.create_up_down_counter( + name="jobs_by_status", + description="Current number of jobs by status", + unit="1" + ) + + self.job_processing_duration = self.meter.create_histogram( + name="job_processing_duration_seconds", + description="Time taken to process jobs through each stage", + unit="s" + ) + + # AI service metrics + self.ai_requests_counter = self.meter.create_counter( + name="ai_requests_total", + description="Total AI service requests", + unit="1" + ) + + self.ai_request_duration = self.meter.create_histogram( + name="ai_request_duration_seconds", + description="Duration of AI service requests", + unit="s" + ) + + self.ai_confidence_histogram = self.meter.create_histogram( + name="ai_confidence_score", + description="AI confidence scores distribution", + unit="1" + ) + + # Storage metrics + self.storage_operations_counter = self.meter.create_counter( + name="storage_operations_total", + description="Total storage operations", + unit="1" + ) + + self.storage_operation_duration = self.meter.create_histogram( + name="storage_operation_duration_seconds", + description="Duration of storage operations", + unit="s" + ) + + # Queue metrics + self.queue_tasks_counter = self.meter.create_counter( + name="celery_tasks_total", + description="Total Celery tasks processed", + unit="1" + ) + + self.queue_task_duration = self.meter.create_histogram( + name="celery_task_duration_seconds", + description="Duration of Celery task execution", + unit="s" + ) + + # User activity metrics + self.auth_attempts_counter = self.meter.create_counter( + name="auth_attempts_total", + description="Total authentication attempts", + unit="1" + ) + + self.active_users_gauge = self.meter.create_up_down_counter( + name="active_users", + description="Number of currently active users", + unit="1" + ) + + # Rate limiting metrics + self.rate_limit_counter = self.meter.create_counter( + name="rate_limit_checks_total", + description="Total rate limit checks performed", + unit="1" + ) + + # Request validation metrics + self.validation_counter = self.meter.create_counter( + name="request_validation_total", + description="Total request validations performed", + unit="1" + ) + + self.validation_duration = self.meter.create_histogram( + name="request_validation_duration_seconds", + description="Duration of request validation", + unit="s" + ) + + def setup_metrics(self): + """Initialize metrics provider and meter""" + resource = Resource.create({ + "service.name": "accessible-video-api", + "service.version": "1.0.0", + "deployment.environment": settings.app_env, + }) + + # Set up Prometheus metrics reader (disabled for local dev) + # prometheus_reader = PrometheusMetricReader() + + # Create metrics provider + provider = MeterProvider( + resource=resource, + # metric_readers=[prometheus_reader] # Disabled for local dev + ) + metrics.set_meter_provider(provider) + + # Get meter for this service + self.meter = metrics.get_meter("accessible-video-api") + + logger.info("Metrics provider initialized with Prometheus exporter") + + def start_prometheus_server(self, port: int = 8001): + """Start Prometheus metrics HTTP server""" + try: + start_http_server(port) + logger.info(f"Prometheus metrics server started on port {port}") + except Exception as e: + logger.error(f"Failed to start Prometheus server: {e}") + + # Job metrics methods + def record_job_created(self, client_id: str, language: str): + """Record a new job creation""" + self.job_total_counter.add( + 1, + attributes={ + "client_id": client_id, + "source_language": language, + "action": "created" + } + ) + + def record_job_status_change(self, job_id: str, old_status: str, new_status: str): + """Record job status change""" + # Decrement old status count + self.job_status_gauge.add( + -1, + attributes={"status": old_status} + ) + + # Increment new status count + self.job_status_gauge.add( + 1, + attributes={"status": new_status} + ) + + def record_job_processing_time(self, stage: str, duration_seconds: float, job_id: str): + """Record time taken for job processing stage""" + self.job_processing_duration.record( + duration_seconds, + attributes={ + "stage": stage, + "job_id": job_id + } + ) + + # AI service metrics methods + def record_ai_request(self, service: str, operation: str, language: Optional[str] = None): + """Record AI service request""" + attributes = { + "service": service, + "operation": operation + } + if language: + attributes["language"] = language + + self.ai_requests_counter.add(1, attributes=attributes) + + def record_ai_request_duration(self, service: str, operation: str, duration_seconds: float): + """Record AI request duration""" + self.ai_request_duration.record( + duration_seconds, + attributes={ + "service": service, + "operation": operation + } + ) + + def record_ai_confidence(self, confidence: float, service: str): + """Record AI confidence score""" + self.ai_confidence_histogram.record( + confidence, + attributes={"service": service} + ) + + # Storage metrics methods + def record_storage_operation(self, operation: str, file_type: str, success: bool): + """Record storage operation""" + self.storage_operations_counter.add( + 1, + attributes={ + "operation": operation, + "file_type": file_type, + "result": "success" if success else "error" + } + ) + + def record_storage_duration(self, operation: str, duration_seconds: float): + """Record storage operation duration""" + self.storage_operation_duration.record( + duration_seconds, + attributes={"operation": operation} + ) + + # Queue metrics methods + def record_celery_task(self, task_name: str, queue: str, result: str): + """Record Celery task execution""" + self.queue_tasks_counter.add( + 1, + attributes={ + "task_name": task_name, + "queue": queue, + "result": result + } + ) + + def record_celery_task_duration(self, task_name: str, duration_seconds: float): + """Record Celery task duration""" + self.queue_task_duration.record( + duration_seconds, + attributes={"task_name": task_name} + ) + + # Auth metrics methods + def record_auth_attempt(self, result: str, user_role: Optional[str] = None): + """Record authentication attempt""" + attributes = {"result": result} + if user_role: + attributes["user_role"] = user_role + + self.auth_attempts_counter.add(1, attributes=attributes) + + def update_active_users(self, count_change: int, user_role: str): + """Update active users count""" + self.active_users_gauge.add( + count_change, + attributes={"user_role": user_role} + ) + + +# Global metrics instance +app_metrics = ApplicationMetrics() + + +class MetricsTimer: + """Context manager for timing operations""" + + def __init__(self, metric_recorder, *args, **kwargs): + self.metric_recorder = metric_recorder + self.args = args + self.kwargs = kwargs + self.start_time = None + + def __enter__(self): + self.start_time = time.time() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.start_time: + duration = time.time() - self.start_time + self.metric_recorder(duration, *self.args, **self.kwargs) + + +# Convenience functions for common metrics patterns +def time_job_processing(stage: str, job_id: str): + """Time a job processing stage""" + return MetricsTimer( + app_metrics.record_job_processing_time, + stage, job_id + ) + + +def time_ai_request(service: str, operation: str): + """Time an AI service request""" + return MetricsTimer( + app_metrics.record_ai_request_duration, + service, operation + ) + + +def time_storage_operation(operation: str): + """Time a storage operation""" + return MetricsTimer( + app_metrics.record_storage_duration, + operation + ) + + +def time_celery_task(task_name: str): + """Time a Celery task execution""" + return MetricsTimer( + app_metrics.record_celery_task_duration, + task_name + ) + + +def track_rate_limit_metrics(identifier: str, is_allowed: bool, current_requests: int, limit: int): + """Track rate limiting metrics""" + if hasattr(app_metrics, 'rate_limit_counter'): + app_metrics.rate_limit_counter.add( + 1, + attributes={ + "identifier_type": identifier.split(":")[0] if ":" in identifier else "unknown", + "is_allowed": str(is_allowed), + "status": "allowed" if is_allowed else "blocked" + } + ) + + +def track_validation_metrics(endpoint: str, method: str, is_valid: bool, validation_time: float, error_types: list): + """Track request validation metrics""" + if hasattr(app_metrics, 'validation_counter'): + app_metrics.validation_counter.add( + 1, + attributes={ + "endpoint": endpoint, + "method": method, + "is_valid": str(is_valid), + "error_types": ",".join(error_types) if error_types else "none" + } + ) + + if hasattr(app_metrics, 'validation_duration'): + app_metrics.validation_duration.record( + validation_time, + attributes={ + "endpoint": endpoint, + "method": method + } + ) \ No newline at end of file diff --git a/backend/app/telemetry/tracing.py b/backend/app/telemetry/tracing.py new file mode 100644 index 0000000..78a911c --- /dev/null +++ b/backend/app/telemetry/tracing.py @@ -0,0 +1,268 @@ +import logging +from typing import Optional + +from opentelemetry import trace +# from opentelemetry.exporter.gcp.trace import CloudTraceSpanExporter # Disabled for local dev +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.instrumentation.pymongo import PymongoInstrumentor +from opentelemetry.instrumentation.redis import RedisInstrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +from ..core.config import settings + +logger = logging.getLogger(__name__) + + +def setup_tracing(app_name: str = "accessible-video-api", service_version: str = "1.0.0"): + """Initialize OpenTelemetry tracing for the application""" + + # Create resource with service information + resource = Resource.create({ + "service.name": app_name, + "service.version": service_version, + "service.namespace": "accessible-video", + "deployment.environment": settings.app_env, + }) + + # Set up tracer provider + tracer_provider = TracerProvider(resource=resource) + trace.set_tracer_provider(tracer_provider) + + # Configure span processor and exporter based on environment + if settings.app_env == "prod" and settings.gcp_project_id: + # Use Google Cloud Trace in production (disabled for local dev) + # cloud_trace_exporter = CloudTraceSpanExporter( + # project_id=settings.gcp_project_id + # ) + # span_processor = BatchSpanProcessor(cloud_trace_exporter) + # tracer_provider.add_span_processor(span_processor) + logger.info("Google Cloud Trace disabled for local dev") + + elif settings.otel_exporter_otlp_endpoint: + # Use OTLP exporter for other observability platforms + from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter + + otlp_exporter = OTLPSpanExporter( + endpoint=settings.otel_exporter_otlp_endpoint, + headers={}, + ) + span_processor = BatchSpanProcessor(otlp_exporter) + tracer_provider.add_span_processor(span_processor) + logger.info(f"Configured OTLP trace exporter: {settings.otel_exporter_otlp_endpoint}") + + else: + # Development mode - use console exporter + from opentelemetry.sdk.trace.export import ConsoleSpanExporter + + console_exporter = ConsoleSpanExporter() + span_processor = BatchSpanProcessor(console_exporter) + tracer_provider.add_span_processor(span_processor) + logger.info("Configured console trace exporter for development") + + logger.info(f"OpenTelemetry tracing initialized for {app_name}") + + return tracer_provider + + +def instrument_fastapi_app(app): + """Instrument FastAPI application with automatic tracing""" + FastAPIInstrumentor.instrument_app( + app, + tracer_provider=trace.get_tracer_provider(), + excluded_urls="health,metrics", # Don't trace health checks + ) + logger.info("FastAPI instrumentation enabled") + + +def instrument_dependencies(): + """Instrument external dependencies for automatic tracing""" + # Instrument MongoDB + PymongoInstrumentor().instrument( + tracer_provider=trace.get_tracer_provider() + ) + logger.info("MongoDB instrumentation enabled") + + # Instrument Redis + RedisInstrumentor().instrument( + tracer_provider=trace.get_tracer_provider() + ) + logger.info("Redis instrumentation enabled") + + +def get_tracer(name: str = "accessible-video"): + """Get a tracer instance for manual instrumentation""" + return trace.get_tracer(name) + + +def trace_async_operation(operation_name: str, **attributes): + """Decorator for tracing async operations""" + def decorator(func): + async def wrapper(*args, **kwargs): + tracer = get_tracer() + + with tracer.start_as_current_span( + operation_name, + attributes=attributes + ) as span: + try: + result = await func(*args, **kwargs) + span.set_attribute("operation.result", "success") + return result + except Exception as e: + span.set_attribute("operation.result", "error") + span.set_attribute("operation.error_message", str(e)) + span.record_exception(e) + raise + + return wrapper + return decorator + + +def trace_job_pipeline(job_id: str, pipeline_stage: str): + """Decorator for tracing job pipeline stages""" + def decorator(func): + async def wrapper(*args, **kwargs): + tracer = get_tracer() + + with tracer.start_as_current_span( + f"job_pipeline.{pipeline_stage}", + attributes={ + "job.id": job_id, + "job.pipeline_stage": pipeline_stage, + } + ) as span: + try: + result = await func(*args, **kwargs) + span.set_attribute("job.result", "success") + return result + except Exception as e: + span.set_attribute("job.result", "error") + span.set_attribute("job.error_message", str(e)) + span.record_exception(e) + raise + + return wrapper + return decorator + + +def trace_ai_operation(operation_type: str, language: Optional[str] = None): + """Decorator for tracing AI service operations""" + def decorator(func): + async def wrapper(*args, **kwargs): + tracer = get_tracer() + + span_attributes = { + "ai.operation_type": operation_type, + "ai.provider": "gemini" if "gemini" in operation_type else "google_translate" + } + + if language: + span_attributes["ai.language"] = language + + with tracer.start_as_current_span( + f"ai.{operation_type}", + attributes=span_attributes + ) as span: + try: + result = await func(*args, **kwargs) + + # Add result attributes if available + if isinstance(result, dict): + if "confidence" in result: + span.set_attribute("ai.confidence", result["confidence"]) + if "language" in result: + span.set_attribute("ai.detected_language", result["language"]) + + span.set_attribute("ai.result", "success") + return result + except Exception as e: + span.set_attribute("ai.result", "error") + span.set_attribute("ai.error_message", str(e)) + span.record_exception(e) + raise + + return wrapper + return decorator + + +def trace_storage_operation(operation_type: str, file_path: str): + """Decorator for tracing storage operations""" + def decorator(func): + async def wrapper(*args, **kwargs): + tracer = get_tracer() + + with tracer.start_as_current_span( + f"storage.{operation_type}", + attributes={ + "storage.operation": operation_type, + "storage.path": file_path, + "storage.provider": "gcs" + } + ) as span: + try: + result = await func(*args, **kwargs) + span.set_attribute("storage.result", "success") + + if isinstance(result, str) and result.startswith("gs://"): + span.set_attribute("storage.result_uri", result) + + return result + except Exception as e: + span.set_attribute("storage.result", "error") + span.set_attribute("storage.error_message", str(e)) + span.record_exception(e) + raise + + return wrapper + return decorator + + +class TracingContext: + """Context manager for manual span creation with attributes""" + + def __init__(self, span_name: str, attributes: Optional[dict] = None): + self.span_name = span_name + self.attributes = attributes or {} + self.tracer = get_tracer() + self.span = None + + def __enter__(self): + self.span = self.tracer.start_span(self.span_name, attributes=self.attributes) + return self.span + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type: + self.span.set_attribute("error", True) + self.span.set_attribute("error_message", str(exc_val)) + self.span.record_exception(exc_val) + + self.span.end() + + +# Convenience functions for common tracing patterns +def trace_api_request(endpoint: str, user_id: Optional[str] = None): + """Create span for API request with common attributes""" + attributes = { + "http.route": endpoint, + "component": "api" + } + + if user_id: + attributes["user.id"] = user_id + + return TracingContext(f"api.{endpoint.replace('/', '_')}", attributes) + + +def trace_celery_task(task_name: str, job_id: Optional[str] = None): + """Create span for Celery task execution""" + attributes = { + "celery.task_name": task_name, + "component": "worker" + } + + if job_id: + attributes["job.id"] = job_id + + return TracingContext(f"celery.{task_name}", attributes) \ No newline at end of file diff --git a/backend/celery_worker.py b/backend/celery_worker.py new file mode 100644 index 0000000..057d75c --- /dev/null +++ b/backend/celery_worker.py @@ -0,0 +1,42 @@ +import sentry_sdk +from sentry_sdk.integrations.celery import CeleryIntegration + +from app.core.config import settings +from app.core.logging import setup_logging, get_logger +from app.tasks import celery_app + +# Set up logging first +setup_logging() +logger = get_logger(__name__) + +# Initialize Sentry for worker +if settings.sentry_dsn and settings.sentry_dsn.startswith(('http', 'https')): + sentry_sdk.init( + dsn=settings.sentry_dsn, + integrations=[CeleryIntegration(monitor_beat_tasks=True)], + environment=settings.app_env, + release="1.0.0", + send_default_pii=False, + ) + +logger.info("Starting Celery worker with structured logging") + +# Import task modules to register them +from app.tasks import ingest_and_ai +from app.tasks import translate_and_synthesize + +# Debug: Show registered tasks +logger.info(f"Celery app: {celery_app}") +logger.info(f"Registered tasks: {list(celery_app.tasks.keys())}") +logger.info(f"Task routes: {celery_app.conf.task_routes}") +logger.info(f"Worker listening to queues: default,ingest") + +# Specifically check for our translation task +if 'app.tasks.translate_and_synthesize.translate_and_synthesize_task' in celery_app.tasks: + logger.info("✅ translate_and_synthesize_task is registered") +else: + logger.error("❌ translate_and_synthesize_task is NOT registered") + logger.error(f"Available tasks: {[t for t in celery_app.tasks.keys() if not t.startswith('celery.')]}") + +if __name__ == "__main__": + celery_app.start() diff --git a/backend/cors-config.json b/backend/cors-config.json new file mode 100644 index 0000000..78a52e4 --- /dev/null +++ b/backend/cors-config.json @@ -0,0 +1,8 @@ +[ + { + "origin": ["*"], + "method": ["GET", "HEAD", "OPTIONS"], + "responseHeader": ["*"], + "maxAgeSeconds": 3600 + } +] \ No newline at end of file diff --git a/backend/create_test_users.py b/backend/create_test_users.py new file mode 100644 index 0000000..3643712 --- /dev/null +++ b/backend/create_test_users.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Create test users for the accessible video platform.""" + +import asyncio +from datetime import datetime +from passlib.context import CryptContext +from motor.motor_asyncio import AsyncIOMotorClient + +from app.core.config import settings +from app.models.user import UserRole + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +async def create_test_users(): + """Create test users in the database.""" + print("Connecting to MongoDB...") + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + # Test connection + await client.admin.command('ping') + print("Connected to MongoDB successfully") + + users_collection = db.users + + # Check if users already exist + existing_admin = await users_collection.find_one({"email": "admin@example.com"}) + existing_reviewer = await users_collection.find_one({"email": "reviewer@example.com"}) + + test_users = [ + { + "email": "admin@example.com", + "hashed_password": pwd_context.hash("admin"), + "full_name": "Admin User", + "role": UserRole.ADMIN.value, + "is_active": True, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + }, + { + "email": "reviewer@example.com", + "hashed_password": pwd_context.hash("reviewer"), + "full_name": "Reviewer User", + "role": UserRole.REVIEWER.value, + "is_active": True, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + }, + { + "email": "client@example.com", + "hashed_password": pwd_context.hash("client123"), + "full_name": "Client User", + "role": UserRole.CLIENT.value, + "is_active": True, + "created_at": datetime.utcnow(), + "updated_at": datetime.utcnow(), + } + ] + + for user in test_users: + existing = await users_collection.find_one({"email": user["email"]}) + if existing: + print(f"User {user['email']} already exists, skipping...") + continue + + result = await users_collection.insert_one(user) + print(f"Created user: {user['email']} (ID: {result.inserted_id})") + + # Show all users + print("\nAll users in database:") + async for user in users_collection.find({}, {"email": 1, "role": 1, "is_active": 1}): + print(f" {user['email']} - {user['role']} - Active: {user['is_active']}") + + client.close() + print("Done!") + +if __name__ == "__main__": + asyncio.run(create_test_users()) \ No newline at end of file diff --git a/backend/debug_login.py b/backend/debug_login.py new file mode 100644 index 0000000..1797abf --- /dev/null +++ b/backend/debug_login.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Debug login issues by testing components individually.""" + +import asyncio +from motor.motor_asyncio import AsyncIOMotorClient +from app.core.config import settings +from app.core.security import verify_password +from app.models.user import User + +async def test_database_connection(): + """Test direct database connection.""" + print("1. Testing database connection...") + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + # Test connection + await client.admin.command('ping') + print("✅ Database connection successful") + + # Check if users collection exists + collections = await db.list_collection_names() + print(f"✅ Collections: {collections}") + + # Count users + user_count = await db.users.count_documents({}) + print(f"✅ User count: {user_count}") + + # Find admin user + user_doc = await db.users.find_one({"email": "admin@example.com"}) + if user_doc: + print(f"✅ Found admin user: {user_doc['email']}") + user = User(**user_doc) + print(f"✅ User model validation successful") + + # Test password verification + print("2. Testing password verification...") + password_correct = verify_password("admin", user.hashed_password) + print(f"✅ Password verification result: {password_correct}") + + else: + print("❌ Admin user not found") + + except Exception as e: + print(f"❌ Database error: {e}") + import traceback + traceback.print_exc() + finally: + client.close() + +if __name__ == "__main__": + asyncio.run(test_database_connection()) \ No newline at end of file diff --git a/backend/gunicorn_conf.py b/backend/gunicorn_conf.py new file mode 100644 index 0000000..b0e29ee --- /dev/null +++ b/backend/gunicorn_conf.py @@ -0,0 +1,29 @@ +import multiprocessing +import os + +# Server socket +bind = f"0.0.0.0:{os.getenv('PORT', '8000')}" +backlog = 2048 + +# Worker processes +workers = multiprocessing.cpu_count() * 2 + 1 +worker_class = "uvicorn.workers.UvicornWorker" +worker_connections = 1000 +max_requests = 1000 +max_requests_jitter = 50 + +# Timeouts +timeout = 120 +keepalive = 2 + +# Logging +loglevel = "info" +access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s' +accesslog = "-" +errorlog = "-" + +# Process naming +proc_name = "accessible-video-api" + +# Application +module = "app.main:app" diff --git a/backend/migrate.py b/backend/migrate.py new file mode 100755 index 0000000..66f8ab6 --- /dev/null +++ b/backend/migrate.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Database migration CLI for the Accessible Video Platform. + +Usage: + python migrate.py status # Show migration status + python migrate.py up # Apply all pending migrations + python migrate.py up # Apply migrations up to version + python migrate.py down # Rollback to version + python migrate.py create # Create new migration template +""" + +import asyncio +import sys +from datetime import datetime +from pathlib import Path +from typing import Optional + +from app.migrations.migrator import MigrationManager +from app.core.config import get_settings +from app.core.database import connect_to_mongo, close_mongo_connection + + +class MigrationCLI: + """Command-line interface for database migrations.""" + + def __init__(self): + self.manager = MigrationManager() + + async def initialize(self): + """Initialize database connection and migration manager""" + await connect_to_mongo() + await self.manager.initialize() + + async def status(self) -> None: + """Show current migration status.""" + status = await self.manager.get_migration_status() + + print("🗃️ Database Migration Status") + print("=" * 40) + print(f"Total migrations: {status['total_migrations']}") + print(f"Applied migrations: {status['applied_migrations']}") + print(f"Pending migrations: {status['pending_migrations']}") + print(f"Latest applied: {status['latest_applied'] or 'None'}") + + if status['pending_migrations'] > 0: + print(f"⚠️ {status['pending_migrations']} migrations pending") + else: + print("✅ Database is up to date") + + print("\nApplied migrations:") + for version in status['all_applied']: + print(f" ✅ {version}") + + async def migrate_up(self, target_version: Optional[str] = None) -> None: + """Apply migrations.""" + print(f"🚀 Applying migrations{f' up to {target_version}' if target_version else ''}...") + + try: + applied = await self.manager.migrate_up(target_version) + + if applied: + print(f"✅ Successfully applied {len(applied)} migrations:") + for version in applied: + print(f" ✅ {version}") + else: + print("ℹ️ No migrations to apply") + + except Exception as e: + print(f"❌ Migration failed: {e}") + sys.exit(1) + + async def migrate_down(self, target_version: str) -> None: + """Rollback migrations.""" + print(f"⚠️ Rolling back migrations to {target_version}...") + print("⚠️ This operation may be destructive!") + + # Confirmation prompt + response = input("Are you sure you want to proceed? (y/N): ") + if response.lower() != 'y': + print("❌ Rollback cancelled") + return + + try: + rolled_back = await self.manager.migrate_down(target_version) + + if rolled_back: + print(f"✅ Successfully rolled back {len(rolled_back)} migrations:") + for version in rolled_back: + print(f" ⬇️ {version}") + else: + print("ℹ️ No migrations to rollback") + + except Exception as e: + print(f"❌ Rollback failed: {e}") + sys.exit(1) + + def create_migration(self, description: str) -> None: + """Create a new migration template.""" + # Generate version from current timestamp + now = datetime.utcnow() + version = now.strftime("%Y-%m-%d-%H%M%S") + + # Sanitize description for filename + safe_description = "".join(c if c.isalnum() or c in "-_" else "_" for c in description.lower()) + + filename = f"migration_{version}_{safe_description}.py" + migrations_dir = Path(__file__).parent / "app" / "migrations" / "scripts" + migrations_dir.mkdir(parents=True, exist_ok=True) + + filepath = migrations_dir / filename + + # Create migration template + template = f'''"""Migration: {description}.""" + +from app.migrations.migrator import Migration + + +class Migration(Migration): + """{description}.""" + + def __init__(self): + super().__init__() + self.version = "{version}" + self.description = "{description}" + + async def up(self) -> None: + """Apply the migration.""" + + # TODO: Implement your migration logic here + # Example: + # await self.db.collection_name.create_index([("field", 1)]) + # await self.db.collection_name.update_many( + # {{"old_field": {{"$exists": True}}}}, + # {{"$rename": {{"old_field": "new_field"}}}} + # ) + + print(f"✅ Applied migration {{self.version}}: {{self.description}}") + + async def down(self) -> None: + """Rollback the migration.""" + + # TODO: Implement your rollback logic here + # Example: + # await self.db.collection_name.drop_index("index_name") + # await self.db.collection_name.update_many( + # {{"new_field": {{"$exists": True}}}}, + # {{"$rename": {{"new_field": "old_field"}}}} + # ) + + print(f"⚠️ Rolled back migration {{self.version}}: {{self.description}}") +''' + + filepath.write_text(template) + print(f"✅ Created migration template: {filepath}") + print(f"📝 Edit the file to implement your migration logic") + + +async def main(): + """Main CLI entry point.""" + + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + + command = sys.argv[1] + cli = MigrationCLI() + + try: + # Initialize database connection for all commands except create + if command != "create": + await cli.initialize() + + if command == "status": + await cli.status() + + elif command == "up": + target = sys.argv[2] if len(sys.argv) > 2 else None + await cli.migrate_up(target) + + elif command == "down": + if len(sys.argv) < 3: + print("❌ Target version required for rollback") + sys.exit(1) + target = sys.argv[2] + await cli.migrate_down(target) + + elif command == "create": + if len(sys.argv) < 3: + print("❌ Description required for new migration") + sys.exit(1) + description = " ".join(sys.argv[2:]) + cli.create_migration(description) + + else: + print(f"❌ Unknown command: {command}") + print(__doc__) + sys.exit(1) + + finally: + # Close database connection + await close_mongo_connection() + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/backend/optical-414516-80e2475f6412.json b/backend/optical-414516-80e2475f6412.json new file mode 100644 index 0000000..cf2c07a --- /dev/null +++ b/backend/optical-414516-80e2475f6412.json @@ -0,0 +1,13 @@ +{ + "type": "service_account", + "project_id": "optical-414516", + "private_key_id": "80e2475f641260d5c28e29d10574cef0ba5bff01", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDBPenCel/D+oNr\nf3OZTHsb4GYmqIZpzKLHYsj6/578Oayng0SR8zgAqV1JZSAud3bMFH7tT32Pa6qE\ntB1PNslhgtlYAGa5z9iXDSHksOZ6dAgk2YilZ7deAteGvoeNwkALrxR0FW9Uj0q0\nc1oszSekmpSwzy5QPuQOmt9D1xH+tbX5/zUXxkQmNSKzyPtE/0B5FxdeyoVgK4ZT\nHca6IonDXdW58c9iNdCqboShlb6VZP9zMRMykEuvD9fKMzQUGmjhqI3oGf/B11s9\n+PrtImb9uSrohUVerc/1PjDwA+y/uWet3PGxobFU05GPIbz2sj/nm6Vo1+XDIVgw\nFXSahTdhAgMBAAECggEAC+VTBC6iwcTxXVpVmqF9D25BwfqsRTJC79TcKN3R9haN\nOZKr7SaOOZwzd4n+I5FYtgXc+m1JfkOGfImjjdwCWAcrq6GUSupjAiMQ0kWbKpae\nzOxUErqbxlgucS3X2MyVQyLead1kvE15FjqzpmZkT/Tw8LsQT5uCtoam9kPBgjum\noO0tR6MChkI07LUQ2XXINLLWVbhWLBImksiW9ehcR/htsNMrszSFem6hLe+7PgRq\nxFocz1jt7G/x+csLgyI4cZN1jDv3xd+quxgSgdBZEeOvTWfuTWM+rbMavWzqD2rn\nBpPI1+N1bwNUf0XbKtG6e7WYFUPGGbQjJmLjimAnMQKBgQD7Pdr2fTgek94mvzzb\nnd1Ksri9waf3YJKYchDe5HHtTq+y7IdgFWbmL8ybjKz5TzzHCLt1clg85Fptb14g\noAZxJcS7N1P0uWgHgIWNfm8oFEVmEu2fHfeYjlCPEuroRk6BT9gR7bLwt0mM0mIO\nJJcBbXZyDt4qok/i5r4yeVY2swKBgQDE5tiRjOGq8r5w7q9OMee671g33xw3UcNN\nGlBcbqHnNZZF8+P62ampuHSadsYtOmbQDFbHo7taV7ZhDmtavUU8LQw8TERxj0xQ\nD+p3uCBBQeKPg6h4e2XNjRc6+7riShiCEPwg92M4qpZvlNoGzTogiXiRPBW97Y6z\nacA3Y5oDmwKBgQDodvhF/+DQMiNoGKSX1D6wYiObuDbRJrMdiNVhV2CuoZLibAZq\negMG041vE7/swktLIiJJbm6EkQm2nkgqycaMJNUeIPh2xKKj5mAsZqM1I2R/KN5i\nztiMeInDiE6AcqUq8xTKqfRa1EyilvsRePub34urx2P7cMmX+cZcb3a9DwKBgQCO\nWBxkTKavwMDwP304WFegCnuKGJ77Vv6LdOR3jfs5fMHgXEqKBGTlL1YMfKUT+U5u\nRR1PQgylaReN3rC5bm7o6+AWj0RDnEac8oSce93Fj23MNm/KedrE2KTcnTMjeFFz\nZff/lRiD1L7gd4mOtTq6XudshzVokp5BEchFwpmK1QKBgQC4yrXV4IxIHgCm3mfN\n/rz5iIt6fOGmp07Uv4ZtFcEBQKrWatWMfAAX/lbOGrje9HFNpl5FlYZe/k4ow3O+\ncxXpQOsu9TZdmDJ0YVH6o/+TAPaF/OrMJ8BqrO4J8fJiD0F+y3Ii3pxr9NrH9hjK\n63QAJ9PaA93UVVEbkh98yIOGJA==\n-----END PRIVATE KEY-----\n", + "client_email": "video-accessibility@optical-414516.iam.gserviceaccount.com", + "client_id": "115091905183525974710", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/video-accessibility%40optical-414516.iam.gserviceaccount.com", + "universe_domain": "googleapis.com" +} diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 0000000..75fee57 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,3980 @@ +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.12.15" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, + {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, + {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, + {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, + {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, + {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, + {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, + {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, + {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, + {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, + {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, + {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, + {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, + {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, + {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, + {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, + {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, + {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, + {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, + {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, + {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, + {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, + {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, + {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, + {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, + {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, + {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "brotlicffi ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "amqp" +version = "5.3.1" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.10.0" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, + {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "asgiref" +version = "3.9.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c"}, + {file = "asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142"}, +] + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.11\" and python_full_version < \"3.11.3\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +description = "LTS Port of Python audioop" +optional = false +python-versions = ">=3.13" +groups = ["main"] +markers = "python_version >= \"3.13\"" +files = [ + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"}, + {file = "audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc"}, + {file = "audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7"}, + {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e"}, + {file = "audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547"}, + {file = "audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b"}, + {file = "audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd"}, + {file = "audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0"}, +] + +[[package]] +name = "backoff" +version = "2.2.1" +description = "Function decoration for backoff and retry" +optional = false +python-versions = ">=3.7,<4.0" +groups = ["main"] +files = [ + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, +] + +[[package]] +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "billiard" +version = "4.2.1" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + +[[package]] +name = "black" +version = "23.12.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4) ; sys_platform != \"win32\" or implementation_name != \"pypy\"", "aiohttp (>=3.7.4,!=3.9.0) ; sys_platform == \"win32\" and implementation_name == \"pypy\""] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "cachetools" +version = "5.5.2" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, + {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, +] + +[[package]] +name = "celery" +version = "5.5.3" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525"}, + {file = "celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5"}, +] + +[package.dependencies] +billiard = ">=4.2.1,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = [ + {version = ">=5.5.2,<5.6"}, + {version = "*", extras = ["redis"], optional = true, markers = "extra == \"redis\""}, +] +python-dateutil = ">=2.8.2" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==44.0.2)"] +azureblockblob = ["azure-identity (>=1.19.0)", "azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0) ; platform_python_implementation == \"CPython\"", "brotlipy (>=0.7.0) ; platform_python_implementation == \"PyPy\""] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0) ; platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")"] +couchdb = ["pycouchdb (==1.16.0)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.17.1)", "elasticsearch (<=8.17.2)"] +eventlet = ["eventlet (>=0.32.0) ; python_version < \"3.10\""] +gcs = ["google-cloud-firestore (==2.20.1)", "google-cloud-storage (>=2.10.0)", "grpcio (==1.67.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +memcache = ["pylibmc (==1.6.3) ; platform_system != \"Windows\""] +mongodb = ["kombu[mongodb]"] +msgpack = ["kombu[msgpack]"] +pydantic = ["pydantic (>=2.4)"] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82) ; python_version < \"3.11\""] +pytest = ["pytest-celery[all] (>=1.2.0,<1.3.0)"] +redis = ["kombu[redis]"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +solar = ["ephem (==4.2) ; platform_python_implementation != \"PyPy\""] +sqlalchemy = ["kombu[sqlalchemy]"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.5.0)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0) ; python_version < \"3.8.0\"", "tblib (>=1.5.0) ; python_version >= \"3.8.0\""] +yaml = ["kombu[yaml]"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.23.0)"] + +[[package]] +name = "certifi" +version = "2025.8.3" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, + {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, + {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, + {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, + {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, + {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, + {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, + {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, + {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, + {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, + {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, +] + +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "click-didyoumean" +version = "0.3.1" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2" +groups = ["main"] +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1.2" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6"}, + {file = "click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "45.0.6" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +files = [ + {file = "cryptography-45.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:048e7ad9e08cf4c0ab07ff7f36cc3115924e22e2266e034450a890d9e312dd74"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:44647c5d796f5fc042bbc6d61307d04bf29bccb74d188f18051b635f20a9c75f"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e40b80ecf35ec265c452eea0ba94c9587ca763e739b8e559c128d23bff7ebbbf"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:00e8724bdad672d75e6f069b27970883179bd472cd24a63f6e620ca7e41cc0c5"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a3085d1b319d35296176af31c90338eeb2ddac8104661df79f80e1d9787b8b2"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1b7fa6a1c1188c7ee32e47590d16a5a0646270921f8020efc9a511648e1b2e08"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:275ba5cc0d9e320cd70f8e7b96d9e59903c815ca579ab96c1e37278d231fc402"}, + {file = "cryptography-45.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f4028f29a9f38a2025abedb2e409973709c660d44319c61762202206ed577c42"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ee411a1b977f40bd075392c80c10b58025ee5c6b47a822a33c1198598a7a5f05"}, + {file = "cryptography-45.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e2a21a8eda2d86bb604934b6b37691585bd095c1f788530c1fcefc53a82b3453"}, + {file = "cryptography-45.0.6-cp311-abi3-win32.whl", hash = "sha256:d063341378d7ee9c91f9d23b431a3502fc8bfacd54ef0a27baa72a0843b29159"}, + {file = "cryptography-45.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:833dc32dfc1e39b7376a87b9a6a4288a10aae234631268486558920029b086ec"}, + {file = "cryptography-45.0.6-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:3436128a60a5e5490603ab2adbabc8763613f638513ffa7d311c900a8349a2a0"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d9ef57b6768d9fa58e92f4947cea96ade1233c0e236db22ba44748ffedca394"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea3c42f2016a5bbf71825537c2ad753f2870191134933196bee408aac397b3d9"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:20ae4906a13716139d6d762ceb3e0e7e110f7955f3bc3876e3a07f5daadec5f3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dac5ec199038b8e131365e2324c03d20e97fe214af051d20c49db129844e8b3"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:18f878a34b90d688982e43f4b700408b478102dd58b3e39de21b5ebf6509c301"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5bd6020c80c5b2b2242d6c48487d7b85700f5e0038e67b29d706f98440d66eb5"}, + {file = "cryptography-45.0.6-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:eccddbd986e43014263eda489abbddfbc287af5cddfd690477993dbb31e31016"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:550ae02148206beb722cfe4ef0933f9352bab26b087af00e48fdfb9ade35c5b3"}, + {file = "cryptography-45.0.6-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5b64e668fc3528e77efa51ca70fadcd6610e8ab231e3e06ae2bab3b31c2b8ed9"}, + {file = "cryptography-45.0.6-cp37-abi3-win32.whl", hash = "sha256:780c40fb751c7d2b0c6786ceee6b6f871e86e8718a8ff4bc35073ac353c7cd02"}, + {file = "cryptography-45.0.6-cp37-abi3-win_amd64.whl", hash = "sha256:20d15aed3ee522faac1a39fbfdfee25d17b1284bafd808e1640a74846d7c4d1b"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:705bb7c7ecc3d79a50f236adda12ca331c8e7ecfbea51edd931ce5a7a7c4f012"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:826b46dae41a1155a0c0e66fafba43d0ede1dc16570b95e40c4d83bfcf0a451d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cc4d66f5dc4dc37b89cfef1bd5044387f7a1f6f0abb490815628501909332d5d"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f68f833a9d445cc49f01097d95c83a850795921b3f7cc6488731e69bde3288da"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3b5bf5267e98661b9b888a9250d05b063220dfa917a8203744454573c7eb79db"}, + {file = "cryptography-45.0.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2384f2ab18d9be88a6e4f8972923405e2dbb8d3e16c6b43f15ca491d7831bd18"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fc022c1fa5acff6def2fc6d7819bbbd31ccddfe67d075331a65d9cfb28a20983"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3de77e4df42ac8d4e4d6cdb342d989803ad37707cf8f3fbf7b088c9cbdd46427"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:599c8d7df950aa68baa7e98f7b73f4f414c9f02d0e8104a30c0182a07732638b"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:31a2b9a10530a1cb04ffd6aa1cd4d3be9ed49f7d77a4dafe198f3b382f41545c"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e5b3dda1b00fb41da3af4c5ef3f922a200e33ee5ba0f0bc9ecf0b0c173958385"}, + {file = "cryptography-45.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:629127cfdcdc6806dfe234734d7cb8ac54edaf572148274fa377a7d3405b0043"}, + {file = "cryptography-45.0.6.tar.gz", hash = "sha256:5c966c732cf6e4a276ce83b6e4c729edda2df6929083a952cc7da973c539c719"}, +] + +[package.dependencies] +cffi = {version = ">=1.14", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs ; python_full_version >= \"3.8.0\"", "sphinx-rtd-theme (>=3.0.0) ; python_full_version >= \"3.8.0\""] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_full_version >= \"3.8.0\""] +pep8test = ["check-sdist ; python_full_version >= \"3.8.0\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==45.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "deprecated" +version = "1.2.18" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +groups = ["main"] +files = [ + {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, + {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] + +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "dnspython" +version = "2.7.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, + {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, +] + +[package.extras] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=43)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] + +[[package]] +name = "ecdsa" +version = "0.19.1" +description = "ECDSA cryptographic signature library (pure python)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" +groups = ["main"] +files = [ + {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, + {file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"}, +] + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "email-validator" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.115.14" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, + {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "ffmpeg-python" +version = "0.2.0" +description = "Python bindings for FFmpeg - with complex filtering support" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127"}, + {file = "ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5"}, +] + +[package.dependencies] +future = "*" + +[package.extras] +dev = ["Sphinx (==2.1.0)", "future (==0.17.1)", "numpy (==1.16.4)", "pytest (==4.6.1)", "pytest-mock (==1.10.4)", "tox (==3.12.1)"] + +[[package]] +name = "filelock" +version = "3.19.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d"}, + {file = "filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58"}, +] + +[[package]] +name = "frozenlist" +version = "1.7.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, + {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, + {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, + {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, + {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, + {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, + {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, + {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, + {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, + {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, + {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, + {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, + {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, + {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, + {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, + {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, + {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, + {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, + {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, + {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, + {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, + {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, + {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, + {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, + {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, + {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, + {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, + {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, + {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, + {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, + {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, + {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, +] + +[[package]] +name = "future" +version = "1.0.0" +description = "Clean single-source support for Python 3 and 2" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] +files = [ + {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, + {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, +] + +[[package]] +name = "google-api-core" +version = "2.25.1" +description = "Google API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7"}, + {file = "google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8"}, +] + +[package.dependencies] +google-auth = ">=2.14.1,<3.0.0" +googleapis-common-protos = ">=1.56.2,<2.0.0" +grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, +] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" +requests = ">=2.18.0,<3.0.0" + +[package.extras] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\""] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] + +[[package]] +name = "google-auth" +version = "2.40.3" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca"}, + {file = "google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] + +[[package]] +name = "google-cloud-core" +version = "2.4.3" +description = "Google Cloud API client core library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_core-2.4.3-py2.py3-none-any.whl", hash = "sha256:5130f9f4c14b4fafdff75c79448f9495cfade0d8775facf1b09c3bf67e027f6e"}, + {file = "google_cloud_core-2.4.3.tar.gz", hash = "sha256:1fab62d7102844b278fe6dead3af32408b1df3eb06f5c7e8634cbd40edc4da53"}, +] + +[package.dependencies] +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0dev" + +[package.extras] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] + +[[package]] +name = "google-cloud-secret-manager" +version = "2.24.0" +description = "Google Cloud Secret Manager API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_secret_manager-2.24.0-py3-none-any.whl", hash = "sha256:9bea1254827ecc14874bc86c63b899489f8f50bfe1442bfb2517530b30b3a89b"}, + {file = "google_cloud_secret_manager-2.24.0.tar.gz", hash = "sha256:ce573d40ffc2fb7d01719243a94ee17aa243ea642a6ae6c337501e58fbf642b5"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +grpc-google-iam-v1 = ">=0.14.0,<1.0.0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "google-cloud-storage" +version = "2.19.0" +description = "Google Cloud Storage API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_storage-2.19.0-py2.py3-none-any.whl", hash = "sha256:aeb971b5c29cf8ab98445082cbfe7b161a1f48ed275822f59ed3f1524ea54fba"}, + {file = "google_cloud_storage-2.19.0.tar.gz", hash = "sha256:cd05e9e7191ba6cb68934d8eb76054d9be4562aa89dbc4236feee4d7d51342b2"}, +] + +[package.dependencies] +google-api-core = ">=2.15.0,<3.0.0dev" +google-auth = ">=2.26.1,<3.0dev" +google-cloud-core = ">=2.3.0,<3.0dev" +google-crc32c = ">=1.0,<2.0dev" +google-resumable-media = ">=2.7.2" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +protobuf = ["protobuf (<6.0.0dev)"] +tracing = ["opentelemetry-api (>=1.1.0)"] + +[[package]] +name = "google-cloud-texttospeech" +version = "2.27.0" +description = "Google Cloud Texttospeech API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_texttospeech-2.27.0-py3-none-any.whl", hash = "sha256:0f7c5fe05281beb6d005ea191f61c913085e8439e5ffd2d5d21e29d106150b54"}, + {file = "google_cloud_texttospeech-2.27.0.tar.gz", hash = "sha256:94a382c95b7cc58efd2505a24c2968e2614fc6bdf9d76fb9a819d4ed29ae188e"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "google-cloud-trace" +version = "1.16.2" +description = "Google Cloud Trace API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_trace-1.16.2-py3-none-any.whl", hash = "sha256:40fb74607752e4ee0f3d7e5fc6b8f6eb1803982254a1507ba918172484131456"}, + {file = "google_cloud_trace-1.16.2.tar.gz", hash = "sha256:89bef223a512465951eb49335be6d60bee0396d576602dbf56368439d303cab4"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "google-cloud-translate" +version = "3.21.1" +description = "Google Cloud Translate API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_cloud_translate-3.21.1-py3-none-any.whl", hash = "sha256:f7d74592c3be41ce308a2b88eed6b76ff0ebbd9f87ddac4523324a64fce94e61"}, + {file = "google_cloud_translate-3.21.1.tar.gz", hash = "sha256:760f25e1b979fea6a59dca44ffc8a8dc708693c50ae37a39568ff1284c534be2"}, +] + +[package.dependencies] +google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" +google-cloud-core = ">=1.4.4,<3.0.0" +grpc-google-iam-v1 = ">=0.14.0,<1.0.0" +proto-plus = [ + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, +] +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "google-crc32c" +version = "1.7.1" +description = "A python wrapper of the C library 'Google CRC32C'" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76"}, + {file = "google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb"}, + {file = "google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603"}, + {file = "google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06"}, + {file = "google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77"}, + {file = "google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53"}, + {file = "google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194"}, + {file = "google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337"}, + {file = "google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65"}, + {file = "google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35"}, + {file = "google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb"}, + {file = "google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6"}, + {file = "google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3"}, + {file = "google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315"}, + {file = "google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242"}, + {file = "google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582"}, + {file = "google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589"}, + {file = "google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48"}, + {file = "google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82"}, + {file = "google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472"}, +] + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "google-genai" +version = "1.31.0" +description = "GenAI Python SDK" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "google_genai-1.31.0-py3-none-any.whl", hash = "sha256:5c6959bcf862714e8ed0922db3aaf41885bacf6318751b3421bf1e459f78892f"}, + {file = "google_genai-1.31.0.tar.gz", hash = "sha256:8572b47aa684357c3e5e10d290ec772c65414114939e3ad2955203e27cd2fcbc"}, +] + +[package.dependencies] +anyio = ">=4.8.0,<5.0.0" +google-auth = ">=2.14.1,<3.0.0" +httpx = ">=0.28.1,<1.0.0" +pydantic = ">=2.0.0,<3.0.0" +requests = ">=2.28.1,<3.0.0" +tenacity = ">=8.2.3,<9.2.0" +typing-extensions = ">=4.11.0,<5.0.0" +websockets = ">=13.0.0,<15.1.0" + +[package.extras] +aiohttp = ["aiohttp (<4.0.0)"] + +[[package]] +name = "google-resumable-media" +version = "2.7.2" +description = "Utilities for Google Media Downloads and Resumable Uploads" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa"}, + {file = "google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0"}, +] + +[package.dependencies] +google-crc32c = ">=1.0,<2.0dev" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)", "google-auth (>=1.22.0,<2.0dev)"] +requests = ["requests (>=2.18.0,<3.0.0dev)"] + +[[package]] +name = "googleapis-common-protos" +version = "1.70.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8"}, + {file = "googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257"}, +] + +[package.dependencies] +grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] + +[[package]] +name = "grpc-google-iam-v1" +version = "0.14.2" +description = "IAM API client library" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "grpc_google_iam_v1-0.14.2-py3-none-any.whl", hash = "sha256:a3171468459770907926d56a440b2bb643eec1d7ba215f48f3ecece42b4d8351"}, + {file = "grpc_google_iam_v1-0.14.2.tar.gz", hash = "sha256:b3e1fc387a1a329e41672197d0ace9de22c78dd7d215048c4c78712073f7bd20"}, +] + +[package.dependencies] +googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]} +grpcio = ">=1.44.0,<2.0.0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" + +[[package]] +name = "grpcio" +version = "1.74.0" +description = "HTTP/2-based RPC framework" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907"}, + {file = "grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb"}, + {file = "grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486"}, + {file = "grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11"}, + {file = "grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9"}, + {file = "grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc"}, + {file = "grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e"}, + {file = "grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82"}, + {file = "grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7"}, + {file = "grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5"}, + {file = "grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31"}, + {file = "grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4"}, + {file = "grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce"}, + {file = "grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3"}, + {file = "grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182"}, + {file = "grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d"}, + {file = "grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f"}, + {file = "grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4"}, + {file = "grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b"}, + {file = "grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11"}, + {file = "grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8"}, + {file = "grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6"}, + {file = "grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5"}, + {file = "grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49"}, + {file = "grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7"}, + {file = "grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3"}, + {file = "grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707"}, + {file = "grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b"}, + {file = "grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c"}, + {file = "grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc"}, + {file = "grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89"}, + {file = "grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01"}, + {file = "grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e"}, + {file = "grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91"}, + {file = "grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249"}, + {file = "grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362"}, + {file = "grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f"}, + {file = "grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20"}, + {file = "grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa"}, + {file = "grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24"}, + {file = "grpcio-1.74.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:4bc5fca10aaf74779081e16c2bcc3d5ec643ffd528d9e7b1c9039000ead73bae"}, + {file = "grpcio-1.74.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:6bab67d15ad617aff094c382c882e0177637da73cbc5532d52c07b4ee887a87b"}, + {file = "grpcio-1.74.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:655726919b75ab3c34cdad39da5c530ac6fa32696fb23119e36b64adcfca174a"}, + {file = "grpcio-1.74.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a2b06afe2e50ebfd46247ac3ba60cac523f54ec7792ae9ba6073c12daf26f0a"}, + {file = "grpcio-1.74.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f251c355167b2360537cf17bea2cf0197995e551ab9da6a0a59b3da5e8704f9"}, + {file = "grpcio-1.74.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8f7b5882fb50632ab1e48cb3122d6df55b9afabc265582808036b6e51b9fd6b7"}, + {file = "grpcio-1.74.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:834988b6c34515545b3edd13e902c1acdd9f2465d386ea5143fb558f153a7176"}, + {file = "grpcio-1.74.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22b834cef33429ca6cc28303c9c327ba9a3fafecbf62fae17e9a7b7163cc43ac"}, + {file = "grpcio-1.74.0-cp39-cp39-win32.whl", hash = "sha256:7d95d71ff35291bab3f1c52f52f474c632db26ea12700c2ff0ea0532cb0b5854"}, + {file = "grpcio-1.74.0-cp39-cp39-win_amd64.whl", hash = "sha256:ecde9ab49f58433abe02f9ed076c7b5be839cf0153883a6d23995937a82392fa"}, + {file = "grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1"}, +] + +[package.extras] +protobuf = ["grpcio-tools (>=1.74.0)"] + +[[package]] +name = "grpcio-status" +version = "1.62.3" +description = "Status proto mapping for gRPC" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485"}, + {file = "grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.5.5" +grpcio = ">=1.62.3" +protobuf = ">=4.21.6" + +[[package]] +name = "gunicorn" +version = "21.2.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, + {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.6.4" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"}, + {file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1"}, + {file = "httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959"}, + {file = "httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4"}, + {file = "httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069"}, + {file = "httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975"}, + {file = "httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721"}, + {file = "httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988"}, + {file = "httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2"}, + {file = "httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1"}, + {file = "httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81"}, + {file = "httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f"}, + {file = "httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660"}, + {file = "httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3"}, + {file = "httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5"}, + {file = "httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0"}, + {file = "httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d3f0d369e7ffbe59c4b6116a44d6a8eb4783aae027f2c0b366cf0aa964185dba"}, + {file = "httptools-0.6.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:94978a49b8f4569ad607cd4946b759d90b285e39c0d4640c6b36ca7a3ddf2efc"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40dc6a8e399e15ea525305a2ddba998b0af5caa2566bcd79dcbe8948181eeaff"}, + {file = "httptools-0.6.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab9ba8dcf59de5181f6be44a77458e45a578fc99c31510b8c65b7d5acc3cf490"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc411e1c0a7dcd2f902c7c48cf079947a7e65b5485dea9decb82b9105ca71a43"}, + {file = "httptools-0.6.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d54efd20338ac52ba31e7da78e4a72570cf729fac82bc31ff9199bedf1dc7440"}, + {file = "httptools-0.6.4-cp38-cp38-win_amd64.whl", hash = "sha256:df959752a0c2748a65ab5387d08287abf6779ae9165916fe053e68ae1fbdc47f"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003"}, + {file = "httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547"}, + {file = "httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076"}, + {file = "httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd"}, + {file = "httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6"}, + {file = "httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c"}, +] + +[package.extras] +test = ["Cython (>=0.29.24)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "identify" +version = "2.6.13" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b"}, + {file = "identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "6.11.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "importlib_metadata-6.11.0-py3-none-any.whl", hash = "sha256:f0afba6205ad8f8947c7d338b5342d5db2afbfd82f9cbef7879a9539cc12eb9b"}, + {file = "importlib_metadata-6.11.0.tar.gz", hash = "sha256:1231cf92d825c9e03cfc4da076a16de6422c863558229ea0b22b675657463443"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7) ; platform_python_implementation != \"PyPy\"", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1) ; platform_python_implementation != \"PyPy\"", "pytest-perf (>=0.9.2)", "pytest-ruff"] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "kombu" +version = "5.5.4" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8"}, + {file = "kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +packaging = "*" +redis = {version = ">=4.5.2,<4.5.5 || >4.5.5,<5.0.2 || >5.0.2,<=5.2.1", optional = true, markers = "extra == \"redis\""} +tzdata = {version = ">=2025.2", markers = "python_version >= \"3.9\""} +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +gcpubsub = ["google-cloud-monitoring (>=2.16.0)", "google-cloud-pubsub (>=2.18.4)", "grpcio (==1.67.0)", "protobuf (==4.25.5)"] +librabbitmq = ["librabbitmq (>=2.0.0) ; python_version < \"3.11\""] +mongodb = ["pymongo (==4.10.1)"] +msgpack = ["msgpack (==1.1.0)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2,<=5.2.1)"] +slmq = ["softlayer_messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + +[[package]] +name = "libpass" +version = "1.9.1.post0" +description = "Fork of passlib, a comprehensive password hashing framework supporting over 30 schemes" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "libpass-1.9.1.post0-py3-none-any.whl", hash = "sha256:5bb794bac97042b814ab1eeb187c72a95d128cefbcee3c00c630f062c2da199c"}, + {file = "libpass-1.9.1.post0.tar.gz", hash = "sha256:5a4c3eb4985c6847cf046ae83c1ec1620bc157cf18714facad324650231c457e"}, +] + +[package.dependencies] +bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} + +[package.extras] +argon2 = ["argon2-cffi (>=18.2.0)"] +bcrypt = ["bcrypt (>=3.1.0)"] +totp = ["cryptography (>=43.0.1)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "motor" +version = "3.7.1" +description = "Non-blocking MongoDB driver for Tornado or asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "motor-3.7.1-py3-none-any.whl", hash = "sha256:8a63b9049e38eeeb56b4fdd57c3312a6d1f25d01db717fe7d82222393c410298"}, + {file = "motor-3.7.1.tar.gz", hash = "sha256:27b4d46625c87928f331a6ca9d7c51c2f518ba0e270939d395bc1ddc89d64526"}, +] + +[package.dependencies] +pymongo = ">=4.9,<5.0" + +[package.extras] +aws = ["pymongo[aws] (>=4.5,<5)"] +docs = ["aiohttp", "furo (==2024.8.6)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<8)", "sphinx-rtd-theme (>=2,<3)", "tornado"] +encryption = ["pymongo[encryption] (>=4.5,<5)"] +gssapi = ["pymongo[gssapi] (>=4.5,<5)"] +ocsp = ["pymongo[ocsp] (>=4.5,<5)"] +snappy = ["pymongo[snappy] (>=4.5,<5)"] +test = ["aiohttp (>=3.8.7)", "cffi (>=1.17.0rc1) ; python_version == \"3.13\"", "mockupdb", "pymongo[encryption] (>=4.5,<5)", "pytest (>=7)", "pytest-asyncio", "tornado (>=5)"] +zstd = ["pymongo[zstd] (>=4.5,<5)"] + +[[package]] +name = "multidict" +version = "6.6.4" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, + {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, + {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, + {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, + {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, + {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, + {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, + {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, + {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, + {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, + {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, + {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, + {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, + {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, + {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, + {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, + {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, + {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, + {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, + {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, + {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, + {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, + {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, + {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, + {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, + {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, + {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, + {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, + {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, + {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, + {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, + {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, + {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, + {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, + {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, + {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, + {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, + {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, + {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, +] + +[[package]] +name = "mypy" +version = "1.17.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, + {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, + {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, + {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, + {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, + {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, + {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, + {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, + {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, + {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, + {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, + {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, + {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, + {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, + {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, + {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, + {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, + {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, + {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, + {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, + {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, + {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, + {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, + {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, + {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, + {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, + {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, +] + +[package.dependencies] +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.21.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.21.0-py3-none-any.whl", hash = "sha256:4bb86b28627b7e41098f0e93280fe4892a1abed1b79a19aec6f928f39b17dffb"}, + {file = "opentelemetry_api-1.21.0.tar.gz", hash = "sha256:d6185fd5043e000075d921822fd2d26b953eba8ca21b1e2fa360dd46a7686316"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<7.0" + +[[package]] +name = "opentelemetry-exporter-gcp-trace" +version = "1.9.0" +description = "Google Cloud Trace exporter for OpenTelemetry" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_gcp_trace-1.9.0-py3-none-any.whl", hash = "sha256:0a8396e8b39f636eeddc3f0ae08ddb40c40f288bc8c5544727c3581545e77254"}, + {file = "opentelemetry_exporter_gcp_trace-1.9.0.tar.gz", hash = "sha256:c3fc090342f6ee32a0cc41a5716a6bb716b4422d19facefcb22dc4c6b683ece8"}, +] + +[package.dependencies] +google-cloud-trace = ">=1.1,<2.0" +opentelemetry-api = ">=1.0,<2.0" +opentelemetry-resourcedetector-gcp = ">=1.5.0dev0,<2.dev0" +opentelemetry-sdk = ">=1.0,<2.0" + +[[package]] +name = "opentelemetry-exporter-otlp" +version = "1.21.0" +description = "OpenTelemetry Collector Exporters" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp-1.21.0-py3-none-any.whl", hash = "sha256:40552c016ad3f26c1650b0f08acbf0fef96d57b056a07d4dd00b6df3d5c27b7e"}, + {file = "opentelemetry_exporter_otlp-1.21.0.tar.gz", hash = "sha256:2a959e6893b14d737f259d309e972f6b7343ab2be58e592fa6d8c23127f62875"}, +] + +[package.dependencies] +opentelemetry-exporter-otlp-proto-grpc = "1.21.0" +opentelemetry-exporter-otlp-proto-http = "1.21.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.21.0" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.21.0-py3-none-any.whl", hash = "sha256:97b1022b38270ec65d11fbfa348e0cd49d12006485c2321ea3b1b7037d42b6ec"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.21.0.tar.gz", hash = "sha256:61db274d8a68d636fb2ec2a0f281922949361cdd8236e25ff5539edf942b3226"}, +] + +[package.dependencies] +backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} +opentelemetry-proto = "1.21.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-grpc" +version = "1.21.0" +description = "OpenTelemetry Collector Protobuf over gRPC Exporter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_grpc-1.21.0-py3-none-any.whl", hash = "sha256:ab37c63d6cb58d6506f76d71d07018eb1f561d83e642a8f5aa53dddf306087a4"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.21.0.tar.gz", hash = "sha256:a497c5611245a2d17d9aa1e1cbb7ab567843d53231dcc844a62cea9f0924ffa7"}, +] + +[package.dependencies] +backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +grpcio = ">=1.0.0,<2.0.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.21.0" +opentelemetry-proto = "1.21.0" +opentelemetry-sdk = ">=1.21.0,<1.22.0" + +[package.extras] +test = ["pytest-grpc"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.21.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.21.0-py3-none-any.whl", hash = "sha256:56837773de6fb2714c01fc4895caebe876f6397bbc4d16afddf89e1299a55ee2"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.21.0.tar.gz", hash = "sha256:19d60afa4ae8597f7ef61ad75c8b6c6b7ef8cb73a33fb4aed4dbc86d5c8d3301"}, +] + +[package.dependencies] +backoff = {version = ">=1.10.0,<3.0.0", markers = "python_version >= \"3.7\""} +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.21.0" +opentelemetry-proto = "1.21.0" +opentelemetry-sdk = ">=1.21.0,<1.22.0" +requests = ">=2.7,<3.0" + +[package.extras] +test = ["responses (==0.22.0)"] + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.42b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation-0.42b0-py3-none-any.whl", hash = "sha256:65ae54ddb90ca2d05d2d16bf6863173e7141eba1bbbf41fc9bbb02446adbe369"}, + {file = "opentelemetry_instrumentation-0.42b0.tar.gz", hash = "sha256:6a653a1fed0f76eea32885321d77c750483e987eeefa4cbf219fc83559543198"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +setuptools = ">=16.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.42b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.42b0-py3-none-any.whl", hash = "sha256:79b7278fb614aba1bf2211060960d3e8501c1d7d9314b857b30ad80ba34a2805"}, + {file = "opentelemetry_instrumentation_asgi-0.42b0.tar.gz", hash = "sha256:da1d5dd4f172c44c6c100dae352e1fd0ae36dc4f266b3fed68ce9d5ab94c9146"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.42b0" +opentelemetry-semantic-conventions = "0.42b0" +opentelemetry-util-http = "0.42b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] +test = ["opentelemetry-instrumentation-asgi[instruments]", "opentelemetry-test-utils (==0.42b0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.42b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.42b0-py3-none-any.whl", hash = "sha256:d53a26c4859767d5ba67109038cabc7165d97a8a8b7654ccde4ce290036d1725"}, + {file = "opentelemetry_instrumentation_fastapi-0.42b0.tar.gz", hash = "sha256:7181d4886e57182e93477c4b797a7cd5467820b93c238eeb3e7d27a563c176e8"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.42b0" +opentelemetry-instrumentation-asgi = "0.42b0" +opentelemetry-semantic-conventions = "0.42b0" +opentelemetry-util-http = "0.42b0" + +[package.extras] +instruments = ["fastapi (>=0.58,<1.0)"] +test = ["httpx (>=0.22,<1.0)", "opentelemetry-instrumentation-fastapi[instruments]", "opentelemetry-test-utils (==0.42b0)", "requests (>=2.23,<3.0)"] + +[[package]] +name = "opentelemetry-instrumentation-pymongo" +version = "0.42b0" +description = "OpenTelemetry pymongo instrumentation" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_pymongo-0.42b0-py3-none-any.whl", hash = "sha256:c91cb72efee5e8ca9af0d5bb65a42d1c045a7b01cef4bdf0039d66615eebe4b0"}, + {file = "opentelemetry_instrumentation_pymongo-0.42b0.tar.gz", hash = "sha256:7d23d478f1654a451dc07bb6f5b1c002eb99359b06c88c4d6c2807c9bfcca3bb"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.42b0" +opentelemetry-semantic-conventions = "0.42b0" + +[package.extras] +instruments = ["pymongo (>=3.1,<5.0)"] +test = ["opentelemetry-instrumentation-pymongo[instruments]", "opentelemetry-test-utils (==0.42b0)"] + +[[package]] +name = "opentelemetry-instrumentation-redis" +version = "0.42b0" +description = "OpenTelemetry Redis instrumentation" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_redis-0.42b0-py3-none-any.whl", hash = "sha256:2c509adfec59f5c04952fcc3d8ab44d1d0c0d1201b4be788c4a7ffb1f8fd0601"}, + {file = "opentelemetry_instrumentation_redis-0.42b0.tar.gz", hash = "sha256:c1f461baf6a3b266d4fca5188ab384505a71cc10bf71d32a6784b49d5db822f0"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.42b0" +opentelemetry-semantic-conventions = "0.42b0" +wrapt = ">=1.12.1" + +[package.extras] +instruments = ["redis (>=2.6)"] +test = ["opentelemetry-instrumentation-redis[instruments]", "opentelemetry-sdk (>=1.3,<2.0)", "opentelemetry-test-utils (==0.42b0)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.21.0" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_proto-1.21.0-py3-none-any.whl", hash = "sha256:32fc4248e83eebd80994e13963e683f25f3b443226336bb12b5b6d53638f50ba"}, + {file = "opentelemetry_proto-1.21.0.tar.gz", hash = "sha256:7d5172c29ed1b525b5ecf4ebe758c7138a9224441b3cfe683d0a237c33b1941f"}, +] + +[package.dependencies] +protobuf = ">=3.19,<5.0" + +[[package]] +name = "opentelemetry-resourcedetector-gcp" +version = "1.9.0a0" +description = "Google Cloud resource detector for OpenTelemetry" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_resourcedetector_gcp-1.9.0a0-py3-none-any.whl", hash = "sha256:4e5a0822b0f0d7647b7ceb282d7aa921dd7f45466540bd0a24f954f90db8fde8"}, + {file = "opentelemetry_resourcedetector_gcp-1.9.0a0.tar.gz", hash = "sha256:6860a6649d1e3b9b7b7f09f3918cc16b72aa0c0c590d2a72ea6e42b67c9a42e7"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.0,<2.0" +opentelemetry-sdk = ">=1.0,<2.0" +requests = ">=2.24,<3.0" +typing-extensions = ">=4.0,<5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.21.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_sdk-1.21.0-py3-none-any.whl", hash = "sha256:9fe633243a8c655fedace3a0b89ccdfc654c0290ea2d8e839bd5db3131186f73"}, + {file = "opentelemetry_sdk-1.21.0.tar.gz", hash = "sha256:3ec8cd3020328d6bc5c9991ccaf9ae820ccb6395a5648d9a95d3ec88275b8879"}, +] + +[package.dependencies] +opentelemetry-api = "1.21.0" +opentelemetry-semantic-conventions = "0.42b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.42b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_semantic_conventions-0.42b0-py3-none-any.whl", hash = "sha256:5cd719cbfec448af658860796c5d0fcea2fdf0945a2bed2363f42cb1ee39f526"}, + {file = "opentelemetry_semantic_conventions-0.42b0.tar.gz", hash = "sha256:44ae67a0a3252a05072877857e5cc1242c98d4cf12870159f1a94bec800d38ec"}, +] + +[[package]] +name = "opentelemetry-util-http" +version = "0.42b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "opentelemetry_util_http-0.42b0-py3-none-any.whl", hash = "sha256:764069ed2f7e9a98ed1a7a87111f838000484e388e81f467405933be4b0306c6"}, + {file = "opentelemetry_util_http-0.42b0.tar.gz", hash = "sha256:665e7d372837811aa08cbb9102d4da862441d1c9b1795d649ef08386c8a3cbbd"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "prometheus-client" +version = "0.19.0" +description = "Python client for the Prometheus monitoring system." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"}, + {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, +] + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "propcache" +version = "0.3.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, + {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, + {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, + {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, + {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, + {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, + {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, + {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, + {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, + {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, + {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, + {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, + {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, + {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, + {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, + {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, + {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, + {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, + {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, + {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, + {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, + {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, + {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, + {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, + {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, + {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, + {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, + {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, + {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, + {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, + {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, + {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, + {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, +] + +[[package]] +name = "proto-plus" +version = "1.26.1" +description = "Beautiful, Pythonic protocol buffers" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, + {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, +] + +[package.dependencies] +protobuf = ">=3.19.0,<7.0.0" + +[package.extras] +testing = ["google-api-core (>=1.31.5)"] + +[[package]] +name = "protobuf" +version = "4.25.8" +description = "" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0"}, + {file = "protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9"}, + {file = "protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f"}, + {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7"}, + {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0"}, + {file = "protobuf-4.25.8-cp38-cp38-win32.whl", hash = "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af"}, + {file = "protobuf-4.25.8-cp38-cp38-win_amd64.whl", hash = "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3"}, + {file = "protobuf-4.25.8-cp39-cp39-win32.whl", hash = "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5"}, + {file = "protobuf-4.25.8-cp39-cp39-win_amd64.whl", hash = "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24"}, + {file = "protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59"}, + {file = "protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, +] + +[package.dependencies] +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} +pydantic-core = "2.33.2" +typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, + {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, + {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, + {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, + {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, + {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, + {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, + {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, + {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, + {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, + {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, + {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, + {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, + {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, + {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, + {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, + {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, + {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, + {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, + {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, + {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, + {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, + {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, + {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, + {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pydub" +version = "0.25.1" +description = "Manipulate audio with an simple and easy high level interface" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, + {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, +] + +[[package]] +name = "pymongo" +version = "4.14.1" +description = "PyMongo - the Official MongoDB Python driver" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pymongo-4.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97f0da391fb32f989f0afcd1838faff5595456d24c56d196174eddbb7c3a494c"}, + {file = "pymongo-4.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec160c4e1184da11d375a4315917f5a04180ea0ff522f0a97cf78acbb65810d8"}, + {file = "pymongo-4.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c95ce2e0dcd9a556e1f51a4132db88c40e8e0a49c0b16d1dddba624f640895b"}, + {file = "pymongo-4.14.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7b965614c16ac7d2cf297fbfb16a9ec81c0493bd5916f455a8e8020e432300b"}, + {file = "pymongo-4.14.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f81e8156a862ad8b44a065bd89978361a3054571e61b5e802ebdef91bb13ccad"}, + {file = "pymongo-4.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0fe8e7bbb59cb0652df0efd285e80e6a92207f5ced4a0f7de56275fd9c21b77"}, + {file = "pymongo-4.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c6d426e70a35d1dd5003a535ac8c0683998bea783949daa980d70272baa5cb05"}, + {file = "pymongo-4.14.1-cp310-cp310-win32.whl", hash = "sha256:8a4fe1b1603865e44c3dbce2b91ac2f18b1672208ff49203e8a480ab68a2d8f5"}, + {file = "pymongo-4.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:27cb44c71e6f220b163e1d3c0dd18559e534d5d7cb7e16afa0cf1b7761403492"}, + {file = "pymongo-4.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af4e667902314bcc05c90ea4ac0351bb759410ae0c5496ae47aef80659a12a44"}, + {file = "pymongo-4.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98c36403c97ec3a439a9ea5cdea730e34f0bf3c39eacfcab3fb07b34f5ef42a7"}, + {file = "pymongo-4.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95bfb5fe10a8aa11029868c403939945092fb8d160ca3a10d386778ed9623533"}, + {file = "pymongo-4.14.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44beff3470a6b1736f9e9cf7fb6477fdb2342b6f19a722cab3bbc989c5f3f693"}, + {file = "pymongo-4.14.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3176250b89ecc0db8120caf9945ded340eacebec7183f2093e58370041c2d5a8"}, + {file = "pymongo-4.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37312c841be2c2edd090b49861dab2e6117ff15cabf801f5910931105740e"}, + {file = "pymongo-4.14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573b1ed740dbb51be0819ede005012f4fa37df2c27c94d7d2e18288e16e1ef10"}, + {file = "pymongo-4.14.1-cp311-cp311-win32.whl", hash = "sha256:4812d168f9cd5f257805807a44637afcd0bb7fd22ac4738321bc6aa50ebd9d4f"}, + {file = "pymongo-4.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:9485278fed0a8933c8ce8f97ab518158b82e884d4a7bc34e1d784b751c7b69f3"}, + {file = "pymongo-4.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2cafb545a77738f0506cd538be1b14e9f40ad0b62634d89e1845dee3c726ad5"}, + {file = "pymongo-4.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a76afb1375f6914fecfdc3bfe6fb7c8c36b682c4707b7fb8ded5c2e17a1c2d77"}, + {file = "pymongo-4.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f5a4223c6acecb0ab25202a5b4ed6f2b6a41c30204ef44d3d46525e8ea455a9"}, + {file = "pymongo-4.14.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89c1f6804ae16101d5dd6cf0bd06b10e70e5e870aa98a198824c772ce3cb8ba3"}, + {file = "pymongo-4.14.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaef22550ba1034e9b0ed309395ec72944348c277e27cc973cd5b07322b1d088"}, + {file = "pymongo-4.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71500e97dbbda5d3e5dc9354dca865246c7502eea9d041c1ce0ae2c3fa018fd2"}, + {file = "pymongo-4.14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6eeea7c92fd8ccd24ad156e2f9c2a117220f1ba0a41968b26d953dc6b8082b1d"}, + {file = "pymongo-4.14.1-cp312-cp312-win32.whl", hash = "sha256:78e9ec6345a14e2144a514f501e3bfe69ec8c8fefd0759757e4f47bf0b243522"}, + {file = "pymongo-4.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:714589ce1df891e91f808b1e6e678990040997972d2c70454efebfefd1c8e299"}, + {file = "pymongo-4.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb147d0d77863ae89fa73cf8c0cc1a68d7dd7c5689cf0381501505307136b2bd"}, + {file = "pymongo-4.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e386721b57a50a5acd6e19c3c14cb975cbc0bf1a0364227d6cc15b486bb094cc"}, + {file = "pymongo-4.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49a2bf594ce1693f8a3cc4123ec3fa3a86215b395333b22be83c9eb765b24ecb"}, + {file = "pymongo-4.14.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb6679929e5bab898e9c5b46ee6fd025f6eb14380e9d4a210e122d79b223548"}, + {file = "pymongo-4.14.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcbea95a877b2c7c4e4a18527c4eecbe91bdcb0b202f93d5713d50386138ffa3"}, + {file = "pymongo-4.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04e780ff2854278d24f7a2011aed45b3df89520c89ca29a7c1ccf9a9f0d513d0"}, + {file = "pymongo-4.14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:147711a3b95d45dd11377a078e77fa302142b67656a8f57076693aa7fba124c1"}, + {file = "pymongo-4.14.1-cp313-cp313-win32.whl", hash = "sha256:6b945dda0359ba13171201fa2f1e32d4b5e73f57606b8c6dd560eeebf4a69d84"}, + {file = "pymongo-4.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fba1dcad4260a9c96aa5bd576bf96edeea5682cd6da6b5777c644ef103f16f6"}, + {file = "pymongo-4.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:184b0b6c3663bec2c13d7e2f0a99233c24b1bc7d8163b8b9a019a3ab159b1ade"}, + {file = "pymongo-4.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0a9bdb95e6fab64c8453dae84834dfd7a8b91cfbc7a3e288d9cdd161621a867"}, + {file = "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df5cc411dbe2b064945114598fdb3e36c3eeb38ed2559e459d5a7b2d91074a54"}, + {file = "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33a8b2c47db66f3bb33d62e3884fb531b77a58efd412b67b0539c685950c2382"}, + {file = "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f08880ad8bd6bdd4bdb5c93c4a6946c5c4e429b648c3b665c435af02005e7db"}, + {file = "pymongo-4.14.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92f8c2a3d0f17c432d68304d3abcab36a8a7ba78db93a143ac77eef6b70bc126"}, + {file = "pymongo-4.14.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:019f8f9b8a61a5780450c5908c38f63e4248f286d804163d3728bc544f0b07b2"}, + {file = "pymongo-4.14.1-cp313-cp313t-win32.whl", hash = "sha256:414a999a5b9212635f51c8b23481626406b731abaea16659a39df00f538d06d8"}, + {file = "pymongo-4.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9375cf27c04d2be7d02986262e0593ece1e78fa1934744bdd74c0c0b0cd2c2f2"}, + {file = "pymongo-4.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8945b11c4e39c13b47ec79dd0ee05126a6cf4753cf5fdceabf8cc51c02e21e6"}, + {file = "pymongo-4.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7d6114f4a60b04205b4fce120567955402816ac75329b9282fc8a603ac615ef"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6649018ae12a28b8d8399ddda5cb662ac364e338faf0a621e6b9e5ec643134df"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0bd1a446b39216453f53d55143a82e8617730723f100de940f1611ee35e78d6"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e09e59bb15edf0d948de6fa2b6f1cbb25ee63e7beba6d45ef6e94609e759efaa"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1604d9f669b044d30ca1775ebe37ddbd1972eaa7ffd041dde9e026b0334c69bd"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91f9a3d771ab86229244098125b1c22111aa3e3679534d626db8d05cd9c59ea4"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c93d1f5db2bf63b4958aef2a914520c7103187d68359b512a8d6d62f5d7a752"}, + {file = "pymongo-4.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ed9c0e22f874419f07022a9133e8d62aa8b665ceb2d89218ee88450c2824185e"}, + {file = "pymongo-4.14.1-cp39-cp39-win32.whl", hash = "sha256:06e2e8996324823e19bccea4dfd7ed543513410bbc7be9860502b62822d62bd4"}, + {file = "pymongo-4.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:0e679c8f62ec0e6ba64799ce55b22d76c80cd042f7d99fa2cfbb4d935ac61bea"}, + {file = "pymongo-4.14.1.tar.gz", hash = "sha256:d78f5b0b569f4320e2485599d89b088aa6d750aad17cc98fd81a323b544ed3d0"}, +] + +[package.dependencies] +dnspython = ">=1.16.0,<3.0.0" + +[package.extras] +aws = ["pymongo-auth-aws (>=1.1.0,<2.0.0)"] +docs = ["furo (==2025.7.19)", "readthedocs-sphinx-search (>=0.3,<1.0)", "sphinx (>=5.3,<9)", "sphinx-autobuild (>=2020.9.1)", "sphinx-rtd-theme (>=2,<4)", "sphinxcontrib-shellcheck (>=1,<2)"] +encryption = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "pymongo-auth-aws (>=1.1.0,<2.0.0)", "pymongocrypt (>=1.13.0,<2.0.0)"] +gssapi = ["pykerberos ; os_name != \"nt\"", "winkerberos (>=0.5.0) ; os_name == \"nt\""] +ocsp = ["certifi ; os_name == \"nt\" or sys_platform == \"darwin\"", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] +snappy = ["python-snappy"] +test = ["pytest (>=8.2)", "pytest-asyncio (>=0.24.0)"] +zstd = ["zstandard"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.21.2" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.21.2-py3-none-any.whl", hash = "sha256:ab664c88bb7998f711d8039cacd4884da6430886ae8bbd4eded552ed2004f16b"}, + {file = "pytest_asyncio-0.21.2.tar.gz", hash = "sha256:d67738fc232b94b326b9d060750beb16e0074210b98dd8b58a5239fa2a154f45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-mock" +version = "3.14.1" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, + {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +version = "1.1.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, + {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-http-client" +version = "3.3.7" +description = "HTTP REST client, simplified for Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +files = [ + {file = "python_http_client-3.3.7-py3-none-any.whl", hash = "sha256:ad371d2bbedc6ea15c26179c6222a78bc9308d272435ddf1d5c84f068f249a36"}, + {file = "python_http_client-3.3.7.tar.gz", hash = "sha256:bf841ee45262747e00dec7ee9971dfb8c7d83083f5713596488d67739170cea0"}, +] + +[[package]] +name = "python-jose" +version = "3.5.0" +description = "JOSE implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"}, + {file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""} +ecdsa = "!=0.15" +pyasn1 = ">=0.5.0" +rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "python-magic" +version = "0.4.27" +description = "File type identification using libmagic" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"}, + {file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"}, +] + +[[package]] +name = "python-multipart" +version = "0.0.6" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, + {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, +] + +[package.extras] +dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "redis" +version = "5.2.1" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, + {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=3.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rsa" +version = "4.9.1" +description = "Pure-Python RSA implementation" +optional = false +python-versions = "<4,>=3.6" +groups = ["main"] +files = [ + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "sendgrid" +version = "6.12.4" +description = "Twilio SendGrid library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +groups = ["main"] +files = [ + {file = "sendgrid-6.12.4-py3-none-any.whl", hash = "sha256:9a211b96241e63bd5b9ed9afcc8608f4bcac426e4a319b3920ab877c8426e92c"}, + {file = "sendgrid-6.12.4.tar.gz", hash = "sha256:9e88b849daf0fa4bdf256c3b5da9f5a3272402c0c2fd6b1928c9de440db0a03d"}, +] + +[package.dependencies] +ecdsa = ">=0.19.1,<1" +python-http-client = ">=3.2.1" +werkzeug = [ + {version = ">=2.3.5", markers = "python_version >= \"3.12\""}, + {version = ">=2.2.0", markers = "python_version == \"3.11\""}, +] + +[[package]] +name = "sentry-sdk" +version = "1.45.1" +description = "Python client for Sentry (https://sentry.io)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1"}, + {file = "sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26"}, +] + +[package.dependencies] +certifi = "*" +fastapi = {version = ">=0.79.0", optional = true, markers = "extra == \"fastapi\""} +urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""} + +[package.extras] +aiohttp = ["aiohttp (>=3.5)"] +arq = ["arq (>=0.23)"] +asyncpg = ["asyncpg (>=0.23)"] +beam = ["apache-beam (>=2.12)"] +bottle = ["bottle (>=0.12.13)"] +celery = ["celery (>=3)"] +celery-redbeat = ["celery-redbeat (>=2)"] +chalice = ["chalice (>=1.16.0)"] +clickhouse-driver = ["clickhouse-driver (>=0.2.0)"] +django = ["django (>=1.8)"] +falcon = ["falcon (>=1.4)"] +fastapi = ["fastapi (>=0.79.0)"] +flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"] +grpcio = ["grpcio (>=1.21.1)"] +httpx = ["httpx (>=0.16.0)"] +huey = ["huey (>=2)"] +loguru = ["loguru (>=0.5)"] +openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"] +opentelemetry = ["opentelemetry-distro (>=0.35b0)"] +opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"] +pure-eval = ["asttokens", "executing", "pure-eval"] +pymongo = ["pymongo (>=3.1)"] +pyspark = ["pyspark (>=2.4.4)"] +quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] +rq = ["rq (>=0.6)"] +sanic = ["sanic (>=0.8)"] +sqlalchemy = ["sqlalchemy (>=1.2)"] +starlette = ["starlette (>=0.19.1)"] +starlite = ["starlite (>=1.48)"] +tornado = ["tornado (>=5)"] + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "tenacity" +version = "9.1.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, + {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.24.0.post1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, + {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.21.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, + {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, + {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, + {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, + {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, + {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, + {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, + {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, + {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, + {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, + {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, + {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, + {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, + {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, + {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, + {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, + {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, + {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, + {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, + {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026"}, + {file = "virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "watchfiles" +version = "1.1.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "watchfiles-1.1.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:27f30e14aa1c1e91cb653f03a63445739919aef84c8d2517997a83155e7a2fcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3366f56c272232860ab45c77c3ca7b74ee819c8e1f6f35a7125556b198bbc6df"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8412eacef34cae2836d891836a7fff7b754d6bcac61f6c12ba5ca9bc7e427b68"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df670918eb7dd719642e05979fc84704af913d563fd17ed636f7c4783003fdcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7642b9bc4827b5518ebdb3b82698ada8c14c7661ddec5fe719f3e56ccd13c97"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:199207b2d3eeaeb80ef4411875a6243d9ad8bc35b07fc42daa6b801cc39cc41c"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a479466da6db5c1e8754caee6c262cd373e6e6c363172d74394f4bff3d84d7b5"}, + {file = "watchfiles-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:935f9edd022ec13e447e5723a7d14456c8af254544cefbc533f6dd276c9aa0d9"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8076a5769d6bdf5f673a19d51da05fc79e2bbf25e9fe755c47595785c06a8c72"}, + {file = "watchfiles-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:86b1e28d4c37e89220e924305cd9f82866bb0ace666943a6e4196c5df4d58dcc"}, + {file = "watchfiles-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d1caf40c1c657b27858f9774d5c0e232089bca9cb8ee17ce7478c6e9264d2587"}, + {file = "watchfiles-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:a89c75a5b9bc329131115a409d0acc16e8da8dfd5867ba59f1dd66ae7ea8fa82"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c9649dfc57cc1f9835551deb17689e8d44666315f2e82d337b9f07bd76ae3aa2"}, + {file = "watchfiles-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:406520216186b99374cdb58bc48e34bb74535adec160c8459894884c983a149c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb45350fd1dc75cd68d3d72c47f5b513cb0578da716df5fba02fff31c69d5f2d"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11ee4444250fcbeb47459a877e5e80ed994ce8e8d20283857fc128be1715dac7"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bda8136e6a80bdea23e5e74e09df0362744d24ffb8cd59c4a95a6ce3d142f79c"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b915daeb2d8c1f5cee4b970f2e2c988ce6514aace3c9296e58dd64dc9aa5d575"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed8fc66786de8d0376f9f913c09e963c66e90ced9aa11997f93bdb30f7c872a8"}, + {file = "watchfiles-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe4371595edf78c41ef8ac8df20df3943e13defd0efcb732b2e393b5a8a7a71f"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b7c5f6fe273291f4d414d55b2c80d33c457b8a42677ad14b4b47ff025d0893e4"}, + {file = "watchfiles-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7738027989881e70e3723c75921f1efa45225084228788fc59ea8c6d732eb30d"}, + {file = "watchfiles-1.1.0-cp311-cp311-win32.whl", hash = "sha256:622d6b2c06be19f6e89b1d951485a232e3b59618def88dbeda575ed8f0d8dbf2"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:48aa25e5992b61debc908a61ab4d3f216b64f44fdaa71eb082d8b2de846b7d12"}, + {file = "watchfiles-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:00645eb79a3faa70d9cb15c8d4187bb72970b2470e938670240c7998dad9f13a"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9dc001c3e10de4725c749d4c2f2bdc6ae24de5a88a339c4bce32300a31ede179"}, + {file = "watchfiles-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9ba68ec283153dead62cbe81872d28e053745f12335d037de9cbd14bd1877f5"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:130fc497b8ee68dce163e4254d9b0356411d1490e868bd8790028bc46c5cc297"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50a51a90610d0845a5931a780d8e51d7bd7f309ebc25132ba975aca016b576a0"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc44678a72ac0910bac46fa6a0de6af9ba1355669b3dfaf1ce5f05ca7a74364e"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a543492513a93b001975ae283a51f4b67973662a375a403ae82f420d2c7205ee"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ac164e20d17cc285f2b94dc31c384bc3aa3dd5e7490473b3db043dd70fbccfd"}, + {file = "watchfiles-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7590d5a455321e53857892ab8879dce62d1f4b04748769f5adf2e707afb9d4f"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:37d3d3f7defb13f62ece99e9be912afe9dd8a0077b7c45ee5a57c74811d581a4"}, + {file = "watchfiles-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7080c4bb3efd70a07b1cc2df99a7aa51d98685be56be6038c3169199d0a1c69f"}, + {file = "watchfiles-1.1.0-cp312-cp312-win32.whl", hash = "sha256:cbcf8630ef4afb05dc30107bfa17f16c0896bb30ee48fc24bf64c1f970f3b1fd"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:cbd949bdd87567b0ad183d7676feb98136cde5bb9025403794a4c0db28ed3a47"}, + {file = "watchfiles-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:0a7d40b77f07be87c6faa93d0951a0fcd8cbca1ddff60a1b65d741bac6f3a9f6"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5007f860c7f1f8df471e4e04aaa8c43673429047d63205d1630880f7637bca30"}, + {file = "watchfiles-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:20ecc8abbd957046f1fe9562757903f5eaf57c3bce70929fda6c7711bb58074a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2f0498b7d2a3c072766dba3274fe22a183dbea1f99d188f1c6c72209a1063dc"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:239736577e848678e13b201bba14e89718f5c2133dfd6b1f7846fa1b58a8532b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eff4b8d89f444f7e49136dc695599a591ff769300734446c0a86cba2eb2f9895"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12b0a02a91762c08f7264e2e79542f76870c3040bbc847fb67410ab81474932a"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29e7bc2eee15cbb339c68445959108803dc14ee0c7b4eea556400131a8de462b"}, + {file = "watchfiles-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9481174d3ed982e269c090f780122fb59cee6c3796f74efe74e70f7780ed94c"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:80f811146831c8c86ab17b640801c25dc0a88c630e855e2bef3568f30434d52b"}, + {file = "watchfiles-1.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:60022527e71d1d1fda67a33150ee42869042bce3d0fcc9cc49be009a9cded3fb"}, + {file = "watchfiles-1.1.0-cp313-cp313-win32.whl", hash = "sha256:32d6d4e583593cb8576e129879ea0991660b935177c0f93c6681359b3654bfa9"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:f21af781a4a6fbad54f03c598ab620e3a77032c5878f3d780448421a6e1818c7"}, + {file = "watchfiles-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:5366164391873ed76bfdf618818c82084c9db7fac82b64a20c44d335eec9ced5"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:17ab167cca6339c2b830b744eaf10803d2a5b6683be4d79d8475d88b4a8a4be1"}, + {file = "watchfiles-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:328dbc9bff7205c215a7807da7c18dce37da7da718e798356212d22696404339"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7208ab6e009c627b7557ce55c465c98967e8caa8b11833531fdf95799372633"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a8f6f72974a19efead54195bc9bed4d850fc047bb7aa971268fd9a8387c89011"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d181ef50923c29cf0450c3cd47e2f0557b62218c50b2ab8ce2ecaa02bd97e670"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb4167043d3a78280d5d05ce0ba22055c266cf8655ce942f2fb881262ff3cdf"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5701dc474b041e2934a26d31d39f90fac8a3dee2322b39f7729867f932b1d4"}, + {file = "watchfiles-1.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b067915e3c3936966a8607f6fe5487df0c9c4afb85226613b520890049deea20"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:9c733cda03b6d636b4219625a4acb5c6ffb10803338e437fb614fef9516825ef"}, + {file = "watchfiles-1.1.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:cc08ef8b90d78bfac66f0def80240b0197008e4852c9f285907377b2947ffdcb"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:9974d2f7dc561cce3bb88dfa8eb309dab64c729de85fba32e98d75cf24b66297"}, + {file = "watchfiles-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c68e9f1fcb4d43798ad8814c4c1b61547b014b667216cb754e606bfade587018"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95ab1594377effac17110e1352989bdd7bdfca9ff0e5eeccd8c69c5389b826d0"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fba9b62da882c1be1280a7584ec4515d0a6006a94d6e5819730ec2eab60ffe12"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3434e401f3ce0ed6b42569128b3d1e3af773d7ec18751b918b89cd49c14eaafb"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa257a4d0d21fcbca5b5fcba9dca5a78011cb93c0323fb8855c6d2dfbc76eb77"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd1b3879a578a8ec2076c7961076df540b9af317123f84569f5a9ddee64ce92"}, + {file = "watchfiles-1.1.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc7a30eeb0e20ecc5f4bd113cd69dcdb745a07c68c0370cea919f373f65d9e"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:891c69e027748b4a73847335d208e374ce54ca3c335907d381fde4e41661b13b"}, + {file = "watchfiles-1.1.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:12fe8eaffaf0faa7906895b4f8bb88264035b3f0243275e0bf24af0436b27259"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bfe3c517c283e484843cb2e357dd57ba009cff351edf45fb455b5fbd1f45b15f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a9ccbf1f129480ed3044f540c0fdbc4ee556f7175e5ab40fe077ff6baf286d4e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba0e3255b0396cac3cc7bbace76404dd72b5438bf0d8e7cefa2f79a7f3649caa"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4281cd9fce9fc0a9dbf0fc1217f39bf9cf2b4d315d9626ef1d4e87b84699e7e8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d2404af8db1329f9a3c9b79ff63e0ae7131986446901582067d9304ae8aaf7f"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e78b6ed8165996013165eeabd875c5dfc19d41b54f94b40e9fff0eb3193e5e8e"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:249590eb75ccc117f488e2fabd1bfa33c580e24b96f00658ad88e38844a040bb"}, + {file = "watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8"}, + {file = "watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa"}, + {file = "watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29"}, + {file = "watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86"}, + {file = "watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f"}, + {file = "watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267"}, + {file = "watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea"}, + {file = "watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df32d59cb9780f66d165a9a7a26f19df2c7d24e3bd58713108b41d0ff4f929c6"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0ece16b563b17ab26eaa2d52230c9a7ae46cf01759621f4fbbca280e438267b3"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432"}, + {file = "watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866"}, + {file = "watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277"}, + {file = "watchfiles-1.1.0.tar.gz", hash = "sha256:693ed7ec72cbfcee399e92c895362b6e66d63dac6b91e2c11ae03d10d503e575"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wrapt" +version = "1.17.3" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2"}, + {file = "wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775"}, + {file = "wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05"}, + {file = "wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418"}, + {file = "wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390"}, + {file = "wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6"}, + {file = "wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85"}, + {file = "wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311"}, + {file = "wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5"}, + {file = "wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2"}, + {file = "wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89"}, + {file = "wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77"}, + {file = "wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba"}, + {file = "wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828"}, + {file = "wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396"}, + {file = "wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc"}, + {file = "wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe"}, + {file = "wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c"}, + {file = "wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77"}, + {file = "wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277"}, + {file = "wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa"}, + {file = "wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050"}, + {file = "wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8"}, + {file = "wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb"}, + {file = "wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235"}, + {file = "wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b"}, + {file = "wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7"}, + {file = "wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4"}, + {file = "wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10"}, + {file = "wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6"}, + {file = "wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067"}, + {file = "wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e"}, + {file = "wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056"}, + {file = "wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804"}, + {file = "wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116"}, + {file = "wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:70d86fa5197b8947a2fa70260b48e400bf2ccacdcab97bb7de47e3d1e6312225"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:df7d30371a2accfe4013e90445f6388c570f103d61019b6b7c57e0265250072a"}, + {file = "wrapt-1.17.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:caea3e9c79d5f0d2c6d9ab96111601797ea5da8e6d0723f77eabb0d4068d2b2f"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:758895b01d546812d1f42204bd443b8c433c44d090248bf22689df673ccafe00"}, + {file = "wrapt-1.17.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02b551d101f31694fc785e58e0720ef7d9a10c4e62c1c9358ce6f63f23e30a56"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:656873859b3b50eeebe6db8b1455e99d90c26ab058db8e427046dbc35c3140a5"}, + {file = "wrapt-1.17.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a9a2203361a6e6404f80b99234fe7fb37d1fc73487b5a78dc1aa5b97201e0f22"}, + {file = "wrapt-1.17.3-cp38-cp38-win32.whl", hash = "sha256:55cbbc356c2842f39bcc553cf695932e8b30e30e797f961860afb308e6b1bb7c"}, + {file = "wrapt-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:ad85e269fe54d506b240d2d7b9f5f2057c2aa9a2ea5b32c66f8902f768117ed2"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:30ce38e66630599e1193798285706903110d4f057aab3168a34b7fdc85569afc"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65d1d00fbfb3ea5f20add88bbc0f815150dbbde3b026e6c24759466c8b5a9ef9"}, + {file = "wrapt-1.17.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a7c06742645f914f26c7f1fa47b8bc4c91d222f76ee20116c43d5ef0912bba2d"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e18f01b0c3e4a07fe6dfdb00e29049ba17eadbc5e7609a2a3a4af83ab7d710a"}, + {file = "wrapt-1.17.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f5f51a6466667a5a356e6381d362d259125b57f059103dd9fdc8c0cf1d14139"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:59923aa12d0157f6b82d686c3fd8e1166fa8cdfb3e17b42ce3b6147ff81528df"}, + {file = "wrapt-1.17.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46acc57b331e0b3bcb3e1ca3b421d65637915cfcd65eb783cb2f78a511193f9b"}, + {file = "wrapt-1.17.3-cp39-cp39-win32.whl", hash = "sha256:3e62d15d3cfa26e3d0788094de7b64efa75f3a53875cdbccdf78547aed547a81"}, + {file = "wrapt-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:1f23fa283f51c890eda8e34e4937079114c74b4c81d2b2f1f1d94948f5cc3d7f"}, + {file = "wrapt-1.17.3-cp39-cp39-win_arm64.whl", hash = "sha256:24c2ed34dc222ed754247a2702b1e1e89fdbaa4016f324b4b8f1a802d4ffe87f"}, + {file = "wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22"}, + {file = "wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0"}, +] + +[[package]] +name = "yarl" +version = "1.20.1" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, + {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, + {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, + {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, + {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, + {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, + {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, + {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, + {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, + {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, + {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, + {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, + {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, + {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, + {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, + {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, + {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, + {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, + {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, + {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, + {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, + {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, + {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, + {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, + {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, + {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, + {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, + {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, + {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, + {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, + {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, + {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, + {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "548ec497ced9b0c27b0efabe4c3d1370f1acdf8e941bec0d9355d18117ec7607" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..47332eb --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,108 @@ +[tool.poetry] +name = "accessible-video-backend" +version = "0.1.0" +description = "FastAPI backend for accessible video processing platform" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11" +fastapi = "^0.115.0" +uvicorn = {extras = ["standard"], version = "^0.24.0"} +gunicorn = "^21.2.0" +pydantic = {extras = ["email"], version = "^2.5.0"} +pydantic-settings = "^2.1.0" +pymongo = "^4.6.0" +motor = "^3.3.2" +redis = "^5.0.1" +celery = {extras = ["redis"], version = "^5.3.4"} +google-cloud-storage = "^2.10.0" +google-cloud-translate = "^3.12.1" +google-cloud-texttospeech = "^2.16.3" +google-cloud-secret-manager = "^2.18.1" +google-genai = "^1.31.0" +sendgrid = "^6.11.0" +python-jose = {extras = ["cryptography"], version = "^3.3.0"} +libpass = {extras = ["bcrypt"], version = "^1.9.1"} +python-multipart = "^0.0.6" +opentelemetry-api = "^1.21.0" +opentelemetry-sdk = "^1.21.0" +opentelemetry-instrumentation-fastapi = "^0.42b0" +opentelemetry-instrumentation-pymongo = "^0.42b0" +opentelemetry-instrumentation-redis = "^0.42b0" +opentelemetry-exporter-gcp-trace = "^1.6.0" +opentelemetry-exporter-otlp = "^1.21.0" +# opentelemetry-exporter-prometheus = "^1.11.1" # Temporarily disabled - version conflicts +prometheus-client = "^0.19.0" +sentry-sdk = {extras = ["fastapi"], version = "^1.38.0"} +ffmpeg-python = "^0.2.0" +pydub = "^0.25.1" +python-magic = "^0.4.27" +aiohttp = "^3.12.15" +jinja2 = "^3.1.6" +audioop-lts = {version = "^0.2.2", python = ">=3.13"} + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +pytest-asyncio = "^0.21.1" +pytest-mock = "^3.12.0" +httpx = "^0.28.1" +black = "^23.11.0" +ruff = "^0.1.6" +mypy = "^1.7.1" +pre-commit = "^3.6.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 88 +target-version = ['py311'] +include = '\.pyi?$' + +[tool.ruff] +target-version = "py311" +line-length = 88 +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long, handled by black + "B008", # do not perform function calls in argument defaults + "C901", # too complex +] + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.mypy] +python_version = "3.11" +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +ignore_missing_imports = true +no_implicit_optional = true +show_error_codes = true +strict_equality = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" +asyncio_mode = "auto" \ No newline at end of file diff --git a/backend/setup_secrets.py b/backend/setup_secrets.py new file mode 100755 index 0000000..191f457 --- /dev/null +++ b/backend/setup_secrets.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +Setup script for creating secrets in Google Cloud Secret Manager. + +This script creates all required secrets for the Accessible Video Platform. +Run this once before deploying to production. + +Usage: + python setup_secrets.py --project-id YOUR_PROJECT_ID + python setup_secrets.py --project-id YOUR_PROJECT_ID --env prod +""" + +import argparse +import asyncio +import os +import secrets +import string +from typing import Dict + +from app.services.secrets_manager import secrets_manager, SecretManagerError + + +def generate_secure_key(length: int = 64) -> str: + """Generate a cryptographically secure random key.""" + alphabet = string.ascii_letters + string.digits + "!@#$%^&*" + return ''.join(secrets.choice(alphabet) for _ in range(length)) + + +async def create_secrets(project_id: str, environment: str = "prod") -> None: + """Create all required secrets in Secret Manager.""" + + # Configure project ID + os.environ["GOOGLE_CLOUD_PROJECT"] = project_id + + # Define secrets to create + secrets_config = { + "mongodb-url": { + "description": "MongoDB Atlas connection string", + "example": "mongodb+srv://user:password@cluster.mongodb.net/accessible_video?retryWrites=true&w=majority", + "generate": False + }, + "redis-url": { + "description": "Redis connection URL", + "example": "redis://redis-instance:6379/0", + "generate": False + }, + "jwt-secret": { + "description": "JWT signing secret key", + "example": None, # Will be generated + "generate": True + }, + "jwt-refresh-secret": { + "description": "JWT refresh token secret key", + "example": None, # Will be generated + "generate": True + }, + "gemini-api-key": { + "description": "Google Gemini API key", + "example": "AIza...your-api-key", + "generate": False + }, + "sendgrid-api-key": { + "description": "SendGrid API key for email notifications", + "example": "SG.xxx.xxx-your-sendgrid-key", + "generate": False + }, + "elevenlabs-api-key": { + "description": "ElevenLabs API key for text-to-speech", + "example": "el_xxx_your-elevenlabs-key", + "generate": False + }, + "sentry-dsn": { + "description": "Sentry DSN for error tracking", + "example": "https://xxx@xxx.ingest.sentry.io/xxx", + "generate": False + } + } + + print(f"🔐 Setting up secrets for project: {project_id}") + print(f"Environment: {environment}") + print("=" * 50) + + labels = { + "environment": environment, + "service": "accessible-video-platform" + } + + for secret_name, config in secrets_config.items(): + try: + if config["generate"]: + # Generate secure key + secret_value = generate_secure_key() + print(f"✅ Generated secure key for {secret_name}") + else: + # Prompt user for value + print(f"\n📝 {config['description']}") + if config["example"]: + print(f"Example: {config['example']}") + + secret_value = input(f"Enter value for {secret_name}: ").strip() + + if not secret_value: + print(f"⏭️ Skipping {secret_name} (empty value)") + continue + + # Create the secret + await secrets_manager.create_secret(secret_name, secret_value, labels) + print(f"✅ Created secret: {secret_name}") + + except SecretManagerError as e: + if "already exists" in str(e).lower(): + print(f"ℹ️ Secret {secret_name} already exists") + else: + print(f"❌ Failed to create secret {secret_name}: {e}") + except KeyboardInterrupt: + print(f"\n❌ Setup cancelled by user") + break + except Exception as e: + print(f"❌ Unexpected error creating {secret_name}: {e}") + + print("\n🎉 Secret setup completed!") + print("\n📋 Next steps:") + print("1. Verify all secrets are created in the GCP Console") + print("2. Ensure your service accounts have secretmanager.secretAccessor role") + print("3. Deploy your application using the Cloud Run configurations") + + +async def list_secrets() -> None: + """List all existing secrets in the project.""" + try: + # This would require additional implementation to list secrets + print("📋 Listing existing secrets...") + print("(Feature not implemented - check GCP Console)") + except Exception as e: + print(f"❌ Failed to list secrets: {e}") + + +async def test_secrets() -> None: + """Test secret retrieval to ensure everything is working.""" + print("🧪 Testing secret retrieval...") + + test_secrets = [ + "jwt-secret", + "jwt-refresh-secret" + ] + + for secret_name in test_secrets: + try: + value = await secrets_manager.get_secret(secret_name) + if value: + print(f"✅ Successfully retrieved {secret_name}") + else: + print(f"❌ Empty value for {secret_name}") + except SecretManagerError as e: + print(f"❌ Failed to retrieve {secret_name}: {e}") + + +def main(): + """Main CLI entry point.""" + parser = argparse.ArgumentParser(description="Setup secrets for Accessible Video Platform") + parser.add_argument("--project-id", required=True, help="Google Cloud Project ID") + parser.add_argument("--env", default="prod", choices=["dev", "staging", "prod"], help="Environment") + parser.add_argument("--list", action="store_true", help="List existing secrets") + parser.add_argument("--test", action="store_true", help="Test secret retrieval") + + args = parser.parse_args() + + if args.list: + asyncio.run(list_secrets()) + elif args.test: + asyncio.run(test_secrets()) + else: + asyncio.run(create_secrets(args.project_id, args.env)) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/simple_login_test.py b/backend/simple_login_test.py new file mode 100644 index 0000000..3d00a08 --- /dev/null +++ b/backend/simple_login_test.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Simple test server to isolate the login issue.""" + +from fastapi import FastAPI, Response +from motor.motor_asyncio import AsyncIOMotorClient +from app.core.config import settings +from app.core.security import verify_password, create_access_token, create_refresh_token +from app.models.user import User +from app.schemas.auth import LoginRequest, LoginResponse + +app = FastAPI() + +@app.post("/test-login") +async def test_login(login_data: LoginRequest, response: Response): + print(f"1. Received login request for: {login_data.email}") + + # Create database connection directly + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + print("2. Connecting to database...") + # Find user by email + user_doc = await db.users.find_one({"email": login_data.email}) + print(f"3. User lookup result: {user_doc is not None}") + + if not user_doc: + print("4. User not found") + return {"error": "User not found"} + + user = User(**user_doc) + print(f"5. User model created: {user.email}") + + # Verify password + print("6. Verifying password...") + password_valid = verify_password(login_data.password, user.hashed_password) + print(f"7. Password valid: {password_valid}") + + if not password_valid: + print("8. Password invalid") + return {"error": "Invalid password"} + + if not user.is_active: + print("9. User inactive") + return {"error": "User inactive"} + + print("10. Creating tokens...") + # Create tokens + access_token = create_access_token(subject=str(user.id)) + refresh_token = create_refresh_token(subject=str(user.id)) + print("11. Tokens created successfully") + + # Set refresh token as HttpOnly cookie + response.set_cookie( + key="refresh_token", + value=refresh_token, + httponly=True, + secure=settings.cookie_secure, + samesite=settings.cookie_samesite, + domain=settings.cookie_domain if settings.app_env == "prod" else None, + max_age=settings.jwt_refresh_ttl_days * 24 * 60 * 60, + ) + print("12. Cookie set") + + result = LoginResponse( + access_token=access_token, + user_id=str(user.id), + role=user.role.value, + ) + print("13. Response prepared") + return result + + except Exception as e: + print(f"ERROR: {e}") + import traceback + traceback.print_exc() + return {"error": str(e)} + + finally: + print("14. Closing database connection") + client.close() + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8002) \ No newline at end of file diff --git a/backend/test_auth.py b/backend/test_auth.py new file mode 100644 index 0000000..d09255e --- /dev/null +++ b/backend/test_auth.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Test auth functionality.""" + +import asyncio +from motor.motor_asyncio import AsyncIOMotorClient +from passlib.context import CryptContext +from app.core.config import settings +from app.models.user import User +from app.core.security import verify_password, create_access_token + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +async def test_login(): + print("Testing login logic...") + + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + try: + # Find user + user_doc = await db.users.find_one({"email": "admin@example.com"}) + print("Found user document:", bool(user_doc)) + + if user_doc: + print("User email:", user_doc.get('email')) + print("User role:", user_doc.get('role')) + print("User active:", user_doc.get('is_active')) + + # Create User object + user = User(**user_doc) + print("✅ User object created successfully") + + # Test password verification + password_ok = verify_password("admin", user.hashed_password) + print(f"Password verification: {password_ok}") + + if password_ok: + # Create token + token = create_access_token(subject=str(user.id)) + print(f"✅ Token created: {token[:20]}...") + else: + print("❌ Password verification failed") + + except Exception as e: + print(f"❌ Error: {e}") + import traceback + traceback.print_exc() + + client.close() + +if __name__ == "__main__": + asyncio.run(test_login()) \ No newline at end of file diff --git a/backend/test_db.py b/backend/test_db.py new file mode 100644 index 0000000..8154613 --- /dev/null +++ b/backend/test_db.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Test database connection.""" + +import asyncio +from motor.motor_asyncio import AsyncIOMotorClient +from app.core.config import settings + +async def test_db(): + print("Testing database connection...") + print(f"URI: {settings.mongodb_uri}") + + client = AsyncIOMotorClient(settings.mongodb_uri) + db = client[settings.mongodb_db] + + # Test connection + try: + await client.admin.command('ping') + print("✅ MongoDB connection successful") + + # Count users + user_count = await db.users.count_documents({}) + print(f"✅ User collection accessible, found {user_count} users") + + # Test finding a user + user = await db.users.find_one({"email": "admin@example.com"}) + if user: + print("✅ Found admin user:", user.get('email')) + else: + print("❌ Admin user not found") + + except Exception as e: + print(f"❌ Database error: {e}") + + client.close() + +if __name__ == "__main__": + asyncio.run(test_db()) \ No newline at end of file diff --git a/backend/test_endpoint.py b/backend/test_endpoint.py new file mode 100644 index 0000000..6eb9dc8 --- /dev/null +++ b/backend/test_endpoint.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Add a simple test endpoint to debug the issue.""" + +from fastapi import FastAPI, Depends +from motor.motor_asyncio import AsyncIOMotorDatabase +from app.core.database import get_database + +app = FastAPI() + +@app.post("/test-db") +async def test_db_endpoint(db: AsyncIOMotorDatabase = Depends(get_database)): + """Test database dependency injection.""" + try: + print("1. Inside test endpoint") + user_count = await db.users.count_documents({}) + print(f"2. User count: {user_count}") + return {"status": "success", "user_count": user_count} + except Exception as e: + print(f"3. Error: {e}") + return {"status": "error", "message": str(e)} + +@app.post("/test-simple") +async def test_simple_endpoint(): + """Test simple endpoint without dependencies.""" + print("Simple endpoint called") + return {"status": "success", "message": "Simple endpoint works"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8001) \ No newline at end of file diff --git a/backend/test_mp3_serving.py b/backend/test_mp3_serving.py new file mode 100644 index 0000000..a56d30c --- /dev/null +++ b/backend/test_mp3_serving.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +""" +Test MP3 serving to understand why frontend players show zero duration. +""" + +import asyncio +import aiohttp +import tempfile +import os +from app.services.gcs import get_signed_download_url, gcs_service +from app.core.config import settings + + +async def test_mp3_serving(): + """Test the complete MP3 serving pipeline.""" + + print("🔍 Testing MP3 Serving Pipeline") + print("=" * 50) + + # Find available MP3 files + blobs = list(gcs_service.bucket.list_blobs()) + mp3_blobs = [b for b in blobs if b.name.endswith('.mp3')] + + if not mp3_blobs: + print("❌ No MP3 files found in bucket") + return False + + print(f"Found {len(mp3_blobs)} MP3 files") + + # Test the first MP3 file + test_blob = mp3_blobs[0] + print(f"\n🎵 Testing: {test_blob.name}") + print(f" Size in GCS: {test_blob.size / 1024:.1f} KB") + + # Step 1: Generate signed URL (like the API does) + print(f"\n1️⃣ Generating signed download URL...") + try: + signed_url = await get_signed_download_url(test_blob.name, 24) + print(f"✅ Signed URL generated") + print(f" URL: {signed_url[:100]}...") + except Exception as e: + print(f"❌ Failed to generate signed URL: {e}") + return False + + # Step 2: Download via signed URL (like frontend does) + print(f"\n2️⃣ Downloading via signed URL...") + try: + async with aiohttp.ClientSession() as session: + async with session.get(signed_url) as response: + print(f" Status: {response.status}") + print(f" Content-Type: {response.headers.get('content-type')}") + print(f" Content-Length: {response.headers.get('content-length')}") + + if response.status != 200: + print(f"❌ HTTP error: {response.status}") + return False + + # Download the content + content = await response.read() + print(f" Downloaded: {len(content)} bytes") + + # Check if content size matches expectations + if len(content) == 0: + print(f"❌ Downloaded content is empty!") + return False + elif len(content) != test_blob.size: + print(f"⚠️ Size mismatch: downloaded {len(content)} vs GCS {test_blob.size}") + else: + print(f"✅ Content size matches GCS") + + # Step 3: Check if it's valid MP3 content + print(f"\n3️⃣ Validating MP3 content...") + print(f" First 20 bytes: {content[:20].hex()}") + + # Check MP3 headers + if content[:3] == b'ID3': + print(f"✅ Valid MP3 with ID3 header") + elif len(content) >= 2 and content[0] == 0xFF and (content[1] & 0xE0) == 0xE0: + print(f"✅ Valid MP3 with MPEG sync header") + else: + print(f"⚠️ May not be valid MP3 format") + + # Step 4: Save to temp file and test duration + print(f"\n4️⃣ Testing audio duration...") + try: + with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file: + temp_file.write(content) + temp_filename = temp_file.name + + # Try to get duration using pydub (same library used for TTS) + try: + from pydub import AudioSegment + audio = AudioSegment.from_mp3(temp_filename) + duration_seconds = len(audio) / 1000.0 + print(f"✅ MP3 duration: {duration_seconds:.2f} seconds") + + if duration_seconds == 0: + print(f"❌ MP3 has zero duration!") + return False + + except Exception as e: + print(f"❌ Could not parse MP3 with pydub: {e}") + return False + + finally: + # Clean up temp file + os.unlink(temp_filename) + + except Exception as e: + print(f"❌ Error testing duration: {e}") + return False + + return True + + except Exception as e: + print(f"❌ Download failed: {e}") + return False + + +async def test_direct_gcs_download(): + """Test downloading directly from GCS (bypass signed URL).""" + + print(f"\n🔄 Testing Direct GCS Download") + print("-" * 40) + + # Get the first MP3 blob + blobs = list(gcs_service.bucket.list_blobs()) + mp3_blobs = [b for b in blobs if b.name.endswith('.mp3')] + + if not mp3_blobs: + return False + + test_blob = mp3_blobs[0] + print(f"Testing: {test_blob.name}") + + try: + # Download directly from GCS + content = test_blob.download_as_bytes() + print(f"✅ Downloaded {len(content)} bytes directly from GCS") + + # Test with pydub + try: + with tempfile.NamedTemporaryFile(suffix='.mp3', delete=False) as temp_file: + temp_file.write(content) + temp_filename = temp_file.name + + from pydub import AudioSegment + audio = AudioSegment.from_mp3(temp_filename) + duration_seconds = len(audio) / 1000.0 + print(f"✅ Direct download MP3 duration: {duration_seconds:.2f} seconds") + + os.unlink(temp_filename) + return duration_seconds > 0 + + except Exception as e: + print(f"❌ Could not parse directly downloaded MP3: {e}") + return False + + except Exception as e: + print(f"❌ Direct download failed: {e}") + return False + + +if __name__ == "__main__": + async def main(): + success1 = await test_mp3_serving() + success2 = await test_direct_gcs_download() + + if success1 and success2: + print(f"\n🎉 MP3 serving works correctly!") + print("The issue may be in the frontend MP3 player implementation.") + else: + print(f"\n❌ Found issues with MP3 serving") + + asyncio.run(main()) \ No newline at end of file diff --git a/backend/tests/fixtures/sample_en_ad.vtt b/backend/tests/fixtures/sample_en_ad.vtt new file mode 100644 index 0000000..aee2ac6 --- /dev/null +++ b/backend/tests/fixtures/sample_en_ad.vtt @@ -0,0 +1,33 @@ +WEBVTT + +00:00:00.500 --> 00:00:01.000 +[Soft intro music fades in] + +00:00:01.000 --> 00:00:04.500 +[Speaker sits at modern desk with laptop, +bright office setting] + +00:00:10.500 --> 00:00:11.000 +[Screen displays HTML code example] + +00:00:14.000 --> 00:00:14.500 +[Cursor highlights heading tags h1, h2, h3] + +00:00:18.000 --> 00:00:20.500 +[Animation shows screen reader navigation] + +00:00:20.000 --> 00:00:20.500 +[Color palette appears on screen] + +00:00:24.500 --> 00:00:28.000 +[Contrast checker tool demonstrates +text readability differences] + +00:00:27.500 --> 00:00:28.000 +[Speaker gestures toward keyboard] + +00:00:31.000 --> 00:00:31.500 +[Tab navigation highlights interactive elements] + +00:00:34.500 --> 00:00:35.000 +[Video fades to end screen with resources] \ No newline at end of file diff --git a/backend/tests/fixtures/sample_en_captions.vtt b/backend/tests/fixtures/sample_en_captions.vtt new file mode 100644 index 0000000..f6a847d --- /dev/null +++ b/backend/tests/fixtures/sample_en_captions.vtt @@ -0,0 +1,39 @@ +WEBVTT + +00:00:01.000 --> 00:00:04.500 +Hello everyone, and welcome to today's +tutorial on web accessibility. + +00:00:04.500 --> 00:00:08.000 +In this video, we'll explore the essential +features that make websites accessible + +00:00:08.000 --> 00:00:10.500 +to users with disabilities. + +00:00:11.000 --> 00:00:14.500 +First, let's discuss the importance +of semantic HTML elements. + +00:00:14.500 --> 00:00:18.000 +When we use proper heading structures, +screen readers can navigate content + +00:00:18.000 --> 00:00:20.000 +more effectively. + +00:00:20.500 --> 00:00:24.000 +Next, we'll cover color contrast +requirements and how to ensure + +00:00:24.000 --> 00:00:27.500 +text remains readable for users +with visual impairments. + +00:00:28.000 --> 00:00:31.500 +Finally, we'll implement keyboard +navigation patterns that allow users + +00:00:31.500 --> 00:00:35.000 +to interact with our interfaces +without using a mouse. \ No newline at end of file diff --git a/backend/tests/fixtures/sample_es_ad.vtt b/backend/tests/fixtures/sample_es_ad.vtt new file mode 100644 index 0000000..80af527 --- /dev/null +++ b/backend/tests/fixtures/sample_es_ad.vtt @@ -0,0 +1,33 @@ +WEBVTT + +00:00:00.500 --> 00:00:01.000 +[Música suave de introducción se desvanece] + +00:00:01.000 --> 00:00:04.500 +[El presentador se sienta en un escritorio +moderno con laptop, ambiente de oficina luminoso] + +00:00:10.500 --> 00:00:11.000 +[La pantalla muestra ejemplo de código HTML] + +00:00:14.000 --> 00:00:14.500 +[El cursor resalta etiquetas de encabezado h1, h2, h3] + +00:00:18.000 --> 00:00:20.500 +[Animación muestra navegación de lector de pantalla] + +00:00:20.000 --> 00:00:20.500 +[Paleta de colores aparece en pantalla] + +00:00:24.500 --> 00:00:28.000 +[Herramienta de verificación de contraste +demuestra diferencias de legibilidad de texto] + +00:00:27.500 --> 00:00:28.000 +[El presentador gesticula hacia el teclado] + +00:00:31.000 --> 00:00:31.500 +[Navegación con tabulador resalta elementos interactivos] + +00:00:34.500 --> 00:00:35.000 +[El video se desvanece a pantalla final con recursos] \ No newline at end of file diff --git a/backend/tests/fixtures/sample_es_captions.vtt b/backend/tests/fixtures/sample_es_captions.vtt new file mode 100644 index 0000000..2d553bb --- /dev/null +++ b/backend/tests/fixtures/sample_es_captions.vtt @@ -0,0 +1,39 @@ +WEBVTT + +00:00:01.000 --> 00:00:04.500 +Hola a todos, y bienvenidos al tutorial +de hoy sobre accesibilidad web. + +00:00:04.500 --> 00:00:08.000 +En este video, exploraremos las características +esenciales que hacen los sitios web accesibles + +00:00:08.000 --> 00:00:10.500 +para usuarios con discapacidades. + +00:00:11.000 --> 00:00:14.500 +Primero, discutamos la importancia +de los elementos HTML semánticos. + +00:00:14.500 --> 00:00:18.000 +Cuando usamos estructuras de encabezados +apropiadas, los lectores de pantalla + +00:00:18.000 --> 00:00:20.000 +pueden navegar el contenido más efectivamente. + +00:00:20.500 --> 00:00:24.000 +A continuación, cubriremos los requisitos +de contraste de color y cómo asegurar + +00:00:24.000 --> 00:00:27.500 +que el texto permanezca legible para usuarios +con deficiencias visuales. + +00:00:28.000 --> 00:00:31.500 +Finalmente, implementaremos patrones +de navegación por teclado que permiten + +00:00:31.500 --> 00:00:35.000 +a los usuarios interactuar con nuestras +interfaces sin usar un ratón. \ No newline at end of file diff --git a/backend/tests/fixtures/sample_ingestion.json b/backend/tests/fixtures/sample_ingestion.json new file mode 100644 index 0000000..a211a59 --- /dev/null +++ b/backend/tests/fixtures/sample_ingestion.json @@ -0,0 +1,8 @@ +{ + "language": "en", + "confidence": 0.92, + "summary": "A comprehensive tutorial about web accessibility features, demonstrating best practices for inclusive design and development.", + "transcript_plaintext": "Hello everyone, and welcome to today's tutorial on web accessibility. In this video, we'll explore the essential features that make websites accessible to users with disabilities. First, let's discuss the importance of semantic HTML elements. When we use proper heading structures, screen readers can navigate content more effectively. Next, we'll cover color contrast requirements and how to ensure text remains readable for users with visual impairments. Finally, we'll implement keyboard navigation patterns that allow users to interact with our interfaces without using a mouse.", + "captions_vtt": "WEBVTT\n\n00:00:01.000 --> 00:00:04.500\nHello everyone, and welcome to today's\ntutorial on web accessibility.\n\n00:00:04.500 --> 00:00:08.000\nIn this video, we'll explore the essential\nfeatures that make websites accessible\n\n00:00:08.000 --> 00:00:10.500\nto users with disabilities.\n\n00:00:11.000 --> 00:00:14.500\nFirst, let's discuss the importance\nof semantic HTML elements.\n\n00:00:14.500 --> 00:00:18.000\nWhen we use proper heading structures,\nscreen readers can navigate content\n\n00:00:18.000 --> 00:00:20.000\nmore effectively.\n\n00:00:20.500 --> 00:00:24.000\nNext, we'll cover color contrast\nrequirements and how to ensure\n\n00:00:24.000 --> 00:00:27.500\ntext remains readable for users\nwith visual impairments.\n\n00:00:28.000 --> 00:00:31.500\nFinally, we'll implement keyboard\nnavigation patterns that allow users\n\n00:00:31.500 --> 00:00:35.000\nto interact with our interfaces\nwithout using a mouse.", + "audio_description_vtt": "WEBVTT\n\n00:00:00.500 --> 00:00:01.000\n[Soft intro music fades in]\n\n00:00:01.000 --> 00:00:04.500\n[Speaker sits at modern desk with laptop,\nbright office setting]\n\n00:00:10.500 --> 00:00:11.000\n[Screen displays HTML code example]\n\n00:00:14.000 --> 00:00:14.500\n[Cursor highlights heading tags h1, h2, h3]\n\n00:00:18.000 --> 00:00:20.500\n[Animation shows screen reader navigation]\n\n00:00:20.000 --> 00:00:20.500\n[Color palette appears on screen]\n\n00:00:24.500 --> 00:00:28.000\n[Contrast checker tool demonstrates\ntext readability differences]\n\n00:00:27.500 --> 00:00:28.000\n[Speaker gestures toward keyboard]\n\n00:00:31.000 --> 00:00:31.500\n[Tab navigation highlights interactive elements]\n\n00:00:34.500 --> 00:00:35.000\n[Video fades to end screen with resources]" +} \ No newline at end of file diff --git a/backend/tests/unit/test_emailer.py b/backend/tests/unit/test_emailer.py new file mode 100644 index 0000000..579afba --- /dev/null +++ b/backend/tests/unit/test_emailer.py @@ -0,0 +1,241 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from app.services.emailer import EmailService + + +class TestEmailService: + """Test email service functionality""" + + @pytest.fixture + def email_service(self): + """Create email service with mocked SendGrid client""" + with patch('app.services.emailer.settings') as mock_settings: + mock_settings.sendgrid_api_key = "test_api_key" + mock_settings.email_from = "support@example.com" + + with patch('app.services.emailer.SendGridAPIClient') as mock_client: + service = EmailService() + service.client = MagicMock() + return service + + @pytest.fixture + def sample_download_links(self): + """Sample download links for testing""" + return { + "en": { + "captions_vtt": "https://signed-url.example.com/en/captions.vtt", + "audio_description_vtt": "https://signed-url.example.com/en/ad.vtt", + "audio_description_mp3": "https://signed-url.example.com/en/ad.mp3" + }, + "es": { + "captions_vtt": "https://signed-url.example.com/es/captions.vtt", + "audio_description_vtt": "https://signed-url.example.com/es/ad.vtt", + "audio_description_mp3": "https://signed-url.example.com/es/ad.mp3" + } + } + + @pytest.mark.asyncio + async def test_send_completion_email_success(self, email_service, sample_download_links): + """Test successful completion email sending""" + # Mock successful SendGrid response + mock_response = MagicMock() + mock_response.status_code = 202 + email_service.client.send.return_value = mock_response + + result = await email_service.send_completion_email( + recipient_email="client@example.com", + job_title="Test Video Project", + download_links=sample_download_links + ) + + assert result is True + email_service.client.send.assert_called_once() + + @pytest.mark.asyncio + async def test_send_completion_email_no_client(self): + """Test email sending when client is not configured""" + service = EmailService() + service.client = None + + result = await service.send_completion_email( + recipient_email="client@example.com", + job_title="Test Video", + download_links={} + ) + + assert result is False + + @pytest.mark.asyncio + async def test_send_completion_email_api_failure(self, email_service, sample_download_links): + """Test email sending with SendGrid API failure""" + # Mock failed SendGrid response + mock_response = MagicMock() + mock_response.status_code = 400 + email_service.client.send.return_value = mock_response + + result = await email_service.send_completion_email( + recipient_email="client@example.com", + job_title="Test Video", + download_links=sample_download_links + ) + + assert result is False + + @pytest.mark.asyncio + async def test_send_completion_email_exception(self, email_service, sample_download_links): + """Test email sending with exception""" + # Mock SendGrid client raising exception + email_service.client.send.side_effect = Exception("SendGrid error") + + result = await email_service.send_completion_email( + recipient_email="client@example.com", + job_title="Test Video", + download_links=sample_download_links + ) + + assert result is False + + def test_render_completion_template_basic(self, email_service, sample_download_links): + """Test rendering completion email template""" + html_content = email_service._render_completion_template( + job_title="Test Video Project", + download_links=sample_download_links + ) + + # Check that key elements are present + assert "Test Video Project" in html_content + assert "EN Assets" in html_content + assert "ES Assets" in html_content + assert "captions.vtt" in html_content + assert "audio_description_vtt" in html_content + assert "audio_description_mp3" in html_content + assert "24 hours" in html_content # Expiry warning + + def test_render_completion_template_single_language(self, email_service): + """Test rendering template with single language""" + download_links = { + "en": { + "captions_vtt": "https://example.com/captions.vtt" + } + } + + html_content = email_service._render_completion_template( + job_title="English Only Video", + download_links=download_links + ) + + assert "English Only Video" in html_content + assert "EN Assets" in html_content + assert "ES Assets" not in html_content + + def test_render_completion_template_no_downloads(self, email_service): + """Test rendering template with no download links""" + html_content = email_service._render_completion_template( + job_title="Empty Job", + download_links={} + ) + + assert "Empty Job" in html_content + assert "" in html_content + assert "24 hours" in html_content + + def test_render_completion_template_html_structure(self, email_service, sample_download_links): + """Test that rendered template has proper HTML structure""" + html_content = email_service._render_completion_template( + job_title="Test Video", + download_links=sample_download_links + ) + + # Check HTML structure + assert html_content.startswith("") + assert "" in html_content + assert "" in html_content + assert "" in html_content + assert "" in html_content + assert "font-family: Arial" in html_content # CSS present + + def test_render_completion_template_download_link_formatting(self, email_service): + """Test that download links are properly formatted in template""" + download_links = { + "en": { + "captions_vtt": "https://example.com/captions.vtt", + "audio_description_mp3": "https://example.com/ad.mp3" + } + } + + html_content = email_service._render_completion_template( + job_title="Test Video", + download_links=download_links + ) + + # Check that file types are properly formatted + assert "Download Captions Vtt" in html_content + assert "Download Audio Description Mp3" in html_content + assert 'href="https://example.com/captions.vtt"' in html_content + assert 'href="https://example.com/ad.mp3"' in html_content + + def test_service_initialization_with_api_key(self): + """Test service initialization with SendGrid API key""" + with patch('app.services.emailer.settings') as mock_settings: + mock_settings.sendgrid_api_key = "test_api_key" + + with patch('app.services.emailer.SendGridAPIClient') as mock_client: + service = EmailService() + + mock_client.assert_called_once_with(api_key="test_api_key") + assert service.client is not None + + def test_service_initialization_without_api_key(self): + """Test service initialization without SendGrid API key""" + with patch('app.services.emailer.settings') as mock_settings: + mock_settings.sendgrid_api_key = "" + + service = EmailService() + + assert service.client is None + + @pytest.mark.asyncio + async def test_send_completion_email_mail_object_creation(self, email_service, sample_download_links): + """Test that Mail object is created correctly""" + mock_response = MagicMock() + mock_response.status_code = 202 + email_service.client.send.return_value = mock_response + + with patch('app.services.emailer.Mail') as mock_mail: + mock_mail_instance = MagicMock() + mock_mail.return_value = mock_mail_instance + + await email_service.send_completion_email( + recipient_email="client@example.com", + job_title="Test Video", + download_links=sample_download_links + ) + + # Verify Mail object was created with correct parameters + mock_mail.assert_called_once() + call_args = mock_mail.call_args + + # Check that from_email, to_emails, subject, and html_content are set + assert call_args is not None + email_service.client.send.assert_called_once_with(mock_mail_instance) + + def test_template_injection_safety(self, email_service): + """Test that template is safe from injection attacks""" + malicious_title = "Malicious Title" + malicious_links = { + "en": { + "captions_vtt": "javascript:alert('xss')" + } + } + + html_content = email_service._render_completion_template( + job_title=malicious_title, + download_links=malicious_links + ) + + # Jinja2 should escape HTML by default + assert " + + diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..ddce7d4 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,76 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log; + + # Performance + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA routing - serve index.html for all non-file routes + location / { + try_files $uri $uri/ /index.html; + + # No cache for HTML files + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Security headers for all responses + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.accessible-video.com https://*.googleapis.com; frame-ancestors 'none';" always; + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..417c2ba --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,6623 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@sentry/react": "^8.0.0", + "@tailwindcss/postcss": "^4.1.12", + "@tanstack/react-query": "^5.85.3", + "@tanstack/react-query-devtools": "^5.85.3", + "@types/node": "^24.3.0", + "axios": "^1.11.0", + "date-fns": "^4.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-dropzone": "^14.3.8", + "react-hook-form": "^7.62.0", + "react-router-dom": "^7.8.1", + "zod": "^4.0.17", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@playwright/test": "^1.54.2", + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "@vitest/coverage-v8": "^3.2.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "jsdom": "^26.1.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.12", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.1.tgz", + "integrity": "sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.2.tgz", + "integrity": "sha512-A+znathYxPf+72riFd1r1ovOLqsIIB0jKIoPjyK2kqEIe30/6jF6BC7QNluHuwUmsD2tv1XZVugN8GqfTMOxsA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz", + "integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.55.0.tgz", + "integrity": "sha512-ROgqtQfpH/82AQIpESPqPQe0UyWywKJsmVIqi3c5Fh+zkds5LUxnssTj3yNd1x+kxaPDVB023jAP+3ibNgeNDw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.55.0.tgz", + "integrity": "sha512-cP3BD/Q6pquVQ+YL+rwCnorKuTXiS9KXW8HNKu4nmmBAyf7urjs+F6Hr1k9MXP5yQ8W3yK7jRWd09Yu6DHWOiw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.55.0.tgz", + "integrity": "sha512-roCDEGkORwolxBn8xAKedybY+Jlefq3xYmgN2fr3BTnsXjSYOPC7D1/mYqINBat99nDtvgFvNfRcZPiwwZ1hSw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.55.0.tgz", + "integrity": "sha512-nIkfgRWk1091zHdu4NbocQsxZF1rv1f7bbp3tTIlZYbrH62XVZosx5iHAuZG0Zc48AETLE7K4AX9VGjvQj8i9w==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/browser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.55.0.tgz", + "integrity": "sha512-1A31mCEWCjaMxJt6qGUK+aDnLDcK6AwLAZnqpSchNysGni1pSn1RWSmk9TBF8qyTds5FH8B31H480uxMPUJ7Cw==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "8.55.0", + "@sentry-internal/feedback": "8.55.0", + "@sentry-internal/replay": "8.55.0", + "@sentry-internal/replay-canvas": "8.55.0", + "@sentry/core": "8.55.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/core": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.55.0.tgz", + "integrity": "sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==", + "license": "MIT", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.55.0.tgz", + "integrity": "sha512-/qNBvFLpvSa/Rmia0jpKfJdy16d4YZaAnH/TuKLAtm0BWlsPQzbXCU4h8C5Hsst0Do0zG613MEtEmWpWrVOqWA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "8.55.0", + "@sentry/core": "8.55.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.12.tgz", + "integrity": "sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.12.tgz", + "integrity": "sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-arm64": "4.1.12", + "@tailwindcss/oxide-darwin-x64": "4.1.12", + "@tailwindcss/oxide-freebsd-x64": "4.1.12", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.12", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.12", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.12", + "@tailwindcss/oxide-linux-x64-musl": "4.1.12", + "@tailwindcss/oxide-wasm32-wasi": "4.1.12", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.12", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.12" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.12.tgz", + "integrity": "sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.12.tgz", + "integrity": "sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.12.tgz", + "integrity": "sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.12.tgz", + "integrity": "sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.12.tgz", + "integrity": "sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.12.tgz", + "integrity": "sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.12.tgz", + "integrity": "sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.12.tgz", + "integrity": "sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.12.tgz", + "integrity": "sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.12.tgz", + "integrity": "sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.12.tgz", + "integrity": "sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.12.tgz", + "integrity": "sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.12.tgz", + "integrity": "sha512-5PpLYhCAwf9SJEeIsSmCDLgyVfdBhdBpzX1OJ87anT9IVR0Z9pjM0FNixCAUAHGnMBGB8K99SwAheXrT0Kh6QQ==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.12", + "@tailwindcss/oxide": "4.1.12", + "postcss": "^8.4.41", + "tailwindcss": "4.1.12" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.85.3", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.85.3.tgz", + "integrity": "sha512-9Ne4USX83nHmRuEYs78LW+3lFEEO2hBDHu7mrdIgAFx5Zcrs7ker3n/i8p4kf6OgKExmaDN5oR0efRD7i2J0DQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.84.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz", + "integrity": "sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.85.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.85.3.tgz", + "integrity": "sha512-AqU8TvNh5GVIE8I+TUU0noryBRy7gOY0XhSayVXmOPll4UkZeLWKDwi0rtWOZbwLRCbyxorfJ5DIjDqE7GXpcQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.85.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.85.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.85.3.tgz", + "integrity": "sha512-WSVweCE1Kh1BVvPDHAmLgGT+GGTJQ9+a7bVqzD+zUiUTht+salJjYm5nikpMNaHFPJV102TCYdvgHgBXtURRNg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.84.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.85.3", + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.7.0.tgz", + "integrity": "sha512-RI2e97YZ7MRa+vxP4UUnMuMFL2buSsf0ollxUbTgrbPLKhMn8KVTx7raS6DYjC7v1NDVrioOvaShxsguLNISCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", + "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.39.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.0.tgz", + "integrity": "sha512-Jx9JfsTa05bYkS9xo0hkofp2dCmp1blrKjw9JONs5BTHOvJCgLbaPSuZLGSVJW6u2qe0tc4eevY0+gSNNi0YCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.30", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz", + "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.29", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/attr-accept": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz", + "integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.2.tgz", + "integrity": "sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001733", + "electron-to-chromium": "^1.5.199", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001735", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001735.tgz", + "integrity": "sha512-EV/laoX7Wq2J9TQlyIXRxTJqIw4sxfXS4OYgudGxBYRuTv0q7AM6yMEpU/Vo1I94thg9U6EZ2NfZx9GJq83u7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.203", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.203.tgz", + "integrity": "sha512-uz4i0vLhfm6dLZWbz/iH88KNDV+ivj5+2SA+utpgjKaj9Q0iDLuwk6Idhe9BTxciHudyx6IvTvijhkPvFGUQ0g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-selector": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", + "integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==", + "license": "MIT", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.2.tgz", + "integrity": "sha512-Hu/BMoA1NAdRUuulyvQC0pEqZ4vQbGfn8f7wPXcnqQmM+zct9UliKxsIkLNmz/ku7LElUNqmaiv1TG/aL5ACsw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.2.tgz", + "integrity": "sha512-n5r4HFbMmWsB4twG7tJLDN9gmBUeSPcsBZiWSE4DnYz9mJMAFqr2ID7+eGC9kpEnxExJ1epttwR59LEWCk8mtA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-dropzone": { + "version": "14.3.8", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", + "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "license": "MIT", + "dependencies": { + "attr-accept": "^2.2.4", + "file-selector": "^2.1.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.62.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", + "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz", + "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.46.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.46.2", + "@rollup/rollup-android-arm64": "4.46.2", + "@rollup/rollup-darwin-arm64": "4.46.2", + "@rollup/rollup-darwin-x64": "4.46.2", + "@rollup/rollup-freebsd-arm64": "4.46.2", + "@rollup/rollup-freebsd-x64": "4.46.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", + "@rollup/rollup-linux-arm-musleabihf": "4.46.2", + "@rollup/rollup-linux-arm64-gnu": "4.46.2", + "@rollup/rollup-linux-arm64-musl": "4.46.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", + "@rollup/rollup-linux-ppc64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-gnu": "4.46.2", + "@rollup/rollup-linux-riscv64-musl": "4.46.2", + "@rollup/rollup-linux-s390x-gnu": "4.46.2", + "@rollup/rollup-linux-x64-gnu": "4.46.2", + "@rollup/rollup-linux-x64-musl": "4.46.2", + "@rollup/rollup-win32-arm64-msvc": "4.46.2", + "@rollup/rollup-win32-ia32-msvc": "4.46.2", + "@rollup/rollup-win32-x64-msvc": "4.46.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.12.tgz", + "integrity": "sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz", + "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.2.tgz", + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.0.17", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.0.17.tgz", + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.7.tgz", + "integrity": "sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..5252987 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,56 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "type-check": "tsc --noEmit", + "test": "vitest", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.1", + "@sentry/react": "^8.0.0", + "@tailwindcss/postcss": "^4.1.12", + "@tanstack/react-query": "^5.85.3", + "@tanstack/react-query-devtools": "^5.85.3", + "@types/node": "^24.3.0", + "axios": "^1.11.0", + "date-fns": "^4.1.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-dropzone": "^14.3.8", + "react-hook-form": "^7.62.0", + "react-router-dom": "^7.8.1", + "zod": "^4.0.17", + "zustand": "^5.0.7" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@playwright/test": "^1.54.2", + "@testing-library/jest-dom": "^6.7.0", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "@vitest/coverage-v8": "^3.2.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "jsdom": "^26.1.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.12", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2", + "vitest": "^3.2.4" + } +} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..0a6443c --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + timeout: 30 * 1000, + expect: { + timeout: 5000 + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: [ + { + command: 'npm run dev', + port: 3000, + reuseExistingServer: !process.env.CI, + }, + { + command: 'cd ../backend && poetry run uvicorn app.main:app --host 127.0.0.1 --port 8000', + port: 8000, + reuseExistingServer: !process.env.CI, + env: { + APP_ENV: 'test', + MONGODB_URI: 'mongodb://localhost:27017', + MONGODB_DB: 'test_accessible_video_e2e', + REDIS_URL: 'redis://localhost:6379', + JWT_SECRET: 'test_secret_for_e2e', + GEMINI_API_KEY: 'fake_key_for_e2e', + GCP_PROJECT_ID: 'test-project', + GCS_BUCKET: 'test-bucket', + SENDGRID_API_KEY: 'fake_sendgrid', + EMAIL_FROM: 'test@example.com', + CLIENT_BASE_URL: 'http://localhost:3000', + }, + }, + ], +}); \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..af9d8dc --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..aefbaa7 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,117 @@ +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { queryClient } from './lib/queryClient'; +import './styles/index.css'; + +// Import route components (will be implemented in next phases) +import { Dashboard } from './routes/Dashboard'; +import { Login } from './routes/Login'; +import { NewJob } from './routes/jobs/NewJob'; +import { JobsList } from './routes/jobs/JobsList'; +import { JobDetail } from './routes/jobs/JobDetail'; +import { QCList } from './routes/admin/QCList'; +import { QCDetail } from './routes/admin/QCDetail'; +import { FinalList } from './routes/admin/FinalList'; +import { FinalDetail } from './routes/admin/FinalDetail'; +import { Downloads } from './routes/Downloads'; +import { RequireAuth } from './components/Auth/RequireAuth'; +import { RoleGate } from './components/Auth/RoleGate'; +import { ErrorBoundary } from './components/ErrorBoundary'; +import { ToastContainer } from './components/Toast/Toast'; +import { ToastProvider, useToastContext } from './contexts/ToastContext'; +import { Layout } from './components/Layout/Layout'; + +// Helper component to wrap authenticated routes with Layout +function AuthenticatedRoute({ children }: { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} + +function AppContent() { + const { toasts, removeToast } = useToastContext(); + + return ( + +
+ + } /> + + + + } /> + + + + } /> + + + + } /> + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + + + } /> + + + + } /> + + +
+
+ ); +} + +function App() { + return ( + + + + + + + + + ); +} + +export default App; diff --git a/frontend/src/__tests__/basic.test.ts b/frontend/src/__tests__/basic.test.ts new file mode 100644 index 0000000..48e5f4e --- /dev/null +++ b/frontend/src/__tests__/basic.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' + +describe('Basic Test Setup', () => { + it('should run basic tests', () => { + expect(1 + 1).toBe(2) + }) + + it('should have access to vi globals', () => { + expect(vi).toBeDefined() + }) +}) \ No newline at end of file diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Auth/RequireAuth.tsx b/frontend/src/components/Auth/RequireAuth.tsx new file mode 100644 index 0000000..9e10add --- /dev/null +++ b/frontend/src/components/Auth/RequireAuth.tsx @@ -0,0 +1,25 @@ +import { ReactNode } from 'react'; +import { Navigate } from 'react-router-dom'; +import { useAuthStore } from '../../lib/auth'; + +interface RequireAuthProps { + children: ReactNode; +} + +export function RequireAuth({ children }: RequireAuthProps) { + const { isAuthenticated, isLoading } = useAuthStore(); + + if (isLoading) { + return ( +
+
+
+ ); + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} \ No newline at end of file diff --git a/frontend/src/components/Auth/RoleGate.tsx b/frontend/src/components/Auth/RoleGate.tsx new file mode 100644 index 0000000..56f1833 --- /dev/null +++ b/frontend/src/components/Auth/RoleGate.tsx @@ -0,0 +1,36 @@ +import { ReactNode } from 'react'; +import { useAuthStore } from '../../lib/auth'; +import type { UserRole } from '../../types/api'; + +interface RoleGateProps { + children: ReactNode; + allowedRoles: UserRole[]; +} + +export function RoleGate({ children, allowedRoles }: RoleGateProps) { + const { user } = useAuthStore(); + + if (!user) { + return ( +
+
+

Access Denied

+

You don't have permission to access this page.

+
+
+ ); + } + + if (!allowedRoles.includes(user.role) && user.role !== 'admin') { + return ( +
+
+

Access Denied

+

You don't have permission to access this page.

+
+
+ ); + } + + return <>{children}; +} \ No newline at end of file diff --git a/frontend/src/components/Auth/__tests__/RequireAuth.test.tsx b/frontend/src/components/Auth/__tests__/RequireAuth.test.tsx new file mode 100644 index 0000000..ddaff3d --- /dev/null +++ b/frontend/src/components/Auth/__tests__/RequireAuth.test.tsx @@ -0,0 +1,134 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, waitFor } from '../../../test/utils' +import { RequireAuth } from '../RequireAuth' +import { useAuthStore } from '../../../lib/auth' +import { UserRole } from '../../../types/api' +import { createMockUser } from '../../../test/utils' + +// Mock the auth store +vi.mock('../../../lib/auth', () => ({ + useAuthStore: vi.fn() +})) + +const mockUseAuthStore = vi.mocked(useAuthStore) + +describe('RequireAuth', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders children when user is authenticated', () => { + const mockUser = createMockUser() + mockUseAuthStore.mockReturnValue({ + isAuthenticated: true, + isLoading: false, + refreshAuth: vi.fn() + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText('Protected content')).toBeInTheDocument() + }) + + it('shows loading spinner when loading', () => { + mockUseAuthStore.mockReturnValue({ + isAuthenticated: false, + isLoading: true, + refreshAuth: vi.fn() + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText((content, element) => + element?.classList.contains('animate-spin') || false + )).toBeInTheDocument() // Loading spinner + expect(screen.queryByText('Protected content')).not.toBeInTheDocument() + }) + + it('attempts to refresh auth when not authenticated', async () => { + const mockRefreshAuth = vi.fn().mockResolvedValue(undefined) + + mockUseAuthStore.mockReturnValue({ + isAuthenticated: false, + isLoading: false, + refreshAuth: mockRefreshAuth + }) + + render( + +
Protected content
+
+ ) + + await waitFor(() => { + expect(mockRefreshAuth).toHaveBeenCalled() + }) + }) + + it('handles refresh auth failure gracefully', async () => { + const mockRefreshAuth = vi.fn().mockRejectedValue(new Error('Refresh failed')) + + mockUseAuthStore.mockReturnValue({ + isAuthenticated: false, + isLoading: false, + refreshAuth: mockRefreshAuth + }) + + render( + +
Protected content
+
+ ) + + await waitFor(() => { + expect(mockRefreshAuth).toHaveBeenCalled() + }) + + // Should not crash on refresh failure + expect(screen.queryByText('Protected content')).not.toBeInTheDocument() + }) + + it('does not call refreshAuth multiple times when already loading', () => { + const mockRefreshAuth = vi.fn().mockResolvedValue(undefined) + + mockUseAuthStore.mockReturnValue({ + isAuthenticated: false, + isLoading: true, + refreshAuth: mockRefreshAuth + }) + + render( + +
Protected content
+
+ ) + + expect(mockRefreshAuth).not.toHaveBeenCalled() + }) + + it('does not call refreshAuth when already authenticated', () => { + const mockRefreshAuth = vi.fn() + + mockUseAuthStore.mockReturnValue({ + isAuthenticated: true, + isLoading: false, + refreshAuth: mockRefreshAuth + }) + + render( + +
Protected content
+
+ ) + + expect(mockRefreshAuth).not.toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/frontend/src/components/Auth/__tests__/RoleGate.test.tsx b/frontend/src/components/Auth/__tests__/RoleGate.test.tsx new file mode 100644 index 0000000..4acae1a --- /dev/null +++ b/frontend/src/components/Auth/__tests__/RoleGate.test.tsx @@ -0,0 +1,135 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '../../../test/utils' +import { RoleGate } from '../RoleGate' +import { useAuthStore } from '../../../lib/auth' +import type { UserRole } from '../../../types/api' +import { createMockUser } from '../../../test/utils' + +// Mock the auth store +vi.mock('../../../lib/auth', () => ({ + useAuthStore: vi.fn() +})) + +const mockUseAuthStore = vi.mocked(useAuthStore) + +describe('RoleGate', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('renders children when user has allowed role', () => { + const mockUser = createMockUser({ role: 'reviewer' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText('Protected content')).toBeInTheDocument() + }) + + it('renders children when user is admin (always allowed)', () => { + const mockUser = createMockUser({ role: 'admin' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText('Protected content')).toBeInTheDocument() + }) + + it('shows access denied when user role is not allowed', () => { + const mockUser = createMockUser({ role: 'client' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText('Access Denied')).toBeInTheDocument() + expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument() + expect(screen.queryByText('Protected content')).not.toBeInTheDocument() + }) + + it('shows access denied when no user is present', () => { + mockUseAuthStore.mockReturnValue({ + user: null + }) + + render( + +
Protected content
+
+ ) + + expect(screen.getByText('Access Denied')).toBeInTheDocument() + expect(screen.getByText("You don't have permission to access this page.")).toBeInTheDocument() + expect(screen.queryByText('Protected content')).not.toBeInTheDocument() + }) + + it('allows access when user role is in allowed roles list', () => { + const mockUser = createMockUser({ role: 'reviewer' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Multi-role protected content
+
+ ) + + expect(screen.getByText('Multi-role protected content')).toBeInTheDocument() + }) + + it('denies access when user role is not in allowed roles list', () => { + const mockUser = createMockUser({ role: 'client' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Reviewer only content
+
+ ) + + expect(screen.getByText('Access Denied')).toBeInTheDocument() + expect(screen.queryByText('Reviewer only content')).not.toBeInTheDocument() + }) + + it('has consistent styling for access denied message', () => { + const mockUser = createMockUser({ role: 'client' as UserRole }) + mockUseAuthStore.mockReturnValue({ + user: mockUser + }) + + render( + +
Admin content
+
+ ) + + const container = screen.getByText('Access Denied').closest('.min-h-screen')! + expect(container).toHaveClass('min-h-screen', 'flex', 'items-center', 'justify-center') + + const heading = screen.getByText('Access Denied') + expect(heading).toHaveClass('text-2xl', 'font-bold', 'text-gray-900', 'mb-4') + + const description = screen.getByText("You don't have permission to access this page.") + expect(description).toHaveClass('text-gray-600') + }) +}) \ No newline at end of file diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..ab3e68b --- /dev/null +++ b/frontend/src/components/ErrorBoundary.tsx @@ -0,0 +1,88 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error boundary caught an error:', error, errorInfo); + + // Send to error reporting service + if (import.meta.env.VITE_SENTRY_DSN) { + // Would integrate with Sentry here + console.error('Error details:', { error, errorInfo }); + } + } + + render() { + if (this.state.hasError) { + return ( +
+
+
+
+ +

+ Something went wrong +

+

+ An unexpected error occurred. Please refresh the page and try again. +

+ {import.meta.env.DEV && this.state.error && ( +
+ + Error Details (Development) + +
+                      {this.state.error.message}
+                      {'\n'}
+                      {this.state.error.stack}
+                    
+
+ )} +
+ +
+
+
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/frontend/src/components/Layout/Layout.tsx b/frontend/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..8f0784d --- /dev/null +++ b/frontend/src/components/Layout/Layout.tsx @@ -0,0 +1,51 @@ +import { ReactNode, useState } from 'react'; +import { Sidebar } from './Sidebar'; +import { Navbar } from './Navbar'; + +interface LayoutProps { + children: ReactNode; +} + +export function Layout({ children }: LayoutProps) { + const [showMobileSidebar, setShowMobileSidebar] = useState(false); + + return ( +
+ {/* Desktop Sidebar */} +
+ +
+ + {/* Mobile Sidebar Overlay */} + {showMobileSidebar && ( +
+
setShowMobileSidebar(false)} /> +
+
+ +
+ setShowMobileSidebar(false)} /> +
+
+ )} + + {/* Main Content */} +
+ {/* Top Navigation */} + setShowMobileSidebar(true)} /> + + {/* Page Content */} +
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/Layout/Navbar.tsx b/frontend/src/components/Layout/Navbar.tsx new file mode 100644 index 0000000..179d6d6 --- /dev/null +++ b/frontend/src/components/Layout/Navbar.tsx @@ -0,0 +1,188 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; +import { useAuthStore } from '../../lib/auth'; + +interface NavbarProps { + onMobileMenuClick?: () => void; +} + +export function Navbar({ onMobileMenuClick }: NavbarProps) { + const { user, logout } = useAuthStore(); + const [showUserMenu, setShowUserMenu] = useState(false); + const [showNotifications, setShowNotifications] = useState(false); + + const handleLogout = async () => { + await logout(); + setShowUserMenu(false); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Layout/Sidebar.tsx b/frontend/src/components/Layout/Sidebar.tsx new file mode 100644 index 0000000..8ab1af9 --- /dev/null +++ b/frontend/src/components/Layout/Sidebar.tsx @@ -0,0 +1,125 @@ +import { Link, useLocation } from 'react-router-dom'; +import { useAuthStore } from '../../lib/auth'; + +interface SidebarItem { + label: string; + href: string; + icon: string; + roles?: string[]; + badge?: number; +} + +interface SidebarProps { + onMobileClose?: () => void; +} + +export function Sidebar({ onMobileClose }: SidebarProps) { + const { user } = useAuthStore(); + const location = useLocation(); + + const sidebarItems: SidebarItem[] = [ + { + label: 'Dashboard', + href: '/', + icon: '🏠', + }, + { + label: 'All Jobs', + href: '/jobs', + icon: '📋', + }, + { + label: 'Upload Video', + href: '/jobs/new', + icon: '📤', + roles: ['client'], + }, + { + label: 'QC Review', + href: '/admin/qc', + icon: '🔍', + roles: ['reviewer', 'admin'], + }, + { + label: 'Final Review', + href: '/admin/final', + icon: '✅', + roles: ['reviewer', 'admin'], + }, + ]; + + const filteredItems = sidebarItems.filter(item => + !item.roles || item.roles.includes(user?.role || '') + ); + + const isActive = (href: string) => { + if (href === '/') { + return location.pathname === '/'; + } + return location.pathname.startsWith(href); + }; + + return ( +
+ {/* Logo/Brand */} +
+
+
+ VA +
+
+

Video Access

+

Accessibility Platform

+
+
+
+ + {/* Navigation */} + + + {/* User Info */} +
+
+
+ + {user?.email?.charAt(0).toUpperCase()} + +
+
+

+ {user?.email} +

+

+ {user?.role} +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/StatusBadge.tsx b/frontend/src/components/StatusBadge.tsx new file mode 100644 index 0000000..8b9a1e1 --- /dev/null +++ b/frontend/src/components/StatusBadge.tsx @@ -0,0 +1,67 @@ +import type { JobStatus } from '../types/api'; + +interface StatusBadgeProps { + status: JobStatus; +} + +export function StatusBadge({ status }: StatusBadgeProps) { + const getStatusStyles = (status: JobStatus) => { + switch (status) { + case 'created': + return 'bg-gray-100 text-gray-800'; + case 'ingesting': + return 'bg-blue-100 text-blue-800'; + case 'ai_processing': + return 'bg-purple-100 text-purple-800'; + case 'pending_qc': + return 'bg-yellow-100 text-yellow-800'; + case 'approved_english': + return 'bg-green-100 text-green-800'; + case 'rejected': + return 'bg-red-100 text-red-800'; + case 'translating': + return 'bg-blue-100 text-blue-800'; + case 'tts_generating': + return 'bg-indigo-100 text-indigo-800'; + case 'pending_final_review': + return 'bg-orange-100 text-orange-800'; + case 'completed': + return 'bg-green-100 text-green-800'; + default: + return 'bg-gray-100 text-gray-800'; + } + }; + + const getStatusLabel = (status: JobStatus) => { + switch (status) { + case 'created': + return 'Created'; + case 'ingesting': + return 'Ingesting'; + case 'ai_processing': + return 'AI Processing'; + case 'pending_qc': + return 'Pending QC'; + case 'approved_english': + return 'Approved English'; + case 'rejected': + return 'Rejected'; + case 'translating': + return 'Translating'; + case 'tts_generating': + return 'Generating Audio'; + case 'pending_final_review': + return 'Pending Final Review'; + case 'completed': + return 'Completed'; + default: + return status; + } + }; + + return ( + + {getStatusLabel(status)} + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Toast/Toast.tsx b/frontend/src/components/Toast/Toast.tsx new file mode 100644 index 0000000..7282598 --- /dev/null +++ b/frontend/src/components/Toast/Toast.tsx @@ -0,0 +1,121 @@ +import { useEffect, useState } from 'react'; + +export interface ToastMessage { + id: string; + message: string; + type: 'success' | 'error' | 'warning' | 'info'; + duration?: number; +} + +interface ToastProps { + toast: ToastMessage; + onRemove: (id: string) => void; +} + +export function Toast({ toast, onRemove }: ToastProps) { + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + // Fade in + const fadeInTimer = setTimeout(() => setIsVisible(true), 100); + + // Auto remove + const removeTimer = setTimeout(() => { + onRemove(toast.id); + }, toast.duration || 4000); + + return () => { + clearTimeout(fadeInTimer); + clearTimeout(removeTimer); + }; + }, [toast.id, toast.duration, onRemove]); + + const handleClose = () => { + setIsVisible(false); + setTimeout(() => onRemove(toast.id), 300); + }; + + const getToastStyles = () => { + const baseStyles = "flex items-center p-4 rounded-lg shadow-lg border transition-all duration-300 transform max-w-md"; + + if (!isVisible) { + return `${baseStyles} opacity-0 translate-y-2`; + } + + switch (toast.type) { + case 'success': + return `${baseStyles} bg-green-50 border-green-200 text-green-800`; + case 'error': + return `${baseStyles} bg-red-50 border-red-200 text-red-800`; + case 'warning': + return `${baseStyles} bg-yellow-50 border-yellow-200 text-yellow-800`; + case 'info': + return `${baseStyles} bg-blue-50 border-blue-200 text-blue-800`; + default: + return `${baseStyles} bg-gray-50 border-gray-200 text-gray-800`; + } + }; + + const getIcon = () => { + switch (toast.type) { + case 'success': + return ( + + + + ); + case 'error': + return ( + + + + ); + case 'warning': + return ( + + + + ); + case 'info': + return ( + + + + ); + default: + return null; + } + }; + + return ( +
+ {getIcon()} +

{toast.message}

+ +
+ ); +} + +interface ToastContainerProps { + toasts: ToastMessage[]; + onRemove: (id: string) => void; +} + +export function ToastContainer({ toasts, onRemove }: ToastContainerProps) { + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map(toast => ( + + ))} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/UploadDropzone/UploadDropzone.tsx b/frontend/src/components/UploadDropzone/UploadDropzone.tsx new file mode 100644 index 0000000..f33ca6d --- /dev/null +++ b/frontend/src/components/UploadDropzone/UploadDropzone.tsx @@ -0,0 +1,93 @@ +import { useState, useCallback } from 'react'; +import { useDropzone } from 'react-dropzone'; + +interface UploadDropzoneProps { + onFileSelect: (file: File) => void; + accept?: Record; + maxSize?: number; + disabled?: boolean; +} + +export function UploadDropzone({ + onFileSelect, + accept = { 'video/*': ['.mp4', '.mov', '.avi', '.mkv'] }, + maxSize = 1024 * 1024 * 1024, // 1GB + disabled = false +}: UploadDropzoneProps) { + const [error, setError] = useState(null); + + const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: { file: File; errors: { code: string }[] }[]) => { + setError(null); + + if (rejectedFiles.length > 0) { + const rejection = rejectedFiles[0]; + if (rejection.errors.some((e) => e.code === 'file-too-large')) { + setError(`File is too large. Maximum size is ${Math.round(maxSize / (1024 * 1024))}MB`); + } else if (rejection.errors.some((e) => e.code === 'file-invalid-type')) { + setError('Invalid file type. Please upload a video file.'); + } else { + setError('File upload failed. Please try again.'); + } + return; + } + + if (acceptedFiles.length > 0) { + onFileSelect(acceptedFiles[0]); + } + }, [onFileSelect, maxSize]); + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + onDrop, + accept, + maxSize, + multiple: false, + disabled + }); + + return ( +
+
+ + +
+
+ + + +
+ + {isDragActive ? ( +

Drop the video file here

+ ) : ( +
+

+ Drag and drop a video file here, or click to select +

+

+ Supports MP4, MOV, AVI, MKV up to {Math.round(maxSize / (1024 * 1024))}MB +

+
+ )} +
+
+ + {error && ( +
+ {error} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/UploadDropzone/__tests__/UploadDropzone.test.tsx b/frontend/src/components/UploadDropzone/__tests__/UploadDropzone.test.tsx new file mode 100644 index 0000000..44b8d89 --- /dev/null +++ b/frontend/src/components/UploadDropzone/__tests__/UploadDropzone.test.tsx @@ -0,0 +1,144 @@ +import { describe, it, expect, vi } from 'vitest' +import { render, screen, userEvent, waitFor } from '../../../test/utils' +import { UploadDropzone } from '../UploadDropzone' +import { createMockFile } from '../../../test/utils' + +describe('UploadDropzone', () => { + it('renders default state correctly', () => { + const onFileSelect = vi.fn() + render() + + expect(screen.getByText('Drag and drop a video file here, or click to select')).toBeInTheDocument() + expect(screen.getByText('Supports MP4, MOV, AVI, MKV up to 1024MB')).toBeInTheDocument() + }) + + it('shows custom max size in text', () => { + const onFileSelect = vi.fn() + const maxSize = 500 * 1024 * 1024 // 500MB + + render( + + ) + + expect(screen.getByText('Supports MP4, MOV, AVI, MKV up to 500MB')).toBeInTheDocument() + }) + + it('calls onFileSelect when valid file is uploaded', async () => { + const user = userEvent.setup() + const onFileSelect = vi.fn() + const mockFile = createMockFile('test-video.mp4', 'video/mp4') + + render() + + const input = screen.getByRole('presentation').querySelector('input[type="file"]') as HTMLInputElement + await user.upload(input, mockFile) + + await waitFor(() => { + expect(onFileSelect).toHaveBeenCalledWith(mockFile) + }) + }) + + it('shows error for file too large', async () => { + const user = userEvent.setup() + const onFileSelect = vi.fn() + const maxSize = 100 * 1024 * 1024 // 100MB + const largeFile = createMockFile('large-video.mp4', 'video/mp4', 200 * 1024 * 1024) // 200MB + + render( + + ) + + const input = screen.getByRole('presentation').querySelector('input[type="file"]') as HTMLInputElement + await user.upload(input, largeFile) + + await waitFor(() => { + expect(screen.getByText('File is too large. Maximum size is 100MB')).toBeInTheDocument() + }) + expect(onFileSelect).not.toHaveBeenCalled() + }) + + it('shows error for invalid file type', async () => { + const user = userEvent.setup() + const onFileSelect = vi.fn() + const invalidFile = createMockFile('document.pdf', 'application/pdf') + + render() + + const input = screen.getByRole('presentation').querySelector('input[type="file"]') as HTMLInputElement + await user.upload(input, invalidFile) + + await waitFor(() => { + expect(screen.getByText('Invalid file type. Please upload a video file.')).toBeInTheDocument() + }) + expect(onFileSelect).not.toHaveBeenCalled() + }) + + it('applies disabled styling when disabled', () => { + const onFileSelect = vi.fn() + render( + + ) + + const dropzone = screen.getByText('Drag and drop a video file here, or click to select').closest('[role="presentation"]')! + expect(dropzone).toHaveClass('opacity-50', 'cursor-not-allowed') + }) + + it('clears error state when new file is uploaded', async () => { + const user = userEvent.setup() + const onFileSelect = vi.fn() + const invalidFile = createMockFile('document.pdf', 'application/pdf') + const validFile = createMockFile('video.mp4', 'video/mp4') + + render() + const input = screen.getByRole('presentation').querySelector('input[type="file"]') as HTMLInputElement + + // Upload invalid file first + await user.upload(input, invalidFile) + await waitFor(() => { + expect(screen.getByText('Invalid file type. Please upload a video file.')).toBeInTheDocument() + }) + + // Upload valid file + await user.upload(input, validFile) + + await waitFor(() => { + expect(screen.queryByText('Invalid file type. Please upload a video file.')).not.toBeInTheDocument() + expect(onFileSelect).toHaveBeenCalledWith(validFile) + }) + }) + + it('handles custom accept types', () => { + const onFileSelect = vi.fn() + const customAccept = { 'video/mp4': ['.mp4'] } + + render( + + ) + + // The component should render normally with custom accept types + expect(screen.getByText('Drag and drop a video file here, or click to select')).toBeInTheDocument() + }) + + it('updates styling during drag events', () => { + const onFileSelect = vi.fn() + render() + + // Note: Testing drag events with React Testing Library is limited + // In a real scenario, you might need to mock the useDropzone hook + // or test at the integration level with tools like Cypress/Playwright + const dropzone = screen.getByText('Drag and drop a video file here, or click to select').closest('[role="presentation"]')! + expect(dropzone.className).toContain('border') + }) +}) \ No newline at end of file diff --git a/frontend/src/components/VideoWithCaptions.tsx b/frontend/src/components/VideoWithCaptions.tsx new file mode 100644 index 0000000..3df48a0 --- /dev/null +++ b/frontend/src/components/VideoWithCaptions.tsx @@ -0,0 +1,337 @@ +import { useRef, useEffect, useState, useMemo } from 'react'; +import { VTTParser, type VTTCue } from '../lib/vtt'; + +interface LanguageTrack { + language: string; + label: string; + captionsVtt?: string; + audioDescriptionVtt?: string; + audioDescriptionUrl?: string; +} + +interface VideoWithCaptionsProps { + videoUrl: string; + tracks?: LanguageTrack[]; + // Legacy single-language props (still supported) + captionsVtt?: string; + audioDescriptionVtt?: string; + audioDescriptionUrl?: string; + className?: string; +} + +export function VideoWithCaptions({ + videoUrl, + tracks = [], + // Legacy props + captionsVtt, + audioDescriptionVtt, + audioDescriptionUrl, + className = '' +}: VideoWithCaptionsProps) { + const videoRef = useRef(null); + const timelineRef = useRef(null); + const [currentTime, setCurrentTime] = useState(0); + const [showCaptions, setShowCaptions] = useState(true); + const [showAudioDescription, setShowAudioDescription] = useState(false); + const [selectedLanguage, setSelectedLanguage] = useState(''); + const [languageCues, setLanguageCues] = useState>({}); + + // Combine legacy props with tracks (use useMemo to prevent recreation) + const allTracks = useMemo(() => { + const combined = [...tracks]; + if (captionsVtt || audioDescriptionVtt) { + combined.unshift({ + language: 'en', + label: 'English', + captionsVtt, + audioDescriptionVtt, + audioDescriptionUrl + }); + } + return combined; + }, [tracks, captionsVtt, audioDescriptionVtt, audioDescriptionUrl]); + + // Set initial language selection + useEffect(() => { + if (allTracks.length > 0 && !selectedLanguage) { + setSelectedLanguage(allTracks[0].language); + } + }, [allTracks]); + + // Parse VTT content for all tracks + useEffect(() => { + if (allTracks.length === 0) return; + + const newLanguageCues: Record = {}; + + for (const track of allTracks) { + const captions: VTTCue[] = []; + const ad: VTTCue[] = []; + + if (track.captionsVtt) { + try { + console.log(`Raw VTT content for ${track.language}:`, track.captionsVtt.substring(0, 500)); + const parsedCues = VTTParser.parse(track.captionsVtt); + console.log(`Parsed ${parsedCues.length} cues for ${track.language}:`, parsedCues.slice(0, 3)); + captions.push(...parsedCues); + } catch (error) { + console.error(`Failed to parse captions VTT for ${track.language}:`, error); + } + } + + if (track.audioDescriptionVtt) { + try { + ad.push(...VTTParser.parse(track.audioDescriptionVtt)); + } catch (error) { + console.error(`Failed to parse audio description VTT for ${track.language}:`, error); + } + } + + newLanguageCues[track.language] = { captions, ad }; + } + + // Only update if the content has actually changed + setLanguageCues(prevCues => { + const hasChanged = JSON.stringify(prevCues) !== JSON.stringify(newLanguageCues); + return hasChanged ? newLanguageCues : prevCues; + }); + }, [allTracks]); + + // Update current time + useEffect(() => { + const video = videoRef.current; + if (!video) return; + + let timeoutId: number; + const handleTimeUpdate = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + setCurrentTime(video.currentTime); + }, 100); // Throttle updates to every 100ms + }; + + video.addEventListener('timeupdate', handleTimeUpdate); + return () => { + video.removeEventListener('timeupdate', handleTimeUpdate); + clearTimeout(timeoutId); + }; + }, []); + + // Get current track data (memoized) + const currentTrack = useMemo(() => + allTracks.find(track => track.language === selectedLanguage), + [allTracks, selectedLanguage] + ); + + const currentCues = useMemo(() => + languageCues[selectedLanguage] || { captions: [], ad: [] }, + [languageCues, selectedLanguage] + ); + + // Find current caption and audio description (memoized) + const currentCaption = useMemo(() => + currentCues.captions.find( + cue => currentTime >= cue.startTime && currentTime <= cue.endTime + ), + [currentCues.captions, currentTime] + ); + + const currentAD = useMemo(() => + currentCues.ad.find( + cue => currentTime >= cue.startTime && currentTime <= cue.endTime + ), + [currentCues.ad, currentTime] + ); + + // Auto-scroll to current caption + useEffect(() => { + if (currentCaption && timelineRef.current) { + const timelineContainer = timelineRef.current; + const currentCaptionIndex = currentCues.captions.findIndex( + cue => cue === currentCaption + ); + + if (currentCaptionIndex !== -1) { + const captionElements = timelineContainer.children; + const currentElement = captionElements[currentCaptionIndex] as HTMLElement; + + if (currentElement) { + // Get actual rendered positions + const elementRect = currentElement.getBoundingClientRect(); + const containerRect = timelineContainer.getBoundingClientRect(); + + // Calculate element position relative to container + const elementTopInContainer = elementRect.top - containerRect.top + timelineContainer.scrollTop; + const elementBottomInContainer = elementTopInContainer + elementRect.height; + + const containerHeight = timelineContainer.clientHeight; + const currentScrollTop = timelineContainer.scrollTop; + const visibleTop = currentScrollTop; + const visibleBottom = currentScrollTop + containerHeight; + + // Check if element is outside visible area + const isAbove = elementTopInContainer < visibleTop; + const isBelow = elementBottomInContainer > visibleBottom; + + if (isAbove || isBelow) { + // Calculate scroll position to center the element + const targetScrollTop = elementTopInContainer - (containerHeight / 2) + (elementRect.height / 2); + + timelineContainer.scrollTo({ + top: Math.max(0, targetScrollTop), + behavior: 'smooth' + }); + } + } + } + } + }, [currentCaption, currentCues.captions]); + + const jumpToTime = (time: number) => { + const video = videoRef.current; + if (video && !video.ended) { + video.currentTime = time; + // Don't auto-play, let user manually play if they want + } + }; + + return ( +
+ {/* Video Player */} +
+ + + {/* Caption Overlay */} + {showCaptions && currentCaption && ( +
+
+ {currentCaption.text} +
+
+ )} +
+ + {/* Controls */} +
+ {allTracks.length > 1 && ( +
+ + +
+ )} + + + + {currentTrack?.audioDescriptionUrl && ( + + )} + +
+ Time: {formatTime(currentTime)} +
+
+ + {/* Audio Description Player */} + {showAudioDescription && currentTrack?.audioDescriptionUrl && ( +
+

Audio Description ({currentTrack.label})

+
+ )} + + {/* Caption Cues Timeline */} + {currentCues.captions.length > 0 && ( +
+

+ Caption Timeline ({currentTrack?.label || selectedLanguage}) +

+
+ {currentCues.captions.map((cue, index) => ( +
= cue.startTime && currentTime <= cue.endTime + ? 'bg-blue-100 border border-blue-300' + : 'bg-gray-50 hover:bg-gray-100' + }`} + onClick={() => jumpToTime(cue.startTime)} + > +
+
+ {formatTime(cue.startTime)} → {formatTime(cue.endTime)} +
+ +
+
+ {cue.text} +
+
+ ))} +
+
+ )} +
+ ); +} + +function formatTime(seconds: number): string { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = Math.floor(seconds % 60); + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; + } + return `${minutes}:${secs.toString().padStart(2, '0')}`; +} \ No newline at end of file diff --git a/frontend/src/components/VttEditor/VttEditor.tsx b/frontend/src/components/VttEditor/VttEditor.tsx new file mode 100644 index 0000000..f6b4ed2 --- /dev/null +++ b/frontend/src/components/VttEditor/VttEditor.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from 'react'; +import { VTTParser, VTTValidator, type VTTCue } from '../../lib/vtt'; + +interface VttEditorProps { + vttContent: string; + onChange: (content: string) => void; + title: string; + readOnly?: boolean; +} + +export function VttEditor({ vttContent, onChange, title, readOnly = false }: VttEditorProps) { + const [cues, setCues] = useState([]); + const [errors, setErrors] = useState([]); + const [editingCue, setEditingCue] = useState(null); + + useEffect(() => { + try { + const parsedCues = VTTParser.parse(vttContent); + setCues(parsedCues); + + // Validate content + const validation = VTTValidator.validate(vttContent); + setErrors(validation.errors); + } catch (error) { + setErrors([`Failed to parse VTT: ${error instanceof Error ? error.message : 'Unknown error'}`]); + } + }, [vttContent]); + + const updateCueText = (index: number, newText: string) => { + const updatedCues = [...cues]; + updatedCues[index] = { ...updatedCues[index], text: newText }; + setCues(updatedCues); + + // Rebuild VTT and notify parent + const newVttContent = VTTParser.build(updatedCues); + onChange(newVttContent); + + setEditingCue(null); + }; + + const formatTime = (seconds: number): string => { + const hours = Math.floor(seconds / 3600); + const minutes = Math.floor((seconds % 3600) / 60); + const secs = seconds % 60; + + const wholeSeconds = Math.floor(secs); + const milliseconds = Math.round((secs - wholeSeconds) * 1000); + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${wholeSeconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; + } + return `${minutes}:${wholeSeconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; + }; + + return ( +
+ {/* Header */} +
+

{title}

+ {errors.length > 0 && ( +
+
+

Validation Errors:

+
    + {errors.map((error, i) => ( +
  • {error}
  • + ))} +
+
+
+ )} +
+ + {/* Cue List */} +
+ {cues.length === 0 ? ( +
+ No cues found in VTT content +
+ ) : ( +
+ {cues.map((cue, index) => ( +
+ {/* Timing */} +
+ {formatTime(cue.startTime)} → {formatTime(cue.endTime)} + + ({((cue.endTime - cue.startTime) * 1000).toFixed(0)}ms) + +
+ + {/* Text Content */} + {editingCue === index ? ( + updateCueText(index, newText)} + onCancel={() => setEditingCue(null)} + /> + ) : ( +
+
+ {cue.text} +
+ {!readOnly && ( + + )} +
+ )} +
+ ))} +
+ )} +
+ + {/* Footer Stats */} +
+ {cues.length} cues • Total duration: {formatTime(Math.max(...cues.map(c => c.endTime), 0))} +
+
+ ); +} + +interface CueEditorProps { + initialText: string; + onSave: (text: string) => void; + onCancel: () => void; +} + +function CueEditor({ initialText, onSave, onCancel }: CueEditorProps) { + const [text, setText] = useState(initialText); + + const handleSave = () => { + onSave(text); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onCancel(); + } else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + handleSave(); + } + }; + + return ( +
+