From af2562096a14acb32cd4de96df6edb611c51d539 Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 24 Aug 2025 16:28:33 -0500 Subject: [PATCH] initial commit --- .DS_Store | Bin 0 -> 6148 bytes .github/workflows/cd-backend.yml | 182 + .github/workflows/cd-frontend.yml | 147 + .github/workflows/ci.yml | 312 + .gitignore | 1 + CLAUDE.md | 148 + Makefile | 62 + README.md | 178 + backend/.dockerignore | 92 + backend/.env | 42 + backend/.env.example | 42 + backend/Dockerfile | 127 + .../__pycache__/celery_worker.cpython-313.pyc | Bin 0 -> 2319 bytes backend/app/__pycache__/main.cpython-313.pyc | Bin 0 -> 9388 bytes .../__pycache__/routes_admin.cpython-313.pyc | Bin 0 -> 29587 bytes .../__pycache__/routes_auth.cpython-313.pyc | Bin 0 -> 6121 bytes .../__pycache__/routes_files.cpython-313.pyc | Bin 0 -> 2290 bytes .../__pycache__/routes_jobs.cpython-313.pyc | Bin 0 -> 43302 bytes backend/app/api/v1/routes_admin.py | 770 ++ backend/app/api/v1/routes_auth.py | 161 + backend/app/api/v1/routes_files.py | 51 + backend/app/api/v1/routes_jobs.py | 1033 +++ .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 2686 bytes .../core/__pycache__/database.cpython-313.pyc | Bin 0 -> 4386 bytes .../__pycache__/dependencies.cpython-313.pyc | Bin 0 -> 3995 bytes .../core/__pycache__/logging.cpython-313.pyc | Bin 0 -> 4399 bytes .../core/__pycache__/redis.cpython-313.pyc | Bin 0 -> 2920 bytes .../secrets_config.cpython-313.pyc | Bin 0 -> 6474 bytes .../core/__pycache__/security.cpython-313.pyc | Bin 0 -> 3129 bytes backend/app/core/config.py | 77 + backend/app/core/database.py | 67 + backend/app/core/dependencies.py | 88 + backend/app/core/logging.py | 65 + backend/app/core/redis.py | 49 + backend/app/core/secrets_config.py | 145 + backend/app/core/security.py | 55 + .../app/lib/__pycache__/vtt.cpython-313.pyc | Bin 0 -> 10363 bytes backend/app/lib/vtt.py | 222 + backend/app/main.py | 216 + backend/app/middleware/__init__.py | 12 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 523 bytes .../__pycache__/rate_limiting.cpython-313.pyc | Bin 0 -> 10947 bytes .../__pycache__/validation.cpython-313.pyc | Bin 0 -> 13740 bytes backend/app/middleware/rate_limiting.py | 264 + backend/app/middleware/validation.py | 324 + backend/app/migrations/__init__.py | 5 + backend/app/migrations/migrator.py | 253 + ...ration_2025-08-17-120000_initial_schema.py | 64 + ...on_2025-08-17-120001_index_optimization.py | 134 + ...25-08-17-120002_audit_log_schema_update.py | 155 + .../__pycache__/audit_log.cpython-313.pyc | Bin 0 -> 6623 bytes .../models/__pycache__/job.cpython-313.pyc | Bin 0 -> 5396 bytes .../models/__pycache__/user.cpython-313.pyc | Bin 0 -> 2978 bytes backend/app/models/audit_log.py | 175 + backend/app/models/job.py | 95 + backend/app/models/user.py | 57 + backend/app/prompts/gemini_ingestion.md | 57 + backend/app/prompts/gemini_transcreation.md | 20 + .../schemas/__pycache__/auth.cpython-313.pyc | Bin 0 -> 3786 bytes .../schemas/__pycache__/file.cpython-313.pyc | Bin 0 -> 957 bytes .../schemas/__pycache__/job.cpython-313.pyc | Bin 0 -> 5045 bytes backend/app/schemas/auth.py | 72 + backend/app/schemas/file.py | 15 + backend/app/schemas/job.py | 89 + .../__pycache__/audit_logger.cpython-313.pyc | Bin 0 -> 14431 bytes .../__pycache__/emailer.cpython-313.pyc | Bin 0 -> 5392 bytes .../services/__pycache__/gcs.cpython-313.pyc | Bin 0 -> 9871 bytes .../__pycache__/gemini.cpython-313.pyc | Bin 0 -> 17104 bytes .../__pycache__/translate.cpython-313.pyc | Bin 0 -> 4411 bytes .../services/__pycache__/tts.cpython-313.pyc | Bin 0 -> 12093 bytes .../__pycache__/validation.cpython-313.pyc | Bin 0 -> 6282 bytes backend/app/services/audit_logger.py | 331 + backend/app/services/emailer.py | 123 + backend/app/services/gcs.py | 168 + backend/app/services/gemini.py | 350 + backend/app/services/secrets_manager.py | 284 + backend/app/services/translate.py | 110 + backend/app/services/tts.py | 301 + backend/app/services/validation.py | 130 + backend/app/tasks/__init__.py | 158 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 7294 bytes .../__pycache__/ingest_and_ai.cpython-313.pyc | Bin 0 -> 8653 bytes .../tasks/__pycache__/notify.cpython-313.pyc | Bin 0 -> 6960 bytes .../translate_and_synthesize.cpython-313.pyc | Bin 0 -> 14299 bytes .../__pycache__/watchers.cpython-313.pyc | Bin 0 -> 6241 bytes backend/app/tasks/ingest_and_ai.py | 213 + backend/app/tasks/notify.py | 142 + backend/app/tasks/translate_and_synthesize.py | 317 + backend/app/tasks/watchers.py | 136 + backend/app/telemetry/__init__.py | 33 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 860 bytes .../__pycache__/metrics.cpython-313.pyc | Bin 0 -> 13840 bytes .../__pycache__/tracing.cpython-313.pyc | Bin 0 -> 12517 bytes backend/app/telemetry/metrics.py | 359 + backend/app/telemetry/tracing.py | 268 + backend/celery_worker.py | 42 + backend/cors-config.json | 8 + backend/create_test_users.py | 78 + backend/debug_login.py | 52 + backend/gunicorn_conf.py | 29 + backend/migrate.py | 206 + backend/optical-414516-80e2475f6412.json | 13 + backend/poetry.lock | 3980 ++++++++++ backend/pyproject.toml | 108 + backend/setup_secrets.py | 177 + backend/simple_login_test.py | 85 + backend/test_auth.py | 52 + backend/test_db.py | 37 + backend/test_endpoint.py | 30 + backend/test_mp3_serving.py | 175 + backend/tests/fixtures/sample_en_ad.vtt | 33 + backend/tests/fixtures/sample_en_captions.vtt | 39 + backend/tests/fixtures/sample_es_ad.vtt | 33 + backend/tests/fixtures/sample_es_captions.vtt | 39 + backend/tests/fixtures/sample_ingestion.json | 8 + backend/tests/unit/test_emailer.py | 241 + backend/tests/unit/test_gcs.py | 287 + backend/tests/unit/test_gemini.py | 296 + backend/tests/unit/test_models.py | 352 + backend/tests/unit/test_security.py | 233 + backend/tests/unit/test_translate.py | 238 + backend/tests/unit/test_tts.py | 266 + backend/tests/unit/test_vtt.py | 350 + docker-compose.yml | 131 + ...app Technical Documentation 2025-08-24.pdf | Bin 0 -> 682249 bytes ...accessibility_technical_docs_2025-08-24.md | 1465 ++++ frontend/.env.example | 3 + frontend/.gitignore | 24 + frontend/Dockerfile | 49 + frontend/README.md | 69 + frontend/eslint.config.js | 23 + frontend/index.html | 13 + frontend/nginx.conf | 76 + frontend/package-lock.json | 6623 +++++++++++++++++ frontend/package.json | 56 + frontend/playwright.config.ts | 60 + frontend/postcss.config.js | 6 + frontend/public/vite.svg | 1 + frontend/src/App.css | 42 + frontend/src/App.tsx | 117 + frontend/src/__tests__/basic.test.ts | 11 + frontend/src/assets/react.svg | 1 + frontend/src/components/Auth/RequireAuth.tsx | 25 + frontend/src/components/Auth/RoleGate.tsx | 36 + .../Auth/__tests__/RequireAuth.test.tsx | 134 + .../Auth/__tests__/RoleGate.test.tsx | 135 + frontend/src/components/ErrorBoundary.tsx | 88 + frontend/src/components/Layout/Layout.tsx | 51 + frontend/src/components/Layout/Navbar.tsx | 188 + frontend/src/components/Layout/Sidebar.tsx | 125 + frontend/src/components/StatusBadge.tsx | 67 + frontend/src/components/Toast/Toast.tsx | 121 + .../UploadDropzone/UploadDropzone.tsx | 93 + .../__tests__/UploadDropzone.test.tsx | 144 + frontend/src/components/VideoWithCaptions.tsx | 337 + .../src/components/VttEditor/VttEditor.tsx | 175 + .../VttEditor/__tests__/VttEditor.test.tsx | 266 + .../components/__tests__/StatusBadge.test.tsx | 86 + frontend/src/contexts/ToastContext.tsx | 34 + frontend/src/hooks/__tests__/useJob.test.tsx | 285 + frontend/src/hooks/useJob.ts | 196 + frontend/src/hooks/useToast.ts | 58 + frontend/src/index.css | 68 + frontend/src/lib/__tests__/auth.test.ts | 274 + frontend/src/lib/__tests__/vtt-simple.test.ts | 15 + frontend/src/lib/api.ts | 213 + frontend/src/lib/auth.ts | 71 + frontend/src/lib/queryClient.ts | 16 + frontend/src/lib/vtt.ts | 166 + frontend/src/main.tsx | 36 + frontend/src/routes/Dashboard.tsx | 358 + frontend/src/routes/Downloads.tsx | 242 + frontend/src/routes/Login.tsx | 176 + frontend/src/routes/admin/FinalDetail.tsx | 388 + frontend/src/routes/admin/FinalList.tsx | 195 + frontend/src/routes/admin/QCDetail.tsx | 542 ++ frontend/src/routes/admin/QCList.tsx | 254 + frontend/src/routes/jobs/JobDetail.tsx | 340 + frontend/src/routes/jobs/JobsList.tsx | 503 ++ frontend/src/routes/jobs/NewJob.tsx | 342 + frontend/src/styles/index.css | 15 + frontend/src/test/setup.ts | 65 + frontend/src/test/utils.tsx | 162 + frontend/src/types/api-new.ts | 38 + frontend/src/types/api.js | 40 + frontend/src/types/api.ts | 145 + frontend/src/types/test.ts | 5 + frontend/src/vite-env.d.ts | 1 + frontend/tailwind.config.js | 7 + frontend/tests/e2e/auth.spec.ts | 50 + frontend/tests/e2e/job-workflow.spec.ts | 142 + frontend/tests/e2e/vtt-editing.spec.ts | 149 + frontend/tests/fixtures/test-data.ts | 120 + frontend/tests/helpers/auth.ts | 92 + frontend/tsconfig.app.json | 27 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 25 + frontend/vite.config.ts | 33 + infra/cloud-cdn/cloudbuild-spa.yaml | 63 + infra/cloud-cdn/deploy-spa.sh | 79 + infra/cloud-cdn/main.tf | 213 + infra/cloud-cdn/terraform.tfvars.example | 13 + infra/cloud-run/README.md | 117 + infra/cloud-run/api-service.yaml | 116 + infra/cloud-run/cloudbuild.yaml | 85 + infra/cloud-run/deploy.sh | 138 + infra/cloud-run/main.tf | 478 ++ infra/cloud-run/terraform.tfvars.example | 11 + infra/cloud-run/worker-service.yaml | 113 + mongo-init.js | 41 + mongo-keyfile | 1 + video_accessibility_development_plan.txt | 798 ++ 212 files changed, 36035 insertions(+) create mode 100644 .DS_Store create mode 100644 .github/workflows/cd-backend.yml create mode 100644 .github/workflows/cd-frontend.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 backend/.dockerignore create mode 100644 backend/.env create mode 100644 backend/.env.example create mode 100644 backend/Dockerfile create mode 100644 backend/__pycache__/celery_worker.cpython-313.pyc create mode 100644 backend/app/__pycache__/main.cpython-313.pyc create mode 100644 backend/app/api/v1/__pycache__/routes_admin.cpython-313.pyc create mode 100644 backend/app/api/v1/__pycache__/routes_auth.cpython-313.pyc create mode 100644 backend/app/api/v1/__pycache__/routes_files.cpython-313.pyc create mode 100644 backend/app/api/v1/__pycache__/routes_jobs.cpython-313.pyc create mode 100644 backend/app/api/v1/routes_admin.py create mode 100644 backend/app/api/v1/routes_auth.py create mode 100644 backend/app/api/v1/routes_files.py create mode 100644 backend/app/api/v1/routes_jobs.py create mode 100644 backend/app/core/__pycache__/config.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/database.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/dependencies.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/logging.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/redis.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/secrets_config.cpython-313.pyc create mode 100644 backend/app/core/__pycache__/security.cpython-313.pyc create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/database.py create mode 100644 backend/app/core/dependencies.py create mode 100644 backend/app/core/logging.py create mode 100644 backend/app/core/redis.py create mode 100644 backend/app/core/secrets_config.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/lib/__pycache__/vtt.cpython-313.pyc create mode 100644 backend/app/lib/vtt.py create mode 100644 backend/app/main.py create mode 100644 backend/app/middleware/__init__.py create mode 100644 backend/app/middleware/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/app/middleware/__pycache__/rate_limiting.cpython-313.pyc create mode 100644 backend/app/middleware/__pycache__/validation.cpython-313.pyc create mode 100644 backend/app/middleware/rate_limiting.py create mode 100644 backend/app/middleware/validation.py create mode 100644 backend/app/migrations/__init__.py create mode 100644 backend/app/migrations/migrator.py create mode 100644 backend/app/migrations/scripts/migration_2025-08-17-120000_initial_schema.py create mode 100644 backend/app/migrations/scripts/migration_2025-08-17-120001_index_optimization.py create mode 100644 backend/app/migrations/scripts/migration_2025-08-17-120002_audit_log_schema_update.py create mode 100644 backend/app/models/__pycache__/audit_log.cpython-313.pyc create mode 100644 backend/app/models/__pycache__/job.cpython-313.pyc create mode 100644 backend/app/models/__pycache__/user.cpython-313.pyc create mode 100644 backend/app/models/audit_log.py create mode 100644 backend/app/models/job.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/prompts/gemini_ingestion.md create mode 100644 backend/app/prompts/gemini_transcreation.md create mode 100644 backend/app/schemas/__pycache__/auth.cpython-313.pyc create mode 100644 backend/app/schemas/__pycache__/file.cpython-313.pyc create mode 100644 backend/app/schemas/__pycache__/job.cpython-313.pyc create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/file.py create mode 100644 backend/app/schemas/job.py create mode 100644 backend/app/services/__pycache__/audit_logger.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/emailer.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/gcs.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/gemini.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/translate.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/tts.cpython-313.pyc create mode 100644 backend/app/services/__pycache__/validation.cpython-313.pyc create mode 100644 backend/app/services/audit_logger.py create mode 100644 backend/app/services/emailer.py create mode 100644 backend/app/services/gcs.py create mode 100644 backend/app/services/gemini.py create mode 100644 backend/app/services/secrets_manager.py create mode 100644 backend/app/services/translate.py create mode 100644 backend/app/services/tts.py create mode 100644 backend/app/services/validation.py create mode 100644 backend/app/tasks/__init__.py create mode 100644 backend/app/tasks/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/app/tasks/__pycache__/ingest_and_ai.cpython-313.pyc create mode 100644 backend/app/tasks/__pycache__/notify.cpython-313.pyc create mode 100644 backend/app/tasks/__pycache__/translate_and_synthesize.cpython-313.pyc create mode 100644 backend/app/tasks/__pycache__/watchers.cpython-313.pyc create mode 100644 backend/app/tasks/ingest_and_ai.py create mode 100644 backend/app/tasks/notify.py create mode 100644 backend/app/tasks/translate_and_synthesize.py create mode 100644 backend/app/tasks/watchers.py create mode 100644 backend/app/telemetry/__init__.py create mode 100644 backend/app/telemetry/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/app/telemetry/__pycache__/metrics.cpython-313.pyc create mode 100644 backend/app/telemetry/__pycache__/tracing.cpython-313.pyc create mode 100644 backend/app/telemetry/metrics.py create mode 100644 backend/app/telemetry/tracing.py create mode 100644 backend/celery_worker.py create mode 100644 backend/cors-config.json create mode 100644 backend/create_test_users.py create mode 100644 backend/debug_login.py create mode 100644 backend/gunicorn_conf.py create mode 100755 backend/migrate.py create mode 100644 backend/optical-414516-80e2475f6412.json create mode 100644 backend/poetry.lock create mode 100644 backend/pyproject.toml create mode 100755 backend/setup_secrets.py create mode 100644 backend/simple_login_test.py create mode 100644 backend/test_auth.py create mode 100644 backend/test_db.py create mode 100644 backend/test_endpoint.py create mode 100644 backend/test_mp3_serving.py create mode 100644 backend/tests/fixtures/sample_en_ad.vtt create mode 100644 backend/tests/fixtures/sample_en_captions.vtt create mode 100644 backend/tests/fixtures/sample_es_ad.vtt create mode 100644 backend/tests/fixtures/sample_es_captions.vtt create mode 100644 backend/tests/fixtures/sample_ingestion.json create mode 100644 backend/tests/unit/test_emailer.py create mode 100644 backend/tests/unit/test_gcs.py create mode 100644 backend/tests/unit/test_gemini.py create mode 100644 backend/tests/unit/test_models.py create mode 100644 backend/tests/unit/test_security.py create mode 100644 backend/tests/unit/test_translate.py create mode 100644 backend/tests/unit/test_tts.py create mode 100644 backend/tests/unit/test_vtt.py create mode 100644 docker-compose.yml create mode 100644 docs/Video Accessibility app Technical Documentation 2025-08-24.pdf create mode 100644 docs/video_accessibility_technical_docs_2025-08-24.md create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/playwright.config.ts create mode 100644 frontend/postcss.config.js create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/__tests__/basic.test.ts create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/Auth/RequireAuth.tsx create mode 100644 frontend/src/components/Auth/RoleGate.tsx create mode 100644 frontend/src/components/Auth/__tests__/RequireAuth.test.tsx create mode 100644 frontend/src/components/Auth/__tests__/RoleGate.test.tsx create mode 100644 frontend/src/components/ErrorBoundary.tsx create mode 100644 frontend/src/components/Layout/Layout.tsx create mode 100644 frontend/src/components/Layout/Navbar.tsx create mode 100644 frontend/src/components/Layout/Sidebar.tsx create mode 100644 frontend/src/components/StatusBadge.tsx create mode 100644 frontend/src/components/Toast/Toast.tsx create mode 100644 frontend/src/components/UploadDropzone/UploadDropzone.tsx create mode 100644 frontend/src/components/UploadDropzone/__tests__/UploadDropzone.test.tsx create mode 100644 frontend/src/components/VideoWithCaptions.tsx create mode 100644 frontend/src/components/VttEditor/VttEditor.tsx create mode 100644 frontend/src/components/VttEditor/__tests__/VttEditor.test.tsx create mode 100644 frontend/src/components/__tests__/StatusBadge.test.tsx create mode 100644 frontend/src/contexts/ToastContext.tsx create mode 100644 frontend/src/hooks/__tests__/useJob.test.tsx create mode 100644 frontend/src/hooks/useJob.ts create mode 100644 frontend/src/hooks/useToast.ts create mode 100644 frontend/src/index.css create mode 100644 frontend/src/lib/__tests__/auth.test.ts create mode 100644 frontend/src/lib/__tests__/vtt-simple.test.ts create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/lib/auth.ts create mode 100644 frontend/src/lib/queryClient.ts create mode 100644 frontend/src/lib/vtt.ts create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/routes/Dashboard.tsx create mode 100644 frontend/src/routes/Downloads.tsx create mode 100644 frontend/src/routes/Login.tsx create mode 100644 frontend/src/routes/admin/FinalDetail.tsx create mode 100644 frontend/src/routes/admin/FinalList.tsx create mode 100644 frontend/src/routes/admin/QCDetail.tsx create mode 100644 frontend/src/routes/admin/QCList.tsx create mode 100644 frontend/src/routes/jobs/JobDetail.tsx create mode 100644 frontend/src/routes/jobs/JobsList.tsx create mode 100644 frontend/src/routes/jobs/NewJob.tsx create mode 100644 frontend/src/styles/index.css create mode 100644 frontend/src/test/setup.ts create mode 100644 frontend/src/test/utils.tsx create mode 100644 frontend/src/types/api-new.ts create mode 100644 frontend/src/types/api.js create mode 100644 frontend/src/types/api.ts create mode 100644 frontend/src/types/test.ts create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tests/e2e/auth.spec.ts create mode 100644 frontend/tests/e2e/job-workflow.spec.ts create mode 100644 frontend/tests/e2e/vtt-editing.spec.ts create mode 100644 frontend/tests/fixtures/test-data.ts create mode 100644 frontend/tests/helpers/auth.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 infra/cloud-cdn/cloudbuild-spa.yaml create mode 100755 infra/cloud-cdn/deploy-spa.sh create mode 100644 infra/cloud-cdn/main.tf create mode 100644 infra/cloud-cdn/terraform.tfvars.example create mode 100644 infra/cloud-run/README.md create mode 100644 infra/cloud-run/api-service.yaml create mode 100644 infra/cloud-run/cloudbuild.yaml create mode 100755 infra/cloud-run/deploy.sh create mode 100644 infra/cloud-run/main.tf create mode 100644 infra/cloud-run/terraform.tfvars.example create mode 100644 infra/cloud-run/worker-service.yaml create mode 100644 mongo-init.js create mode 100644 mongo-keyfile create mode 100644 video_accessibility_development_plan.txt diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bcd98cdf66b58d188d5f114b3dbb101b1ad3f240 GIT binary patch literal 6148 zcmeHKO>5gg5S>lbNJ=R9&_Z%s=+#immQzA6!cG1_6xu^t9H}y`h@v%?T^EEw=e9p6 z`6Kx~dHa!)*p2BaE$t5MyxE-{iTA|rYKe#qW?4YgA|ek>qO%S63zK=-R&2wJYyp{i z#)wMF(s?ybiZ#LKNw3oTP8_(R|o?_C}P`Briu3Rgh;ROnLtv*hFwS5Q-ing z9l;W-FY%--MEMCBS>;eh0-P(oTPe0zkIr_lu7iFL3}1!o=t?@U2kHJm@+YAsq8>i# zSA5mUjtR!wp6T8;!`om@WnCQf96gOqXMvn|YDJMx!n# zW-cGhyqTFB3R7>#^SPQ%EYfJbRlq86Ux7{CZOQZh=;8bS{UY153RngHDFv8w7#|Km z$(*e#a$E+$$Y7%pBQX0Tpk%PlD)6WZ+yKU$ B$Ls(A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..11ef22b51377ab980ecbc400e0e76caf31284684 GIT binary patch literal 2319 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..82e436e165b171e7e875bbee18f49ad51aa6bd6f GIT binary patch literal 6121 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 F`ERAx`5piO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e238f67ee003c5faa8d26f3e5eb6aff97b077fc GIT binary patch literal 2290 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 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 0000000000000000000000000000000000000000..d7c16c0655c9a3875d194e74b42fef8423f0d5d7 GIT binary patch literal 2686 zcmZ`*&2JmW6;P$ok6z zU^%2vk_{@|Fe24TDL&1H6v}k@5yg+Pq~fyy8dhkmYn4;{I7=!1YG9RC=vvomBH%NM zpA685LiqrVDm10gG|MV9qtL8z-T273VceY4*;qLTn&@H}rxda(D2rRh$Hr}=s4T`m zu$V&&HCn0Ln(wymsQ#nus(D70uSmBMVIJXwwgK9`YKC2|i+WMwG~5FVB{ z?z5@tYT*+Zzo>#U>>?u$IgmY8y>1fkpqbedqP|)xxxQ_=dp;MdEA#X7GGW!JS%C^N z8_bn`&%YNYCpIHOMxPjWmzHG@QkGji8^ke(6|!x@xhaHe)~I*ZgNigN7};fn?Y2&iv=Z z7AAF?mz)#>Csmu9l?Ip#IZ^ffD!7M9b*XI$>U$#x1u?{a2sDL0bAl4itUh!L_xJeq!a_<(`fZ)Z) z?}!J@!Ch8d|E6mJghPsJkGsUfg5Hd(QQ(Drn^D!H8C7;Nc`2R)pTcU(^xZqP`T1?T zwz%ycti4#F`#TT5lIcHuv;2U3|7@F9cQzJZ8e5HhYeS~DUv4~p;XT=5yF2UKyBjvz}HT%H*Z#2wV+uSePoKPIrF#2-J*g?lQjyHR4j3 z?n^Zb<^tB*;hr4#1#wOCvhFhh>+TB|i{GjH)Dyq41D5XGTlfunL2V4%XWaA|ty1s( zV-*fl^_*wn_5QJnhpG`_(W+jE%E626IQPi+c4_r&4eXox9PGw_Xq$zYjGLwhm2R4{ z&opbk(}2n{Wx_OHG%PpJ44URHWn8$_Bc2cHICKZfB{E~0TtE%lrX>WUm4+bPG#P#? z&M`o_Q~H(@#!EG7?^(p1vtdsh(9)W3H)=3vycEbJJER1)UV^zG zATW!$`k{<<5@HMB1(V`<1pGy7r^bGYvrnPTK0|?#Y!;^3GJ<<3R#4nW5eFe7Trde4 zTAt@)CccLW^rAk2Vn7+e+OZ*o!zfb7=@s91Sq3pK0vkth6~zRKNfc8krcunGxQ^l@ z5K?!jE!fA1ODN`1+))~rLPRR|Pmsg}I{Ew#TX?D}fel`VI{2&hI+Z=z_|a(T*><*c z^z3-1rI+5sv}F2+t#&HcQRUhb`BQ!5B+}CJ?flfKo;?AxsrK0H(O1U{Eq%6~n}pV4 zOP_3y=a06IVYoaDGMs7Y>GsIYqo>Ev^(HVwsUJ4sg~MfzIoeOnoa$pIFvtw>nQTiR zeb=FV>-MQW{pwLmzui?{o9uSI6x8(Wsh&Fl!&xvKo9g6|f1R9eU;KXUg)0Yk6(^5g zrB0sw{O5Bm8jZaf(1vfelQ%lQ|7SF$jpkbVSR2CMYGaUDa5FmA$t&BwQ8?8nU!_`l z0qFd_)0M5$iLW7s*<=25p{0NQA3YS!z12W;76YgMy^9X+NW3qdW5N8Q{Sjp6nS~2d zbAE$}JP$5NdDzXu@@$eX=68LTcSs!$2;$j?puvv-6d_!y;urQG)PPJ>34S3vw=K2- zZoY)S+5kt|zpkW?wp5Ipu>X_wL&t&-gZ<7PgY1;iI<~O;hYsSOc20uN*cYJ1%s7_r on@A`W`nPuLZS3<*l(m$;p#m{5mP+GbEAD}4p2mk;8 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a99968b6ff59050f42e124f2241271074f54c8e GIT binary patch literal 4386 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 Dnu1n6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3a514bfe3061b8d4f1ca65a32bf383d8acfe1f63 GIT binary patch literal 4399 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 Gz`p^+sx$)t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ac3564d8c5b8d9b55dd5885a0ae7d3ebd5a3ef3e GIT binary patch literal 2920 zcmb_eO>7%Q6rT0&+UuWeoHoDCpOfa#3AK^ZG)XB)N!kL5lp-(ffds8wd+lu5?3&qi zX>w_3E2TN0a;ZeQRN_`7q#_}4Luz{E5Gw&ktMpJ&4!OBa5ma#C&92vWqEI+6+L?JX z^WMDq`M$TaS5xCdkUksxYT*Y4p`w}88l5{R~QK{fu5nK8O&HR zGs8~1u{(_B-Q8&3GeX&hm}^GeNMJ)qaF4i3ecpL*HyW`U2>S$22w}g#K^YLd^R9TP zG*nfca0$K;y3Any6&ij^qc%fD0=vX^c9*OoLYbT{8HTK8bdY@+$q*GSlaX+;$^oKF zjlB;*xP#^qrUZmt0)^>b*eqt|*?6GZz?SDd0^JWHXmFr$MjRHvnG?(G{tl$6Mi&> z^1hBurguBkv2yO#+1oQ~Ge0vOxWO^mcZFo48<6*YgnK`R!f=gq(6m$BxnQRQYJqZa z8b)&t)=}JDrBV{(^Dtl#eh7@fjx%$-W2lfFTB*w0A&}`t6`Od_v8mL8 zdz`9hDCKo@RNK{8^c^YQvIUYtC#G%6vYOT~xjbA4g~_F{wkYPX zf(g(}uLXb7Hhs28X0zCfj}jw`noO4L#C!H?Qpys}nyiGehGF59q?6zl#*JjQU}-6$ zp}~HE`nV6eiF5rMD0a~b+F^XFGoQ|E1P8X5$WCKxp>Z(ZI9O;L&NmM4aKQo>%5$OZ zy5{Yc_U-0S(Tkc+6cO9xzBzsK{EGl;>A2_r(*ND`_vgPopKm^Mb9#$uD7sme-{G4J zd}p5TEbu*fzGsu~D{y@qT;C4I-}bNhH|j@z;!ZxJk+=Vkr~djqY*OK(KR`a*4wDbQD(ipg~%r<=wBbDh&=9_ z>Y^XfEmLjuqc)bvT{MjDiwT7FHz4o5_P@u30hpGg1e|phpZ{a@cm?k&k*LgU+Y>3v zUg4io<@FHFcumF1k^b3h#%A#ZFqyMB=bBw^ol-O%5NNZ_VFTb0h!^qTc9<0d%@OV* zg@sr)2_};L7xDoROTe^#BX-!f0NZG(*;uWjBp!r`jzQKxgkm3Tk8d&29Zy}s(+pn~ z=z8;f?fgi~81DpI{fg8+ogGCx~O$3PpPq@LCdJlNG zJ`tF#rylauWRQ7SdkW-rFEx3DS@(H}e1wA1bYscLVRav@$Wz9GmA&8tznaoR2XxA* z2U&+o&E9uaPV3;Qc*)zu=5G`%hal@DcvuJFZz%ZN^8U6BrtPIpAacF#=zgq|fIvgu z>xN?AzwgimMX2RdZ%anlSdg3xw%#6LeMwC&U`^F>I>f*M2xH+zIh9hRPZC%P!&@qs zjYRQraB^@1!SV(~`yGQ{2dx-no%{q>(9^12Sa660%^GXR(FAcTYo@0pSg{k{LzX5ZOVLoT9aagA3(O+wZBjFku8aIQ=3NMVg|h z$Ef{R)b<#4KJgy?{GCm2`-=N_#zl2LL!|H_zJ0T%W5xH}9i;f@eJnLp3{q747Y+AX Z@3y``AQm|nw0rLj-5vT3Y}&1j{td>SMtlGO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..804a1fd09876071c80ac08e8cfe5efb085d49b4a GIT binary patch literal 6474 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 Pqgyxrn)(5k;>`a8M&_AZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..767c20829777e930f90c89f275c768d35a74e0cf GIT binary patch literal 10363 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 BVHp4b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b547000f3c69538dd4f895106d7284ff8108bb1b GIT binary patch literal 523 zcmZ8dyGjE=6rFv?gkYhCg~hf($Ra3Mh#-iNjY>4Aq#7o(V=_AX;O-_GJ3qo7u=8^) zoyNjK?8N+lGh(vATO974d(Sz;nbzx7q-uNTX?SNK^sa|lnSaf6OEr(EivV?vuGuz1 z6U@*8E40D(^_$gpLKoc71J6eR>e{CUR0dANz27+IQ6$(c0p@3f-jD(FuTt<2NnY%C zT0Y4#!6_+tnlwMv2T;S3D`%u&M?B_5NtU%%=W@skCU{=RbqY+~V9+4+C64Dl4O6aO z5W%Bg)dh(eYdl{rvK+9KJ=hu?4Xy@H!AWCDdKgD3#aPx$Po50^ab&gBF8P{FguwV2 zO_dyyI5^K4QcNgidCvP>@M045Rg5f& z0u`N7n57$F%oCLZZe|l%*^Sd^B-kD-D?c4;zNX;IG7RGtZGNEM3+lb0@Y{B4=7R@y Gwfq5hs+)BH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..42e59135d14108793c24f1645c0a28d0006773ba GIT binary patch literal 10947 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b36f052b9fea66d5ef20766df32224bea2c18547 GIT binary patch literal 6623 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fa0ffeb02c88c0b588e79a14b6d039a195c6fd37 GIT binary patch literal 2978 zcmZuz-D?|H7QZu^FO4Ki{!nAPiR~u#HnX(R?ow|Owxp?Jg;;Ks#S^Cs5e#E#Y)?9# z(cT%UbzzGjkgD`W3mZ0lEA**v{R{dx*a#Z+lF)~?P`?GNvafs2mE=aQF6cLB&OP_z z{LY8wIF-@~j8E!c?fxne@*}~e*xh!F^MUxlLDb+0cCMrD2P<#a%??Tkf>CU zsm$M{b)}$EmCN$FR*2JhL8p2lK@)`}O@dx2rQQ-vb(TDk=vYZErAo>3q%^iH^v--s zltG~BX>y*J+AJ~SU?DEU4RO#6*Xy89l;X>=zIL2zlb}tN#!s{pTssEZ^h>srT$=&y z_)FTUoe5*|2)8o?rd907iQGL0WGV00_6(6}cdC&a__oL5@A`fafkg#P?`%DA%F$ZI zkXU@h4xQUU#qpSy-?LrMj3|?Dx{g<2=@n-?pw2zpb7Av;!lWVsbO2}j1^7ycEXL4H!lTd)1+ zA5Yb}Lv`+1dZP8`&+E<9Gp;qCsxyb`OedXbYR~kkgY4^V{o;Xi@fcQ;GQwaCf^@t9 zn#ZI>s9+K*ngWfPB9+V-l}(8%rcBix#ZZ}s4sQgWbCiVzOeuRVILb8Y)LrL82QtR4 z?79Beq-bbNvn=1rtmhxwj08V*r0$im}qVP=?$B~p^0phYugN>@cHmz0u<<|WOPU&m)0B!x>l zNa~5CaVfDe4MwL1O_dU08%GOMhh~z7w*-F^X$DRh@1KzD>n2{(rBL@pcNM1u6Z608j~nQ z`7VpQA*5~O)*Ysose_FJ_-A^pifsnvArepW=`5T)Od`)GAP;g%*^vyHCGd)upgyWKQabf!3_Xiz?K@Qu!t_nRC za}vvxrV)5LE}--#!X*GEZ3TfxbNC)Z_!9y@e#HGL-kAgX748CjO1__8{PONM*~9tE zjg_X>nmyFcwx!FR#N@*}o$Pd5n(m}09u_;LfU85~`w`Hl^)7_^p$o-8f#did!0?5za7(f&V!1&1K<=GL5|`AC3@#lWG+t7{ zmY24vP<=;*W~O7d-ryB*gkhw3H>M3rhrWS29%Kwu(MXK+mS@HlG#!;5qqw++#t3+5 zh?Vdu=|y^M{Ne4>f&NZf?6HI^MzgdHGpAX?+Jvte1*tBnJ(fVHfiB+HalSyOgD%n6 z^%xRq62FA{I^G6Nm5xCh>a(`Vd&SW*Y+mHFy+jtL5*ahnBMIaAgIo8Cic`m_=KOKK zd5!UXuA<#25?62n0q+pL4FDuAEH?7ZE3I3HiE~&V^B15#K6(=S>|br^0=_}rw5}ZL zP+Awz7AK|8J~?3+w<|)k!1QAhwjqk2DNKp>tMGNf+XTgLQ7HH@@OxhqT6Mo-`|u}5 zHJIu7q@x7iLH(S-|9N^DfjhQ@66QQl0`A1=7N@=4`*XcUZ-E`Q;P4}W7coH)o|ALW zN%jZwho8yJ59E5ATz{b`g7L)c5_r6rAY*4*;TLxf-niC2`_93ccMen68+uoX2~*9m swb~^hbdyP8zInAv;L)8Egm^RR5_ohqNmy#lbO}7VV_g0fWuAcl2gEISkpKVy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e25bb998bdb898a723bccd7ead7d0c51f9c0aa4d GIT binary patch literal 3786 zcma)9-D}%c6qjsSmgTQF-$~mfYm>F!+GMbewWIrL8rr0(JEC@MU=WI}IO-J3y;n|` z!bTv_r4NI2P?py{_RlGSU@nwB>?v=9cn^EqIY)Bh8I{hEqkDhne*W(H%5gTUOYp1x z^ZWWQ2}$}BJDnf&>32LKOVSI;kPP{bv?Mcm3fr+ev86bRFDXoce%w&*B$iaBPD!KE zdC5qOONLsGe;f~%U`axgfNEW+MraC9y$ekdng%q}h3bT60nK%xX+raW_I05dLJNTQ zccEEA2LK)HLUV);0Xp1;<_R4EbhHcYBXkVV@h-GbJ5xM+gcmK!Lc7=S9j|V=#h6Il zu(*BKtJ<#67B(!$HGFmy3&%a+He2yryAm)I$dE4S_&w;qkjfI14T;4JnZ;+MvQkbM zv5TN7tI&#L%TQRdmME$sv+UKJ`ilLiX>7Uz#WR(&aziki?&vux=r zrsez0S#A0@=vWqJ9K)X^zMaFX^0^JCvToV#Y{j+NrZacbt28(4y3gkt%zJ28pt0#x zZO^nS6`ONs)o~qvYi`x5d~esQb5^4<$1Ce_B|c|0{q@<#7E8hMB=EXC1L9|?l^)o+ z*UA;1-ep4_bu`5{K>cQljW|<@aD@m*>Juj|m8eew^;$6{vLW@n0kqghX;@cef?Jrr z2d5Y6s%o7K3xnZ;+$w|Wg* zWAJ!owZ~s_@MvDsKRN6OOnow9G|BkZny$Mw<$AT6U7dnaBDn!3Y1P7KDn>ukyRbyp zTSYw+9+08H?K&1aaC!UIu5qYb4z7pl= zp)wtiigTk$y$PBqQi&cljJL^-{gkm4gam9+NHA2U0ePC}L-Lj#eri=Su((K|r*o0; zA#}PzX@DIGg*#6=)|85(N+>QYM0Y0F7jdWVM{@H4?7>jrW*yg0=l3$N(&rA8bFFl4 zX9>K1XR*umad-f`fHa_aW?G3b;F^jrhdSTFggMhn^hx8EzAhNVADaVjk&av<~KhNSpf% z6)5lw{5lBW65MWK_v+qlvb*WlVj&we~mE?^H>-MjiqKZ{)$i%R9$PbWm< za?wP;r5M88+$n}sDu#yU*R6WZ{yHptI#to9u!HEUG#P`s!|aqrB2%{?pY%ezOGc7M z98p?}GO#aUd4>Y_;h4yQl;N=hWehU$p)wRiA{-EX%_-3I+S6%x$AYA$6cb_)a+dA) z-t7yR9M}S-*kberPTxH00t>hX3S5k%8IXrOy%(GuHPI(u0nM9FPIsUOf}oKsD<8%1 z81QJlrPY5@6qQ!}UsX4px&ieiKgrbdFj=H2n3_Rkg|Z4FtNGAdt2)b@bUG?$NvQO4MMR}bt*n(dXZRjJxF;` zY~IH4DDc4iA_!o!FnFL0w)!wj4UX;)>{s?jpO+4l^R1Eb17*B5Iep>|II~pj7b;YX zPQ6Ax3$^@K)v80W%@j}vWqBHkc-!T($OUE8pf0A05KAShH-bchVV^=E`7YTiy*C|` zE+4!!mXt!f9{hLkIlB*C8UFaMApVNWviw>)^M`clwRFC%#^nCJYi$WWZC#Obdy8!e zKJ8pgp4@W~pKR;0d}Z&uwgjKHuF5kw9zN}U9KRKgSLB)f6t-vDx$bsM9^QvF;e+c8 T?!#R8w6z3`ANyN^51sj6d{p}; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..189d4b26532f903da6bd9d8f5dd2fc920f4eab51 GIT binary patch literal 957 zcmbVL&uiN-6qaMhak8{cn}&5QbfsksU+T+t7>pjq4yC0j%oKJA2*p+tZT?kBF3V1X z!nWRWDfGJI{+_}Jf}O?=WtTy^`=0C+*6RfR{OP?D*ic!C3kAW%$5A=!c%% zj_Qm|cY$0WANj^U+B3MZp?PJ$vS)I01FfJ<EKuUC8k+9F`~vk5H+;k^zQN5$sAu)6e&sP_3TA5N zTiou|dXD~d$X1J(&Fcfpt*N%p1_=$1vM43tD|(VsA+K#vD(9F*G$Aon%^*!AO(d41 zjCPe1lXqCK_w*}r8_L00i!fFVjN>%SBh8x_pX4N(Wfm|#W?aaKB{WGPZ3@X1L`jmQ zlIRl)jJfVQ*T+*POQh;xEOau!L`u&3xujZk4+K+qkb3PM3Ce{RvtUSQv>im0pEB=d z8sss2uJAIRzM%n-Qx?(`lOUi%us(~J9C>{byroI#ku38>Fr+aNo<8z+HsZPg1!yAH z06w9~{NmZcWT|sybtdbZA9g)Cn9(Q{s@{*%KF)|77X9kF4%b>fr(s^hf`*ocMF3@m zERejd=eoh=nL3)?zXe@DtO7tUTJ5vLNwYO>kAv~bdH2d%oh&SU?3T;)imq!b8}18B z2BoD=HVR1sZgNW(EIM42zPp`ro|l(shd%@z{C-Gm0sJ-%!}yNYexmgo+cdT=-5Uh2 Qo2F^BFQ5GZ-Yz!(3;&hrD*ylh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f1dfa21a9f824a9de725f97417bda33354b14e1c GIT binary patch literal 5045 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 Z$Fa{G&bchx%Y7}ZF10iNfj-hR{|gqu0(k%c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ce004b299e7c5da915de15cb4e8e556e3f54a603 GIT binary patch literal 14431 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 HLR$QPGz%@y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f1dfdf3b9fc9de869f32904999f3c5fa2a8d2ee3 GIT binary patch literal 4411 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4d1c00bada472e9b0f10f5352ddfe7ce7fdec561 GIT binary patch literal 12093 zcmc&)Yj7Lab-oK=@qUp22@({^<(r^IQZHH}Es2sy>SmZGy%=>#uMh_7STd!(+TTwn`q<68Cxqk zV;^ADH;9fp(n@%1UVE>&ddm6q|iORSPC}}>wv9y z$QdFsJP~y~1H;q#iD+mdO@y!E4QE)Mg`e?^@eIpCBi?kzw9Aa_zifD!5zSAiXDQUP zJfWVoP|pf)t>tZxd8@rp%SLNBpcZmtk@Kvb^PKmsYim zL$_8|x@zsAt+qYL3<>Ba7CVk4lQVIFgUR3`q7a4677NEEE|HYENHQ@Un^}O;fK@a> zZ%XY9tsRnODO6Tl#GxqFQ?P@ll04nPBk>q?r8o)=BI>PC-LH!2)v%a=eXJPHCX;cc zT8InhghV`iRtkmZW1&}s^NP2;>|8ho590!CdxIpzrzKn~PC3;dlmt=gpNmCi!$Q0- z636xIA5BIU@YG2C^J4O4Ap*p?SX4-c!jXs|NwKrBcuYRue>NO>MMy;Z!}Ig~&<$<` zsUJqxH-BC+heEMLOb&$IT zXm7f2ul~}+TNCMqLwD>W`Ie5<=pB30BO~bue5WoK7|jGmv+ZLk!-wXUhtLz5P@Lgt z=$tHvVA^FmckysyW+6NyKuJ_ktml%zVJH!v6BMH;$O~d3Qrtq&0dBW1!tdc#n4C7k z!M1IJ%`yo^n^SSa`U%#+jlLp% zAK&Zg;@0GK+J+SvGnp8IUsVe!9TF1>zeGsF@B}`N(a~+<0dI?(?tueq*D6dF50h2` zGEpBiILXc;KOBzzlZ*`JN1ZP-u@gzx40D(;95l2Op+HSD(`28%nM^ag_0vqiuz1;e zL`Vo?SQfbP=XP>o6#v|`n4Bw+Q9+7`F$x&m3sX~gI&qs=`SS^RR*+(gg2V~o$SfCG z5I9Mg4`WyH^ISL);KN1zf1HRm`v3D7Ysh}^3&d6tu7b8r%^aFQghOc>B-WAt1; z3+IxX4#(qoVyqL3^%F@dFA9gIOinCzmh~_VhgOJg6Rn6m}^qH`NSRf zvlaXH&3@>f&D*Q~d(8s<{8o~H?Khc`TINqW4+hA$n1daLZ|i~jTP*u7mR+x54t5!? z*JA1QdMv)~w>lJc=XqI9(0X3^*43Q2y48lHy~|d;K5BZ3k_~M7%XwJSmMz` z$p-0#XNpR%R**ER^n#OC4#`jfQ?LkzaQ+S2Nsij!Bp2iroZ4a?f$1j+me(bWr*YY7 z3K}%&;vq+WO2`60`uuA_Gq1-g3CpRk482WAcsC)ZzS0Z8oUooQLiwb*&`H^)m7IpR zt0`#Z4ZYBf_6D9EWD>R@TM5>KHmw#&6-&Yh92k*#R*xg6@$AwRESB>2UYPNO>r|4C#W?;mLTrF|K~@`Un1lV$ zsJ4Sqvt^t&X{DuWNiEaV_JsSSx2#RYRp$+Kb=^-Ib#d*gg6_$d(pxnuSOv4;;GKhd z7(vx!n^s=BYCW_QK*E2bc;Q+c*#H*{63W;z=}YDJj$TOsoQQJs;RTfMX9f9HK}f)@ zB!OZf{iYd2xr#JpLJtuH1b%WM5&d_K3B@YOVNnhNS_lvR15Vm|V2wl<0As*_Q4DCj z1oR5ir!XxFGc5K&y9q@!;%9Xc-y(y^0@9y(R@(!!j=3Ul-FdBrHaHlGw_#SCD14zVUs zT@g_X$SNprh}R)Wwbvx2HnJd!z-CA-pHE6LI#Wzc(L)2J3T6`xxYDdDZaOef@ru)G zDwK%5IB;tLox*Zhacp>a_H>B^fV~V)zBqH z!3`OOM6nrP*II^rzXQZ7Oz(ht(w~Bmrl40%+}@n4HREc%v_I?Gkuv<;<+)qECEd6` zTRoh%53d=Ce@n`lZ)s1Po9@>(Uow0p`PSjo!FycWrDrqT_O!hbs<>OSuEBiUu1wo_ z+Rfedcjo-PxBb1}m(tIixE0MjGnw7ZXZ%x3ro6i;=kCb3J1))Karfl|Luq#>wDFGKWcjCFe1GoDQWcvL9Pm8UNlCmeeMr*X=1s-t5Vl{TZ`=W#84BYsNIvNA9_{l{R;+_XESfmQV+I^H9}c7jwh64RV0r!w%z3^N0~{Dx>WW0Ym;T(H2kz59y|K zs;wwjS-@i>;IZj+QH$Py$F~Z2{8rFXz+>yF%SAlCO!3%ux+sq)i-vRQm4HJ?zX*Ty>U>=MTfx z5;B^33*f5jF(Y(U#MNRcZ>6~E4w`~)imaybe^HUO3`>n=++i+2S%<0x%tk3Q#p66yzn-Bt#8$aS$uuVGxHfdj_+;nC*jXqisX+snmgZ0IMCu>=0%n zn2lmKh8a2@#3PUeJe2@RJPMB#v+6B~MMbcJiFh2d6PP`V**Insn4QE72P-~@+4Go9 zV#Z@O1(}3u;s%gVZ&aw@;Gxp_aZphj*F5$X)!@SYWVrCwV z4M6g3UAeZAOxwt{(QF%^YvchRjQ>k~NcRjEAfgu_;?WM`^RM)MXKSu!@9mzw*`EE` z+ToP#_XmSE(lo_rXwW(qU~Y7KG2hFKbs2B$JM2LNB3zk~$sK1;IfL!pl%rv$A+EnPWxgO^KgL^v-Qcyq+rS`b4=Q zfUbf~2}~4*H-gD#1Rz5VX;Yf4*;D|xL1WMm1b6kI9w5UsSu}x4HQNalNan@ClCOpf zbEvU{=M@h>IA}!F-(^i3->l9rx`ae;knf!kK;@3_=p7B{s8d5Rpg(YCCruBBsVD(?z&Zx^%>J+_J)F=Km@C1AjelNoBB>WD*FVI4^63Vxs-1}S<;vm?kbsT9X z9Q>WstKR1dW7^P~BQv(&r{5V{nOEDr*sh)N#e$?$CEHlZv3&5kV~bm7WqDp2>W5fN ztgoan^u?0>=eGCbG3=F-y{b6qUsMg@{<(Q!X3-4}fOsrI-4OjROUVR?Gvh&s^UTYO zc91>jBJZ6#KQAm=UyJoZ_q`Zgs#V_+np=p=5DdaFOFVRL$Bep7?x}KgDqv77V4thu zA~etEAPOZy=qLnHD+%PKB%cR2LKV1HLR7#CqWUDDr_xC=&q3fO9*RX3r>c%=;oFIT zPB9?2ifL8|N5LF_a0}kO=qsBS(5b-bjxO{eZc)$masdk!#UkosiYXjR;-o1KHPXgo zU<*Al;AsH22(T?LNQyy(-c?E20U?k2r^ARsr4X zA)J2+52q$NRnNHy=j`#iAC;Y@rTk~8E7@TOqWrw)-n)8g^?2HOB&7$X&exXT(s$S2 zn)452`~x5eJ=TOl=i-~?trF0@{RtaVcR9NBNV*~^S1ay4LxMU@8ifIUg1YuUM6F~>YGpeFu@~Eq2y$6?iKAzY zJ2na~Op()qsET*T_PK4_rXq8@rsC&DKf5RP%BK#F!$~y(V@g$Nr>f?gBu1ee9@7I; zR5K(*QaA>x^6)8aigsk-JA%H7mf(G;C3QgddGd2xP2ST8j;{MIU(OZCxB@v>Z^i`% zPI@1obxozsQ(`@KYAXu_N6OW0N$9{A1Bj1tl(OhZ zG1fT*3h)so4A$VOcC=$Z?oA06RA4^c`XEjemaX&yUzho1A+9~oV1xP z_F*oK0cO|o_?ZnyKFPeoJiZDu0QP#&G6^w{&FRT2qjAQS&F;r=Rrh;v1^aTGa#mc! zcHmQAiBIq-!Iwpq^s%5t;}G;ny+(t6!j@+8WB7$#afBI%JsQ-5EU+#Pfbk&-6=E6~ zGRIIjV3`Qs@%xE zn1Mz@D6|Gls!NBJKogOvx26y+Q9}{#LR14oCxFXy^VAIl_@9g=;KLd!VBW%RSTwik>Z?%tgTN1w9^XG}Yr*3)A#r@0s zv!1pTdeGh8rPs2q=1aS-?D*!;Rqqd)-*3KFo7;Xgv;An^UA6T5vT=#|k-H9S`c~R6 zypeBi`^Mr|7q4_>n|CbPX^479rg6vBz1NuchjTlRW_BLU?mV7tJh5cWHy~DL8n#{C zvKoHBIk)|AX8Ymn_M_Q`V@v1{s;XAI&o?$hkyP11txMxQB6AlE(@5a^YPHcL$3io7Yj}2h6{qZUK6m-<4ob1}fI1!a5B8 z6W#!R8u|esT&bX&pGxRYI!llMf|x<`P&=Fg146xmFyzfz4G6avg+7Q++I#UCTgH!3 zE9H&U$q&Ap4J|1@TAh<%bZ!V76vYK^!JvCt%c8LaE^TQ_e=;TH^VGf!> zXxJAyFci)SqD;T5DJ~CsW7P1eq%cDdaB)O5tQBS(6&B)`;AYW(ECJEMSQG_{dc0BX zfNj-YnEb{ znB{o3x-Dh8yS4wS<=Ydh&u6#pe_L7^y!8B6_ulb!Upok{5hH=Ou@{c7cr*6KyLQ)- z>B6%sJAhu-GmfTvcF#Jib9kYDNBzpv8GFmRN$+S~(-TWA7!&Tsw0R3Xf#5px!U(Cm z3A9I+de-1`+=+k}zxzXn8hrO>4TZpZTZkiV4~0Gp7GmLvD+J+VQIb*eCX%6$=)+mn zg9}f@J)*E@F{CU477`js6*;8P(L^mKk!p<~+QCjUL}L+|h7uHZI-U&6B3+@H5XOb& zSR@pd!7+Fi*FO|`k5Kki)e{B2h?xU^(sQtEe@6aGXJHRBYgc5&Z;#Qu7yTOl^Wrl+6bvprhyGJS0if#uh)3>dMo&d z-med_Y>>ISbB*Bj+RLL>zxdKGXyKZ(o%LR6TO)9}+J04jzvmH>YtJ%uY$KM# z1U)Bh@V(IOGmSo#?Ko+GMq6mjq8Nw2Q0)@ LeE8@X?LIk$INIjy6nk^DwVl(cRi`pxd`K?tN( z)xLG5o%v?=n{Q^mnfd*`*=?80j^K%9-kxpoA@nC)&>v$#*ycgFjc7y@BWRcqh+%_Z z7$yahLbQP6>e}bHdZjIX4XPs`{y9sMw3V&Xe1bDLLg~_V4O73LtEfA!fQQl0bMg~`-+z904@7hv`T1) z;D*UNm9jojWTk6Nf{QZ}m*i<}Hc#j4H4GQ;~rc( z<#@vZI)+_3WW>;*V64WLkLai=LU_#R@nPQ zO#6+Jw&|8|@3bBIFPx2wh<40^zitq>$uW}?@#cuR*jwpAvxt~=d(=T=cj_8NYyauk zUAiT8>?PkYc1y%k?Cs#OTXcH|j@_+m=+@%kp+RC4{JqQjw!OXwZ|(1Wk8V$$LFqTl zz#6gc>9g;^H7M>53Xd&es?v31Nva@IGc3;v4185lPnzXK-1`N^mSFgq6f?t$ihhNXAlgEH8=Sc_De7je&3j9F>eRu^0=hKgA_D z=~j4(iM<394#T{|g=hd2iRsFq@yCZVb>Gkn!)j8Q(@u|Bk@ASSk`o^eS+4IQR zoA(8lk1Y1cPHM}HDw=bF)=%XE=NE?Zm38-y-95H4o2?8ljQqh@llPR} zaV@!Is(1aB_3LuY#Ydju4?O{B?WxPT8#3;OmG9-;9T|7WGC8PHd3U4YoY;hred<5f}_Up9@~f z1TP(IupS$%_Ac7K1cTK}UAoCo&foIcSya(aYz+{og1YCp>sT3B4Q2z!bAc0?z=^e% z4I&F3bmbj^<@T(jMt6Hv``YM6z1)6bBPmy2d*uAqCvNZJsW)F;W-{*Dl``3VRJI=d zOAol^GcQa9l4|eY!~t0C*ZXZlMq<-J4EC5eoqg3%cn~6nkoiG#8_2(@JqGFfc4Ek2 ze&2yr->>LvgzOxjr}OL7&`GJgiS@t z+38A;op;X%&xSVP`9K?dtoA&58{RlkqJJ94IJD;pqMd**1@oW3wkznXz(fcAm}$#6 zTz2+85i@P2ZAPS1BlwqWK)d?$?`?tLDznoc58(}M@BTIrGpvx>lS5mf8EYMy0J7E| z3u0h8ymca=*Nh+5rJbC1suo-kD~zKU#6kdpXHYUxt!qY#VFn(rXsAWBmACd;8w&X} zF^zi2S~N|lgziqCyry9w@HyrwjnOIg6<}tOV)!@()C#~l&kBiKm~7`532673RcF>` zrWvt299D?1f}T}U(_Dh3_@o4?DcqQ+M7V|lA*cPt%Sn=_rWw$S14$W9Q)5pdT!V`j zB&LS77cx$2Vdzue;X;e9^@d-EeihmnekjN1bO^_{oIB;72C*Q>wOH| zdg+Zz^5NFi^lD-~xPE25U#>X!$k~@KqZTLSx{i0~_0nHnTN{#nJ->7I{C&#=t^Og7 zfzMud4KxsoeMkC(=)F?}$nSObS)g#g7Z>iIr9j@SB|+W{+6U^$&6ehYD)K=Y0rG=# z6VyMbGGVz6>+BEd-v2{7cp4S|_W-1k<3&(cKdFgM4~!eYbYBhHd>rWIGC^D5_HU%E z>YYf3UYklLMgZiW0U*Pf(f}FsoNzC4wHq;Y&XfW$7ipvb%%%yrTWQbuAzj+Z=@QkF zJ7U@gm^~Bax@N?*n@Ch(F6B*qCIDtN-vi9vboUN0^AubIzK+bZj7SMABPMxf3Q$)= zX^CS+p%r=+!Z>Ne$qAgme+$GR!=xn-1!)$zS5a;lnt|`I2!~)o} zbMw-z5cfIY2H>&^?CRx$5oTDb312U!)8y#+kkEn6V@eiIK{6I{3Z1xsflGKECtWzf z=qH?p1pjlwsH+%7F3k$(aD5+6&f}yXl8|`^Km`n58gyE)espe1xCm7u4p=l?)IhNo z%Kru^7Vk1KY^3xXK+$LG&Hpbz5#nA~>02XjjQnI2#E$3R?RvLsF@A?z;#N+7P!`^( zTNuo{%a;8`c{-~n(PBca?{=1f0o?rBCygcHn@g1RpbMWb5#D1)pOay?HLqM?fw&>CYlET literal 0 HcmV?d00001 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 + + + + + + + """ + + 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 0000000000000000000000000000000000000000..346a0ca13dbfc1728b8f5891be78dcddf3e5f432 GIT binary patch literal 7294 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 V(c)nI*P2!de6F|r0zbyO{|DSwY;phq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..901e6fa300c2785e4b45fbe2d0813e7707ac8fea GIT binary patch literal 6960 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8049562a610f39b15fc1b959b6fe056bd6a0eb31 GIT binary patch literal 14299 zcmch8Yj7J!cJ2%YkHL!s0g?b8AV=Us1fQZvN+k6n_>@SA(qaIK6cs@rI3!__06hap z7Lt2cS?58glDpbhS@zAY>CIM_&Za6%?%oRXE3{*0RTVoK81w>}rE_%?=ZEY5xX>fJ zUVE!@Pd6|CNK-4VZz@wnPtVNh)2B~&pFaJa<|Cs~N5Iwm4{t2~MH@l<3wn?*bvE

ia92*K$2GhLe^X7+<62$| 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 M@Hh<$Ujcpo3ZtixlmGw# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2f62cdb262c659e7824d24746bf3c78a79d23232 GIT binary patch literal 860 zcmb7?y>1jS5XZfHAA5J3y^lo7392nl6f!>j{_QFNL$#VZEVvHc4!y7sn4~JBih3r?PH$~a6n^>=^pOU1QR;MAsyj} zj&V#UI7vxH=Is{&?k_vD?z@L4P{0ZdZqka23%-Q(RAKtOg7PL~kPBHZQ(oq&KP!b! zg(?aVMym2h;B3x1Hs)2unzS7mxdO%|LwK_WZR|lEI9Cf+p%S3AD`*evpf(DfFjEyE z_Z989jtgfEH_UK-F&o&y5)AWB0PR$k+TeNxrC~WZuQ`-Lg0_dZ5~o}nUPGiq44GvGPpX8A$4ZJmW(iC)Zy9+m=+m{%jYc;{P zN5`Y<5qb1H`W}8RF!tEM6LE@cjiaZ$H>Aq#0X*}_ek z>sRKwl9}D?WsJ#E8pe*QjqTlEseE0)1H9wp{BY@e4nJE#5PT(1zLV+;sjk8}2(QQv OzwVCX@O^|+ckmC8o&bUX literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..433fbd85f90dc49d0f9133df1be4d561e24c32d6 GIT binary patch literal 13840 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 N7H7# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c463124d67016823b447bf059514df9c045e620b GIT binary patch literal 12517 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 # 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 ( +
+
+
+

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() %} +
+ {% 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.

+
+ + +