diff --git a/.env.example b/.env.example index 8fbe2d11..5d71b417 100644 --- a/.env.example +++ b/.env.example @@ -70,7 +70,9 @@ LOG_LEVEL=INFO # Bearer token required to access the Prometheus-style /metrics endpoint. METRICS_TOKEN= -CELERY_TASK_ALWAYS_EAGER=false +RABBITMQ_URL=amqp://digest-engine:digest-engine@rabbitmq:5672/ +TASKIQ_ALWAYS_EAGER=false +TASKIQ_SCHEDULER_SKIP_FIRST_RUN=true DJANGO_SUPERUSER_USERNAME=admin DJANGO_SUPERUSER_EMAIL=admin@example.com diff --git a/.env.test b/.env.test index 0e0db260..736dfefc 100644 --- a/.env.test +++ b/.env.test @@ -6,8 +6,7 @@ OLLAMA_URL=http://ollama:11434 REDDIT_CLIENT_ID=client REDDIT_CLIENT_SECRET=secret REDDIT_USER_AGENT=digest-engine/test -CELERY_BROKER_URL=memory:// -CELERY_RESULT_BACKEND=cache+memory:// +TASKIQ_ALWAYS_EAGER=false ALLOWED_HOSTS=localhost,127.0.0.1,nginx,testserver NEWSLETTER_API_INTERNAL_URL=http://127.0.0.1:8080 NEWSLETTER_PUBLIC_URL=http://127.0.0.1:8080 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b14c4e47..0be4e8b3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,6 +1,6 @@ # Digest Engine Project Instructions -You are working in Digest Engine, a Django + DRF + Celery + Qdrant backend with a Next.js App Router frontend. +You are working in Digest Engine, a Django + Ninja API + Taskiq + Qdrant backend with a Next.js App Router frontend. ## Repository Shape @@ -22,14 +22,14 @@ You are working in Digest Engine, a Django + DRF + Celery + Qdrant backend with - Project scoping is a core invariant. Most API resources are nested under `/api/v1/projects/{project_id}/...`. - Treat `core/` as the home for genuine cross-cutting concerns only. New app-owned runtime logic should live with its owning app rather than expanding `core/`. -- Reuse the established DRF patterns in `core/api.py`, `core/api_urls.py`, and `core/serializer_mixins.py`: - - `ProjectOwnedQuerysetMixin` for nested viewsets - - serializer context containing `project` - - explicit validation for cross-project foreign keys -- Keep viewsets and views thin. Put operational logic in `core/tasks.py`, `core/pipeline.py`, `ingestion/plugins/`, `newsletters/intake.py`, or nearby helpers owned by the feature's app. +- Reuse the established Ninja patterns in `projects/ninja_api.py` and the app `*_ninja_api.py` modules: + - keep most resources nested under `/api/v1/projects/{project_id}/...` + - validate cross-project foreign keys explicitly in schema helpers or endpoint-local validation code + - keep path wiring thin and put workflow logic in app-owned helpers +- Keep route handlers and views thin. Put operational logic in `core/tasks.py`, `core/pipeline.py`, `ingestion/plugins/`, `newsletters/intake.py`, or nearby helpers owned by the feature's app. - Preserve existing API field shapes. Backend serializers and frontend types currently use `snake_case`; do not introduce ad hoc `camelCase` transforms. -- When API behavior changes, update drf-spectacular schema metadata in `core/api.py`. -- When changing ingestion, newsletter intake, AI processing, or embeddings, preserve the handoff between database state, Celery tasks, and Qdrant state. +- When API behavior changes, update the Ninja schema, response typing, and docs metadata in the owning `*_ninja_api.py` module. +- When changing ingestion, newsletter intake, AI processing, or embeddings, preserve the handoff between database state, Taskiq tasks, and Qdrant state. ## Frontend Conventions @@ -85,4 +85,4 @@ Use the workspace skills in `.github/skills/` when they match the task: - `bridge-scaffolder`: features that span Django API work and Next.js consumption. - `project-api-patterns`: adding or changing project-scoped DRF endpoints. - `source-plugin-patterns`: adding or changing ingestion plugins or source-config behavior. -- `ai-pipeline-patterns`: changing embeddings, relevance scoring, newsletter intake, or Celery-driven AI workflow behavior. +- `ai-pipeline-patterns`: changing embeddings, relevance scoring, newsletter intake, or Taskiq-driven AI workflow behavior. diff --git a/.github/instructions/backend-python.instructions.md b/.github/instructions/backend-python.instructions.md index bf15cc52..132f55eb 100644 --- a/.github/instructions/backend-python.instructions.md +++ b/.github/instructions/backend-python.instructions.md @@ -1,6 +1,6 @@ --- name: "Backend Python Guidelines" -description: "Use when editing Django, DRF, Celery, plugin, management command, or backend test code in Python. Covers project scoping, workflow placement, docstrings, and focused validation for core/, digest_engine/, tests/, and manage.py." +description: "Use when editing Django, DRF, Taskiq, plugin, management command, or backend test code in Python. Covers project scoping, workflow placement, docstrings, and focused validation for core/, digest_engine/, tests/, and manage.py." applyTo: - "core/**/*.py" - "digest_engine/**/*.py" @@ -11,13 +11,13 @@ applyTo: # Backend Python Guidelines - Preserve `project` as the scoping boundary. Do not reintroduce `tenant` naming. -- Keep Django views and DRF viewsets thin. Put operational logic in nearby helpers such as `core/tasks.py`, `core/pipeline.py`, `core/newsletters.py`, `core/plugins/`, or focused modules next to the owning workflow. -- For nested API resources, follow the patterns in `core/api.py`, `core/api_urls.py`, and `core/serializers.py`. -- Enforce cross-project relationship validation in serializers instead of trusting the client. +- Keep Django views and Ninja route handlers thin. Put operational logic in nearby helpers such as `core/tasks.py`, `core/pipeline.py`, `core/newsletters.py`, `core/plugins/`, or focused modules next to the owning workflow. +- For nested API resources, follow the patterns in `projects/ninja_api.py` and the owning app `*_ninja_api.py` modules. +- Enforce cross-project relationship validation in request schemas or endpoint-local validation instead of trusting the client. - Preserve existing API field names in `snake_case` unless the contract is intentionally changing across backend and frontend. - Use Google-style docstrings with PEP 257 conventions for public modules, classes, functions, and non-obvious helpers. - Keep changes small and local. Do not create generic `services.py` or `utils.py` files unless the repo already needs that extraction. -- When changing ingestion, embeddings, newsletter intake, or review behavior, keep the database, Celery, and Qdrant handoff coherent. +- When changing ingestion, embeddings, newsletter intake, or review behavior, keep the database, Taskiq, and Qdrant handoff coherent. ## Validation @@ -30,7 +30,7 @@ applyTo: ## Good Anchors - `core/models.py` -- `core/serializers.py` +- `projects/ninja_api.py` - `core/api.py` - `core/tasks.py` - `core/pipeline.py` diff --git a/.github/instructions/dev-server-workflow.instructions.md b/.github/instructions/dev-server-workflow.instructions.md index 5c5b24e7..a87260a4 100644 --- a/.github/instructions/dev-server-workflow.instructions.md +++ b/.github/instructions/dev-server-workflow.instructions.md @@ -11,4 +11,4 @@ applyTo: - Assume the user usually already has the relevant dev server running. - Before running commands such as `pnpm dev`, `next dev`, `vite`, `npm run dev`, `just -dev`, or similar long-lived local servers, ask the user to start the server or confirm that no server is already running. - Only start a dev server yourself when the user explicitly asks you to do that. -- When you need runtime verification and no server is available, prefer asking the user to start the correct app server instead of launching one on their behalf. \ No newline at end of file +- When you need runtime verification and no server is available, prefer asking the user to start the correct app server instead of launching one on their behalf. diff --git a/.github/skills/ai-pipeline-patterns/SKILL.md b/.github/skills/ai-pipeline-patterns/SKILL.md index 122d3282..00d8d7d4 100644 --- a/.github/skills/ai-pipeline-patterns/SKILL.md +++ b/.github/skills/ai-pipeline-patterns/SKILL.md @@ -1,6 +1,6 @@ --- name: ai-pipeline-patterns -description: "Use when changing the AI workflow, embeddings, Qdrant integration, newsletter intake, relevance scoring, summarization, classification, Celery handoff, or Anymail inbound processing. Trigger phrases include pipeline, relevance scoring, embeddings, Qdrant, newsletter intake, summarization, classification, OpenRouter, and review queue." +description: "Use when changing the AI workflow, embeddings, Qdrant integration, newsletter intake, relevance scoring, summarization, classification, Taskiq handoff, or Anymail inbound processing. Trigger phrases include pipeline, relevance scoring, embeddings, Qdrant, newsletter intake, summarization, classification, OpenRouter, and review queue." --- # AI Pipeline Patterns Skill diff --git a/.github/skills/bridge-scaffolder/SKILL.md b/.github/skills/bridge-scaffolder/SKILL.md index 85679292..43d2aedb 100644 --- a/.github/skills/bridge-scaffolder/SKILL.md +++ b/.github/skills/bridge-scaffolder/SKILL.md @@ -1,6 +1,6 @@ --- name: bridge-scaffolder -description: "Use when creating or changing a feature that spans both the Django API and the Next.js frontend. Trigger phrases include full-stack feature, add endpoint and UI, wire frontend to backend, project dashboard change, and bridge serializer to frontend types." +description: "Use when creating or changing a feature that spans both the Django API and the Next.js frontend. Trigger phrases include full-stack feature, add endpoint and UI, wire frontend to backend, project dashboard change, and bridge backend schemas to frontend types." --- # Bridge Scaffolder Skill @@ -9,20 +9,20 @@ Use this skill when a change genuinely crosses the backend and frontend boundary ## Rules -- **Django Side:** Follow the existing patterns in `core/api.py`, `core/api_urls.py`, and `core/serializers.py`. +- **Django Side:** Follow the existing patterns in `projects/ninja_api.py` and the app `*_ninja_api.py` modules. - Most nested resources should stay project-scoped under `/api/v1/projects/{project_id}/...`. -- Keep business logic out of viewsets. Use `core/tasks.py`, `core/pipeline.py`, `core/newsletters.py`, `core/plugins/`, or nearby helpers for real workflow logic. +- Keep business logic out of route handlers. Use `core/tasks.py`, `core/pipeline.py`, `core/newsletters.py`, `core/plugins/`, or nearby helpers for real workflow logic. - **Next.js Side:** Update `frontend/src/lib/types.ts`, `frontend/src/lib/api.ts`, and the relevant pages, components, or route handlers under `frontend/src/app/`. - Preserve the existing `snake_case` payload shape unless the backend contract is intentionally changing. ## Implementation Guidance -- Check `core/api_urls.py` for the current route topology and `core/api.py` for the schema helper patterns. -- Keep serializer validation aligned with project scoping and cross-project relationship rules. +- Check `digest_engine/ninja_api.py`, `projects/ninja_api.py`, and the relevant app `*_ninja_api.py` files for the current route topology and schema patterns. +- Keep schema validation aligned with project scoping and cross-project relationship rules. - If the frontend consumes the new field or endpoint, reflect it in `frontend/src/lib/types.ts` and the corresponding API helpers. - Update docs when the feature changes a core workflow or user-facing behavior. ## Related Guidance -- Use `project-api-patterns` when the backend portion is primarily a new or changed DRF resource. +- Use `project-api-patterns` when the backend portion is primarily a new or changed project-scoped API resource. - Use `coverage-auditor` immediately after scaffolding to add targeted backend and frontend tests. diff --git a/.github/skills/coverage-auditor/SKILL.md b/.github/skills/coverage-auditor/SKILL.md index d022c9e4..dbb24516 100644 --- a/.github/skills/coverage-auditor/SKILL.md +++ b/.github/skills/coverage-auditor/SKILL.md @@ -1,6 +1,6 @@ --- name: coverage-auditor -description: "Use when adding or updating tests for Django, DRF, Celery, admin, serializer, plugin, or Next.js code, or when closing a coverage gap. Trigger phrases include add tests, improve coverage, pytest, vitest, missing branch, serializer test, admin test, and route handler test." +description: "Use when adding or updating tests for Django, Ninja API, Taskiq, admin, plugin, or Next.js code, or when closing a coverage gap. Trigger phrases include add tests, improve coverage, pytest, vitest, missing branch, schema validation test, admin test, and route handler test." --- # Coverage Auditor Skill @@ -24,6 +24,7 @@ Use this skill to add the smallest effective tests around the changed behavior. - `core/tests/test_tasks.py` - `core/tests/test_newsletters.py` - `core/tests/test_pipeline.py` + - the owning app's `tests/` package for Ninja route coverage - For frontend work, add or extend the nearest colocated `*.test.ts` or `*.test.tsx` file beside the owning route, page, or component. - When adding new frontend tests, keep imports sorted to satisfy the repo's ESLint import-order rules. If you hit `Run autofix to sort these imports!`, fix the import block or run file-scoped ESLint before moving on. - After changing tests, run the narrowest relevant validation command first. diff --git a/.github/skills/project-api-patterns/SKILL.md b/.github/skills/project-api-patterns/SKILL.md index 2f1f6fab..a893d84f 100644 --- a/.github/skills/project-api-patterns/SKILL.md +++ b/.github/skills/project-api-patterns/SKILL.md @@ -1,33 +1,32 @@ --- name: project-api-patterns -description: "Use when adding or changing Django REST Framework serializers, viewsets, nested routes, schema docs, or project-scoped endpoints in core/api.py, core/api_urls.py, or core/serializers.py. Trigger phrases include project API, nested route, DRF viewset, serializer validation, project_id endpoint, and drf-spectacular docs." +description: "Use when adding or changing project-scoped Ninja routes, request or response schemas, nested routes, or API docs in `projects/ninja_api.py` and related app `*_ninja_api.py` modules. Trigger phrases include project API, nested route, schema validation, project_id endpoint, and OpenAPI docs." --- # Project API Patterns Skill -Use this skill when changing the project-scoped REST API. +Use this skill when changing the project-scoped API. ## Rules - Treat `Project` as the isolation boundary. - Top-level project resources live on the base router; most other resources are nested under `/api/v1/projects/{project_id}/...`. -- Reuse `ProjectOwnedQuerysetMixin` in `core/api.py` for nested resources. -- Pass `project` through serializer context and enforce cross-project relationship validation in serializers. -- Keep API field names in `snake_case` to match current serializers and frontend types. -- Update drf-spectacular metadata in `core/api.py` when the endpoint contract changes. +- Reuse the existing nested router structure in `projects/ninja_api.py` and the app `*_ninja_api.py` modules for nested resources. +- Enforce cross-project relationship validation explicitly in schema helpers or endpoint-local validation code. +- Keep API field names in `snake_case` to match current backend payloads and frontend types. +- Update Ninja request schemas, response schemas, and OpenAPI metadata in the owning `*_ninja_api.py` module when the endpoint contract changes. ## Implementation Guidance -- Add or update serializers in `core/serializers.py`. -- Add or update viewsets and schema decorators in `core/api.py`. -- Register routes in `core/api_urls.py` using the existing nested router pattern. +- Add or update request and response schemas in the owning `*_ninja_api.py` module or nearby helpers when the logic is shared. +- Add or update route handlers and OpenAPI metadata in the owning `*_ninja_api.py` module. +- Register routes through `projects/ninja_api.py` or `digest_engine/ninja_api.py` using the existing nested router pattern. - If the frontend consumes the API, update `frontend/src/lib/types.ts` and `frontend/src/lib/api.ts`. -- Add or update focused tests near the changed behavior, usually under `core/tests/`. +- Add or update focused tests near the changed behavior, usually under the owning app's `tests/` package. ## References -- `core/api.py` -- `core/api_urls.py` -- `core/serializers.py` +- `digest_engine/ninja_api.py` +- `projects/ninja_api.py` - `frontend/src/lib/types.ts` - `frontend/src/lib/api.ts` diff --git a/.github/skills/source-plugin-patterns/SKILL.md b/.github/skills/source-plugin-patterns/SKILL.md index dbd0a502..fabe1536 100644 --- a/.github/skills/source-plugin-patterns/SKILL.md +++ b/.github/skills/source-plugin-patterns/SKILL.md @@ -20,9 +20,9 @@ Use this skill when working on the ingestion plugin system. - Update `core/plugins/base.py` only when the shared plugin contract must change. - Add or update concrete plugins in `core/plugins/`. -- Update `core/plugins/registry.py` and any related enum or serializer validation paths. -- Confirm `SourceConfig` validation still works through both the serializer and admin paths. -- Add or update focused tests in `core/tests/test_tasks.py`, `core/tests/test_admin.py`, `core/tests/test_serializers.py`, or a new nearby plugin test module. +- Update `core/plugins/registry.py` and any related enum or request-schema validation paths. +- Confirm `SourceConfig` validation still works through both the API validation layer and admin paths. +- Add or update focused tests in `core/tests/test_tasks.py`, `core/tests/test_admin.py`, or a new nearby plugin test module. ## References diff --git a/.gitignore b/.gitignore index 1853ddca..d2b5cc70 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ __pycache__/ **/.turbo/ *storybook.log -celerybeat-schedule* coverage/ db.sqlite3 docs/_internal_only/ diff --git a/.python-version b/.python-version index 3a4f41ef..24ee5b1b 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 \ No newline at end of file +3.13 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 0b20feeb..3473ad21 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,4 +3,4 @@ "astral-sh.uv-vscode", "ms-pyright.pyright" ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 7075b749..a790dd03 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "abangser", "Aptos", "ASGI", + "asyncio", "botocore", "bsky", "buildx", @@ -60,6 +61,7 @@ "pyasn", "pylint", "pyopenssl", + "pyproject", "PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED", "pytokens", @@ -78,6 +80,7 @@ "solomonstre", "srgb", "svix", + "Taskiq", "topicv", "txaio", "ujson", @@ -96,5 +99,6 @@ "xrpc", "xxhash", "zope" - ] + ], + "git.ignoreLimitWarning": true } diff --git a/3rdparty/python/default.lock b/3rdparty/python/default.lock index 413a1d15..31390f1d 100644 --- a/3rdparty/python/default.lock +++ b/3rdparty/python/default.lock @@ -12,13 +12,14 @@ // "Deprecated==1.3.1", // "Django==6.0.5", // "Mastodon.py==2.2.1", +// "PyJWT==2.12.1", +// "asgiref==3.11.1", // "atproto==0.0.65", // "autobahn==25.12.2", // "automat==25.4.16", // "boto3==1.43.8", // "botocore==1.43.8", // "cbor2==6.1.1", -// "celery[redis]==5.6.3", // "cfgv==3.5.0", // "channels-redis==4.3.0", // "channels==4.3.2", @@ -28,25 +29,19 @@ // "cssbeautifier==1.15.4", // "daphne==4.2.1", // "dj-database-url==3.1.2", -// "dj-rest-auth==7.2.0", // "django-allauth==65.16.1", // "django-anymail==15.0", // "django-cors-headers==4.9.0", // "django-import-export==4.4.1", +// "django-ninja>=1.6.2", // "django-storages==1.14.6", // "django-stubs-ext==6.0.4", // "django-stubs==6.0.4", // "django-types==0.24.0", // "django-unfold==0.93.0", -// "djangochannelsrestframework==1.3.0", -// "djangorestframework-simplejwt==5.5.1", -// "djangorestframework==3.17.1", // "djlint==1.36.4", // "dnspython==2.8.0", // "docutils==0.22.4", -// "drf-nested-routers==0.95.0", -// "drf-spectacular==0.29.0", -// "drf-standardized-errors==0.16.0", // "editorconfig==0.17.1", // "feedparser==6.0.12", // "gunicorn==26.0.0", @@ -71,7 +66,6 @@ // "nodeenv==1.10.0", // "opentelemetry-api>=1.41.1", // "opentelemetry-exporter-otlp-proto-http<2,>=1.41.1", -// "opentelemetry-instrumentation-celery<1,>=0.62b1", // "opentelemetry-instrumentation-django<1,>=0.62b1", // "opentelemetry-sdk>=1.41.1", // "ormsgpack==1.12.2", @@ -99,6 +93,9 @@ // "standardwebhooks==1.0.1", // "structlog==25.5.0", // "svix==1.93.0", +// "taskiq-aio-pika==0.4.2", +// "taskiq-redis==1.0.0", +// "taskiq==0.12.4", // "twisted==26.4.0", // "txaio==25.12.2", // "types-Deprecated==1.3.1.20260508", @@ -140,21 +137,184 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", - "url": "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl" + "hash": "2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", + "url": "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", - "url": "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz" + "hash": "c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", + "url": "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz" } ], - "project_name": "amqp", + "project_name": "aio-pika", "requires_dists": [ - "vine<6.0.0,>=5.0.0" + "aiormq<7,>=6.8", + "exceptiongroup<2,>=1; python_full_version < \"3.11\"", + "yarl" ], - "requires_python": ">=3.6", - "version": "5.3.1" + "requires_python": "<4,>=3.10", + "version": "9.6.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", + "url": "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", + "url": "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz" + } + ], + "project_name": "aiohappyeyeballs", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "2.6.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", + "url": "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", + "url": "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", + "url": "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", + "url": "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", + "url": "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", + "url": "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", + "url": "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", + "url": "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", + "url": "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", + "url": "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", + "url": "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", + "url": "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", + "url": "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", + "url": "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", + "url": "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", + "url": "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" + } + ], + "project_name": "aiohttp", + "requires_dists": [ + "Brotli>=1.2; platform_python_implementation == \"CPython\" and extra == \"speedups\"", + "aiodns>=3.3.0; extra == \"speedups\"", + "aiohappyeyeballs>=2.5.0", + "aiosignal>=1.4.0", + "async-timeout<6.0,>=4.0; python_version < \"3.11\"", + "attrs>=17.3.0", + "backports.zstd; (platform_python_implementation == \"CPython\" and python_version < \"3.14\") and extra == \"speedups\"", + "brotlicffi>=1.2; platform_python_implementation != \"CPython\" and extra == \"speedups\"", + "frozenlist>=1.1.1", + "multidict<7.0,>=4.5", + "propcache>=0.2.0", + "yarl<2.0,>=1.17.0" + ], + "requires_python": ">=3.9", + "version": "3.13.5" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", + "url": "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", + "url": "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz" + } + ], + "project_name": "aiormq", + "requires_dists": [ + "pamqp==3.3.0", + "yarl" + ], + "requires_python": "<4,>=3.10", + "version": "6.9.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", + "url": "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", + "url": "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz" + } + ], + "project_name": "aiosignal", + "requires_dists": [ + "frozenlist>=1.1.0", + "typing-extensions>=4.2; python_version < \"3.13\"" + ], + "requires_python": ">=3.9", + "version": "1.4.0" }, { "artifacts": [ @@ -449,24 +609,6 @@ "requires_python": ">=3.9", "version": "25.4.16" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", - "url": "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", - "url": "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz" - } - ], - "project_name": "billiard", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "4.2.4" - }, { "artifacts": [ { @@ -555,91 +697,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", - "url": "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", - "url": "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz" - } - ], - "project_name": "celery", - "requires_dists": [ - "Django>=2.2.28; extra == \"django\"", - "azure-identity>=1.19.0; extra == \"azureblockblob\"", - "azure-storage-blob>=12.15.0; extra == \"azureblockblob\"", - "billiard<5.0,>=4.2.1", - "boto3>=1.26.143; extra == \"dynamodb\"", - "boto3>=1.26.143; extra == \"s3\"", - "boto3>=1.26.143; extra == \"sqs\"", - "brotli>=1.0.0; platform_python_implementation == \"CPython\" and extra == \"brotli\"", - "brotlipy>=0.7.0; platform_python_implementation == \"PyPy\" and extra == \"brotli\"", - "cassandra-driver<4,>=3.25.0; extra == \"cassandra\"", - "click-didyoumean>=0.3.0", - "click-plugins>=1.1.1", - "click-repl>=0.2.0", - "click<9.0,>=8.1.2", - "couchbase>=3.0.0; (platform_python_implementation != \"PyPy\" and (platform_system != \"Windows\" or python_version < \"3.10\")) and extra == \"couchbase\"", - "cryptography==46.0.5; extra == \"auth\"", - "elastic-transport<=9.2.1; extra == \"elasticsearch\"", - "elasticsearch<=9.3.0; extra == \"elasticsearch\"", - "ephem==4.2; platform_python_implementation != \"PyPy\" and extra == \"solar\"", - "eventlet>=0.32.0; python_version < \"3.10\" and extra == \"eventlet\"", - "exceptiongroup>=1.3.0; python_version < \"3.11\"", - "gevent>=1.5.0; extra == \"gevent\"", - "google-cloud-firestore==2.23.0; extra == \"gcs\"", - "google-cloud-storage>=2.10.0; extra == \"gcs\"", - "grpcio==1.76.0; extra == \"gcs\"", - "kazoo>=1.3.1; extra == \"zookeeper\"", - "kombu>=5.6.0", - "kombu[mongodb]; extra == \"mongodb\"", - "kombu[msgpack]; extra == \"msgpack\"", - "kombu[redis]; extra == \"redis\"", - "kombu[sqlalchemy]; extra == \"sqlalchemy\"", - "kombu[sqs]>=5.5.0; extra == \"sqs\"", - "kombu[yaml]; extra == \"yaml\"", - "librabbitmq>=2.0.0; python_version < \"3.11\" and extra == \"librabbitmq\"", - "pyArango>=2.0.2; extra == \"arangodb\"", - "pycouchdb==1.16.0; extra == \"couchdb\"", - "pycurl<7.45.4,>=7.43.0.5; (sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version < \"3.9\") and extra == \"sqs\"", - "pycurl>=7.45.4; (sys_platform != \"win32\" and platform_python_implementation == \"CPython\" and python_version >= \"3.9\") and extra == \"sqs\"", - "pydantic>=2.12.0a1; python_version >= \"3.14\" and extra == \"pydantic\"", - "pydantic>=2.4; python_version < \"3.14\" and extra == \"pydantic\"", - "pydocumentdb==2.3.5; extra == \"cosmosdbsql\"", - "pylibmc==1.6.3; platform_system != \"Windows\" and extra == \"memcache\"", - "pyro4==4.82; python_version < \"3.11\" and extra == \"pyro\"", - "pytest-celery[all]>=1.3.0; extra == \"pytest\"", - "python-consul2==0.1.5; extra == \"consul\"", - "python-dateutil>=2.8.2", - "python-memcached>=1.61; extra == \"pymemcache\"", - "softlayer_messaging>=1.0.3; extra == \"slmq\"", - "tblib==3.2.2; extra == \"tblib\"", - "tzlocal", - "urllib3>=1.26.16; extra == \"sqs\"", - "vine<6.0,>=5.1.0", - "zstandard==0.23.0; extra == \"zstd\"" - ], - "requires_python": ">=3.9", - "version": "5.6.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", - "url": "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl" + "hash": "3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", + "url": "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", - "url": "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz" + "hash": "69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", + "url": "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz" } ], "project_name": "certifi", "requires_dists": [], "requires_python": ">=3.7", - "version": "2026.4.22" + "version": "2026.5.20" }, { "artifacts": [ @@ -864,13 +934,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", - "url": "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl" + "hash": "40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", + "url": "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", - "url": "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz" + "hash": "638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", + "url": "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz" } ], "project_name": "click", @@ -878,75 +948,7 @@ "colorama; platform_system == \"Windows\"" ], "requires_python": ">=3.10", - "version": "8.3.3" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", - "url": "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", - "url": "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz" - } - ], - "project_name": "click-didyoumean", - "requires_dists": [ - "click>=7" - ], - "requires_python": ">=3.6.2", - "version": "0.3.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", - "url": "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", - "url": "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz" - } - ], - "project_name": "click-plugins", - "requires_dists": [ - "click>=4.0", - "coveralls; extra == \"dev\"", - "pytest-cov; extra == \"dev\"", - "pytest>=3.6; extra == \"dev\"", - "wheel; extra == \"dev\"" - ], - "requires_python": null, - "version": "1.1.1.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", - "url": "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", - "url": "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz" - } - ], - "project_name": "click-repl", - "requires_dists": [ - "click>=7.0", - "prompt-toolkit>=3.0.36", - "pytest-cov>=4.0.0; extra == \"testing\"", - "pytest>=7.2.1; extra == \"testing\"", - "tox>=4.4.3; extra == \"testing\"" - ], - "requires_python": ">=3.6", - "version": "0.3.0" + "version": "8.4.0" }, { "artifacts": [ @@ -1439,19 +1441,19 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", - "url": "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl" + "hash": "f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", + "url": "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", - "url": "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz" + "hash": "4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", + "url": "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz" } ], "project_name": "decorator", "requires_dists": [], "requires_python": ">=3.8", - "version": "5.2.1" + "version": "5.3.1" }, { "artifacts": [ @@ -1564,24 +1566,6 @@ "requires_python": ">=3.10", "version": "3.1.2" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f77af37da5cf6ee28f03f283a378a2f5761860e72d1d6f2fa3d50e6286000482", - "url": "https://files.pythonhosted.org/packages/14/fd/c8a185a98f6860b802dc5e663c652efc01cd68f4e046c65f8cb9328c0077/dj_rest_auth-7.2.0.tar.gz" - } - ], - "project_name": "dj-rest-auth", - "requires_dists": [ - "Django>=4.2", - "django-allauth[socialaccount]>=64.0.0; extra == \"with-social\"", - "djangorestframework>=3.13.0", - "pyotp>=2.9.0; extra == \"with-mfa\"" - ], - "requires_python": ">=3.10", - "version": "7.2.0" - }, { "artifacts": [ { @@ -1731,6 +1715,40 @@ "requires_python": ">=3.9", "version": "4.4.1" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "20095f5900bada22ea00cf1a58af50bdb285b2354c61a9d9b47d0dc89ac462d6", + "url": "https://files.pythonhosted.org/packages/21/0c/25f72060a39632fbd2d90e9c8b6052a09cd45b0598fc06c0758d313f0052/django_ninja-1.6.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "d56ae5aa4791068ef4ac9a66cfdf2fc11f507413ded35abb79c51d0d52ad6412", + "url": "https://files.pythonhosted.org/packages/d5/7c/3307e17b872f545c88314b2737a22f965785dfb5a120d739b0131d0492c3/django_ninja-1.6.2.tar.gz" + } + ], + "project_name": "django-ninja", + "requires_dists": [ + "Django<6.1,>=3.1", + "django-stubs; extra == \"test\"", + "markdown-include; extra == \"doc\"", + "mkdocs-material; extra == \"doc\"", + "mkdocs; extra == \"doc\"", + "mkdocstrings; extra == \"doc\"", + "mypy==1.7.1; extra == \"test\"", + "pre-commit; extra == \"dev\"", + "psycopg2-binary; extra == \"test\"", + "pydantic<3.0.0,>=2.0", + "pytest-asyncio; extra == \"test\"", + "pytest-cov; extra == \"test\"", + "pytest-django; extra == \"test\"", + "pytest; extra == \"test\"", + "ruff==0.5.7; extra == \"test\"" + ], + "requires_python": ">=3.7", + "version": "1.6.2" + }, { "artifacts": [ { @@ -1848,108 +1866,6 @@ "requires_python": "<4.0,>=3.12", "version": "0.93.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f4c1e5173952fc86eaf48aff812395c897284859981758ace49856fad2683dee", - "url": "https://files.pythonhosted.org/packages/4f/16/2056b143502f3486b9bdce30f245ecc244e4e37c7119f55cdcad7e73c910/djangochannelsrestframework-1.3.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f797a2b7cdc916f38da32de2a61f708efd2b81ea98afcc3dcc1f7fb4e5269fc3", - "url": "https://files.pythonhosted.org/packages/29/55/a2bae8b7e8a024167e76a131c7c9d4cb406ad131e4d618d0edf06d05f899/djangochannelsrestframework-1.3.0.tar.gz" - } - ], - "project_name": "djangochannelsrestframework", - "requires_dists": [ - "Django>=4.2.16", - "channels>=4.1.0", - "channels[daphne]>=4.1.0; extra == \"tests\"", - "coverage>=6.3.1; extra == \"tests\"", - "djangorestframework>=3.15.2", - "pytest-asyncio>=0.24.0; extra == \"tests\"", - "pytest-django>=4.9.0; extra == \"tests\"", - "pytest>=8.3.3; extra == \"tests\"" - ], - "requires_python": ">=3.8", - "version": "1.3.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", - "url": "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", - "url": "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz" - } - ], - "project_name": "djangorestframework", - "requires_dists": [ - "django>=4.2" - ], - "requires_python": ">=3.10", - "version": "3.17.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", - "url": "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f", - "url": "https://files.pythonhosted.org/packages/a8/27/2874a325c11112066139769f7794afae238a07ce6adf96259f08fd37a9d7/djangorestframework_simplejwt-5.5.1.tar.gz" - } - ], - "project_name": "djangorestframework-simplejwt", - "requires_dists": [ - "Sphinx; extra == \"dev\"", - "Sphinx; extra == \"doc\"", - "cryptography; extra == \"dev\"", - "cryptography; extra == \"test\"", - "cryptography>=3.3.1; extra == \"crypto\"", - "django>=4.2", - "djangorestframework>=3.14", - "freezegun; extra == \"dev\"", - "freezegun; extra == \"test\"", - "ipython; extra == \"dev\"", - "pre-commit; extra == \"dev\"", - "pre-commit; extra == \"lint\"", - "pyjwt>=1.7.1", - "pytest-cov; extra == \"dev\"", - "pytest-cov; extra == \"test\"", - "pytest-django; extra == \"dev\"", - "pytest-django; extra == \"test\"", - "pytest-watch; extra == \"dev\"", - "pytest-xdist; extra == \"dev\"", - "pytest-xdist; extra == \"test\"", - "pytest; extra == \"dev\"", - "pytest; extra == \"test\"", - "python-jose==3.3.0; extra == \"dev\"", - "python-jose==3.3.0; extra == \"python-jose\"", - "pyupgrade; extra == \"dev\"", - "pyupgrade; extra == \"lint\"", - "ruff; extra == \"dev\"", - "ruff; extra == \"lint\"", - "sphinx_rtd_theme>=0.1.9; extra == \"dev\"", - "sphinx_rtd_theme>=0.1.9; extra == \"doc\"", - "tox; extra == \"dev\"", - "tox; extra == \"test\"", - "twine; extra == \"dev\"", - "wheel; extra == \"dev\"", - "yesqa; extra == \"dev\"", - "yesqa; extra == \"lint\"" - ], - "requires_python": ">=3.9", - "version": "5.5.1" - }, { "artifacts": [ { @@ -2057,141 +1973,207 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "dd489c33d667aaa81383ffaa8c74781d2b353d8f0795716ae37fc59ee297b7c4", - "url": "https://files.pythonhosted.org/packages/3e/61/69d951a6e8031389f5feade38531432325019e1047b23d5c6036a86c5e8f/drf_nested_routers-0.95.0-py2.py3-none-any.whl" + "hash": "1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", + "url": "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "815978f802e578fd7035c74040c104909cbe97615de89a275d77e928f4029891", - "url": "https://files.pythonhosted.org/packages/ff/b2/070908ccd41bd6f025f5507d7f6e50009abe8be48393cc697d159b539f0b/drf_nested_routers-0.95.0.tar.gz" + "hash": "23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", + "url": "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz" } ], - "project_name": "drf-nested-routers", + "project_name": "editorconfig", "requires_dists": [ - "Django>=4.2", - "djangorestframework>=3.15.0" + "mypy>=1.15; extra == \"dev\"" ], - "requires_python": ">=3.8", - "version": "0.95.0" + "requires_python": ">=3.9", + "version": "0.17.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", - "url": "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl" + "hash": "6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", + "url": "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", - "url": "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz" + "hash": "64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", + "url": "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz" } ], - "project_name": "drf-spectacular", + "project_name": "feedparser", "requires_dists": [ - "Django>=2.2", - "PyYAML>=5.1", - "djangorestframework>=3.10.3", - "drf-spectacular-sidecar; extra == \"offline\"", - "drf-spectacular-sidecar; extra == \"sidecar\"", - "inflection>=0.3.1", - "jsonschema>=2.6.0", - "typing-extensions; python_version < \"3.10\"", - "uritemplate>=2.0.0" + "sgmllib3k" ], - "requires_python": ">=3.7", - "version": "0.29.0" + "requires_python": ">=3.6", + "version": "6.0.12" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "cf0ef3120cbbe81c1880cbb6b2d9b26cc6d146d07df06de3deb59e7ea5fb78c9", - "url": "https://files.pythonhosted.org/packages/25/7f/a72f5550b3520676e05c43f8da26b4264d994883e93c4c633f79b2f82a03/drf_standardized_errors-0.16.0-py3-none-any.whl" + "hash": "96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", + "url": "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "77b367955af7ed246db12d299a5a75a71eccef54dae4ef69ead29f5fb01be192", - "url": "https://files.pythonhosted.org/packages/99/3f/1a465bde6583355309559dfcfb4e7fdf1b3687f73894d7de8a959c67b50c/drf_standardized_errors-0.16.0.tar.gz" - } - ], - "project_name": "drf-standardized-errors", - "requires_dists": [ - "django>=3.2", - "djangorestframework>=3.12", - "drf-spectacular>=0.29.0; extra == \"openapi\"", - "flit; extra == \"release\"", - "inflection; extra == \"openapi\"", - "keyring; extra == \"release\"", - "myst-parser; extra == \"doc\"", - "pre-commit; extra == \"dev\"", - "sphinx!=5.2.0.post0; extra == \"doc\"", - "sphinx-autobuild; extra == \"doc\"", - "sphinx-rtd-theme>=1.1.0; extra == \"doc\"", - "tbump; extra == \"release\"", - "tox-gh-actions; extra == \"test\"", - "tox; extra == \"test\"" + "hash": "69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", + "url": "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz" + } ], - "requires_python": ">=3.8", - "version": "0.16.0" + "project_name": "filelock", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "3.29.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82", - "url": "https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl" + "hash": "0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", + "url": "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "23c08b00e8e08cc3adcddb825251c497478df1dada6aefeb01e626ad37303745", - "url": "https://files.pythonhosted.org/packages/88/3a/a61d9a1f319a186b05d14df17daea42fcddea63c213bcd61a929fb3a6796/editorconfig-0.17.1.tar.gz" - } - ], - "project_name": "editorconfig", - "requires_dists": [ - "mypy>=1.15; extra == \"dev\"" - ], - "requires_python": ">=3.9", - "version": "0.17.1" - }, - { - "artifacts": [ + "hash": "21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", + "url": "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl" + }, { "algorithm": "sha256", - "hash": "6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", - "url": "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl" + "hash": "f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", + "url": "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", - "url": "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz" - } - ], - "project_name": "feedparser", - "requires_dists": [ - "sgmllib3k" - ], - "requires_python": ">=3.6", - "version": "6.0.12" - }, - { - "artifacts": [ + "hash": "4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", + "url": "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl" + }, { "algorithm": "sha256", - "hash": "96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", - "url": "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl" + "hash": "ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", + "url": "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", - "url": "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz" + "hash": "8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", + "url": "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", + "url": "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", + "url": "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", + "url": "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", + "url": "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", + "url": "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", + "url": "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", + "url": "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", + "url": "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", + "url": "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", + "url": "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", + "url": "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", + "url": "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", + "url": "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", + "url": "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", + "url": "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", + "url": "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", + "url": "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", + "url": "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", + "url": "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", + "url": "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", + "url": "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", + "url": "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl" } ], - "project_name": "filelock", + "project_name": "frozenlist", "requires_dists": [], - "requires_python": ">=3.10", - "version": "3.29.0" + "requires_python": ">=3.9", + "version": "1.8.0" }, { "artifacts": [ @@ -2616,13 +2598,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "efe075535c62e130b30e836b138e13785f6f043d1f0539e0a39aa411a99e90b8", - "url": "https://files.pythonhosted.org/packages/89/a5/33b49ba7bea7c41bb37f74ec0f8beea0831e052330196633fe2c77516ea6/huggingface_hub-1.14.0-py3-none-any.whl" + "hash": "64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22", + "url": "https://files.pythonhosted.org/packages/49/79/621a7dbb80c70974f73a597275351ebe03ce5bc65cb5f8f4acb5859252bc/huggingface_hub-1.16.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "d6d2c9cd6be1d02ae9ec6672d5587d10a427f377db688e82528f426a041622c2", - "url": "https://files.pythonhosted.org/packages/39/40/43109e943fd718b0ccd0cd61eb4f1c347df22bf81f5874c6f22adf44bcff/huggingface_hub-1.14.0.tar.gz" + "hash": "7f1dc4c5ec21aed69be630ad0c3378616be16f3de1a47b141c0e812965d9c832", + "url": "https://files.pythonhosted.org/packages/48/0f/ed994dbade67a54407c28cab96ef845e0e6d25500be56aca6394f8bfc9dd/huggingface_hub-1.16.1.tar.gz" } ], "project_name": "huggingface-hub", @@ -2741,7 +2723,7 @@ "urllib3<2.0; extra == \"testing\"" ], "requires_python": ">=3.10.0", - "version": "1.14.0" + "version": "1.16.1" }, { "artifacts": [ @@ -2806,13 +2788,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", - "url": "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl" + "hash": "cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", + "url": "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", - "url": "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz" + "hash": "d7a6da03db833450fca25d2358ac9ff06cd624577a4aea3a596d5c0f77b8e03d", + "url": "https://files.pythonhosted.org/packages/1a/88/bcf9709822fe69d02c2a6a77956c98ce6ea8ca8767a9aadcedc7eb6a2390/idna-3.16.tar.gz" } ], "project_name": "idna", @@ -2821,47 +2803,8 @@ "pytest>=8.3.2; extra == \"all\"", "ruff>=0.6.2; extra == \"all\"" ], - "requires_python": ">=3.8", - "version": "3.15" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", - "url": "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", - "url": "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz" - } - ], - "project_name": "importlib-metadata", - "requires_dists": [ - "flufl.flake8; extra == \"test\"", - "furo; extra == \"doc\"", - "ipython; extra == \"perf\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.test>=5.4; extra == \"test\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "mypy<1.19; platform_python_implementation == \"PyPy\" and extra == \"type\"", - "packaging; extra == \"test\"", - "pyfakefs; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"check\"", - "pytest-cov; extra == \"cover\"", - "pytest-enabler>=3.4; extra == \"enabler\"", - "pytest-mypy>=1.0.1; extra == \"type\"", - "pytest-perf>=0.9.2; extra == \"test\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"", - "zipp>=3.20" - ], "requires_python": ">=3.9", - "version": "8.7.1" + "version": "3.16" }, { "artifacts": [ @@ -3074,112 +3017,6 @@ "requires_python": ">=3.10", "version": "3.1.1" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", - "url": "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", - "url": "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz" - } - ], - "project_name": "jsonschema", - "requires_dists": [ - "attrs>=22.2.0", - "fqdn; extra == \"format\"", - "fqdn; extra == \"format-nongpl\"", - "idna; extra == \"format\"", - "idna; extra == \"format-nongpl\"", - "isoduration; extra == \"format\"", - "isoduration; extra == \"format-nongpl\"", - "jsonpointer>1.13; extra == \"format\"", - "jsonpointer>1.13; extra == \"format-nongpl\"", - "jsonschema-specifications>=2023.03.6", - "referencing>=0.28.4", - "rfc3339-validator; extra == \"format\"", - "rfc3339-validator; extra == \"format-nongpl\"", - "rfc3986-validator>0.1.0; extra == \"format-nongpl\"", - "rfc3987-syntax>=1.1.0; extra == \"format-nongpl\"", - "rfc3987; extra == \"format\"", - "rpds-py>=0.25.0", - "uri-template; extra == \"format\"", - "uri-template; extra == \"format-nongpl\"", - "webcolors>=1.11; extra == \"format\"", - "webcolors>=24.6.0; extra == \"format-nongpl\"" - ], - "requires_python": ">=3.10", - "version": "4.26.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", - "url": "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", - "url": "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz" - } - ], - "project_name": "jsonschema-specifications", - "requires_dists": [ - "referencing>=0.31.0" - ], - "requires_python": ">=3.9", - "version": "2025.9.1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", - "url": "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", - "url": "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz" - } - ], - "project_name": "kombu", - "requires_dists": [ - "PyYAML>=3.10; extra == \"yaml\"", - "amqp<6.0.0,>=5.1.1", - "azure-identity>=1.12.0; extra == \"azurestoragequeues\"", - "azure-servicebus>=7.10.0; extra == \"azureservicebus\"", - "azure-storage-queue>=12.6.0; extra == \"azurestoragequeues\"", - "boto3>=1.26.143; extra == \"sqs\"", - "confluent-kafka>=2.2.0; extra == \"confluentkafka\"", - "google-cloud-monitoring>=2.16.0; extra == \"gcpubsub\"", - "google-cloud-pubsub>=2.18.4; extra == \"gcpubsub\"", - "grpcio==1.75.1; extra == \"gcpubsub\"", - "kazoo>=2.8.0; extra == \"zookeeper\"", - "librabbitmq>=2.0.0; python_version < \"3.11\" and extra == \"librabbitmq\"", - "msgpack==1.1.2; extra == \"msgpack\"", - "packaging", - "protobuf==6.32.1; extra == \"gcpubsub\"", - "pycurl>=7.43.0.5; (sys_platform != \"win32\" and platform_python_implementation == \"CPython\") and extra == \"sqs\"", - "pymongo==4.15.3; extra == \"mongodb\"", - "pyro4==4.82; extra == \"pyro\"", - "python-consul2==0.1.5; extra == \"consul\"", - "qpid-python==1.36.0-1; extra == \"qpid\"", - "qpid-tools==1.36.0-1; extra == \"qpid\"", - "redis!=4.5.5,!=5.0.2,<6.5,>=4.5.2; extra == \"redis\"", - "softlayer_messaging>=1.0.3; extra == \"slmq\"", - "sqlalchemy<2.1,>=1.4.48; extra == \"sqlalchemy\"", - "tzdata>=2025.2", - "urllib3>=1.26.16; extra == \"sqs\"", - "vine==5.1.0" - ], - "requires_python": ">=3.9", - "version": "5.6.2" - }, { "artifacts": [ { @@ -3704,107 +3541,277 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", - "url": "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl" + "hash": "6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", + "url": "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "url": "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz" + } + ], + "project_name": "mccabe", + "requires_dists": [], + "requires_python": ">=3.6", + "version": "0.7.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", + "url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", + "url": "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" + } + ], + "project_name": "mdurl", + "requires_dists": [], + "requires_python": ">=3.7", + "version": "0.1.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", + "url": "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", + "url": "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz" + } + ], + "project_name": "mpmath", + "requires_dists": [ + "codecov; extra == \"develop\"", + "gmpy2>=2.1.0a4; platform_python_implementation != \"PyPy\" and extra == \"gmpy\"", + "pycodestyle; extra == \"develop\"", + "pytest-cov; extra == \"develop\"", + "pytest>=4.6; extra == \"develop\"", + "pytest>=4.6; extra == \"tests\"", + "sphinx; extra == \"docs\"", + "wheel; extra == \"develop\"" + ], + "requires_python": null, + "version": "1.3.0" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", + "url": "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", + "url": "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", + "url": "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", + "url": "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", + "url": "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", + "url": "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", + "url": "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + } + ], + "project_name": "msgpack", + "requires_dists": [], + "requires_python": ">=3.9", + "version": "1.1.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", + "url": "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", + "url": "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", + "url": "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", + "url": "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", + "url": "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", + "url": "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", + "url": "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", + "url": "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", + "url": "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", + "url": "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", + "url": "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", + "url": "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", + "url": "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", + "url": "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", + "url": "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", + "url": "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl" }, { "algorithm": "sha256", - "hash": "348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "url": "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz" - } - ], - "project_name": "mccabe", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "0.7.0" - }, - { - "artifacts": [ + "hash": "935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", + "url": "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl" + }, { "algorithm": "sha256", - "hash": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "url": "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl" + "hash": "9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", + "url": "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", - "url": "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz" - } - ], - "project_name": "mdurl", - "requires_dists": [], - "requires_python": ">=3.7", - "version": "0.1.2" - }, - { - "artifacts": [ + "hash": "f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", + "url": "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl" + }, { "algorithm": "sha256", - "hash": "a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", - "url": "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl" + "hash": "3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", + "url": "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" }, { "algorithm": "sha256", - "hash": "7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", - "url": "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz" - } - ], - "project_name": "mpmath", - "requires_dists": [ - "codecov; extra == \"develop\"", - "gmpy2>=2.1.0a4; platform_python_implementation != \"PyPy\" and extra == \"gmpy\"", - "pycodestyle; extra == \"develop\"", - "pytest-cov; extra == \"develop\"", - "pytest>=4.6; extra == \"develop\"", - "pytest>=4.6; extra == \"tests\"", - "sphinx; extra == \"docs\"", - "wheel; extra == \"develop\"" - ], - "requires_python": null, - "version": "1.3.0" - }, - { - "artifacts": [ + "hash": "273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", + "url": "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, { "algorithm": "sha256", - "hash": "5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", - "url": "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl" + "hash": "e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", + "url": "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", - "url": "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl" + "hash": "84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", + "url": "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl" }, { "algorithm": "sha256", - "hash": "3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", - "url": "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz" + "hash": "0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", + "url": "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" }, { "algorithm": "sha256", - "hash": "fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", - "url": "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + "hash": "401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", + "url": "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl" }, { "algorithm": "sha256", - "hash": "4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", - "url": "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl" + "hash": "90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", + "url": "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl" }, { "algorithm": "sha256", - "hash": "42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", - "url": "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl" + "hash": "41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", + "url": "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", - "url": "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + "hash": "af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", + "url": "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", + "url": "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl" + }, + { + "algorithm": "sha256", + "hash": "2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", + "url": "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", + "url": "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", + "url": "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl" } ], - "project_name": "msgpack", - "requires_dists": [], + "project_name": "multidict", + "requires_dists": [ + "typing-extensions>=4.1.0; python_version < \"3.11\"" + ], "requires_python": ">=3.9", - "version": "1.1.2" + "version": "6.7.1" }, { "artifacts": [ @@ -3884,89 +3891,89 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", - "url": "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl" + "hash": "a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", + "url": "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", - "url": "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl" + "hash": "eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", + "url": "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", - "url": "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl" + "hash": "ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", + "url": "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", - "url": "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl" + "hash": "bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", + "url": "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", - "url": "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl" + "hash": "ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", + "url": "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", - "url": "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl" + "hash": "112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", + "url": "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", - "url": "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl" + "hash": "68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", + "url": "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl" }, { "algorithm": "sha256", - "hash": "e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", - "url": "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl" + "hash": "043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", + "url": "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl" }, { "algorithm": "sha256", - "hash": "d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", - "url": "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl" + "hash": "a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", + "url": "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", - "url": "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + "hash": "7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", + "url": "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl" }, { "algorithm": "sha256", - "hash": "2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", - "url": "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz" + "hash": "6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", + "url": "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl" }, { "algorithm": "sha256", - "hash": "f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", - "url": "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl" + "hash": "72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", + "url": "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", - "url": "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl" + "hash": "f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", + "url": "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz" }, { "algorithm": "sha256", - "hash": "5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", - "url": "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl" + "hash": "dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", + "url": "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl" }, { "algorithm": "sha256", - "hash": "4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", - "url": "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl" + "hash": "f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", + "url": "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl" }, { "algorithm": "sha256", - "hash": "ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", - "url": "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl" + "hash": "511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", + "url": "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl" } ], "project_name": "numpy", "requires_dists": [], "requires_python": ">=3.11", - "version": "2.4.4" + "version": "2.4.6" }, { "artifacts": [ @@ -4254,54 +4261,53 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", - "url": "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl" + "hash": "51a69edacadbc03a8950ace1c4c21099cacc538820ac2c9e36277e78cebba714", + "url": "https://files.pythonhosted.org/packages/a3/ca/9520cc1f3dfbbd03ac5903bbf55833e257bc64b1cf30fa8b0d6df374d821/opentelemetry_api-1.42.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", - "url": "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz" + "hash": "56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", + "url": "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz" } ], "project_name": "opentelemetry-api", "requires_dists": [ - "importlib-metadata<8.8.0,>=6.0", "typing-extensions>=4.5.0" ], - "requires_python": ">=3.9", - "version": "1.41.1" + "requires_python": ">=3.10", + "version": "1.42.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "10da74dad6a49344b9b7b21b6182e3060373a235fde1528616d5f01f92e66aa9", - "url": "https://files.pythonhosted.org/packages/29/48/bce76d3ea772b609757e9bc844e02ab408a6446609bf74fb562062ba6b71/opentelemetry_exporter_otlp_proto_common-1.41.1-py3-none-any.whl" + "hash": "f48d395ab815b444da118868977e9798ea354c25737d5cf39578ae894011c140", + "url": "https://files.pythonhosted.org/packages/d6/43/2375e7612e1121a4518c17603b6e0b03ad94f565aafad53f464dc5be2bf6/opentelemetry_exporter_otlp_proto_common-1.42.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "0e253156ea9c36b0bd3d2440c5c9ba7dd1f3fb64ba7a08fc85fbac536b56e1fb", - "url": "https://files.pythonhosted.org/packages/ae/fa/f9e3bd3c4d692b3ce9a2880a167d1f79681a1bea11f00d5bf76adc03e6ea/opentelemetry_exporter_otlp_proto_common-1.41.1.tar.gz" + "hash": "04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", + "url": "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz" } ], "project_name": "opentelemetry-exporter-otlp-proto-common", "requires_dists": [ - "opentelemetry-proto==1.41.1" + "opentelemetry-proto==1.42.1" ], - "requires_python": ">=3.9", - "version": "1.41.1" + "requires_python": ">=3.10", + "version": "1.42.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "1a21e8f49c7a946d935551e90947d6c3eb39236723c6624401da0f33d68edcb4", - "url": "https://files.pythonhosted.org/packages/ba/4d/ef07ff2fc630849f2080ae0ae73a61f67257905b7ac79066640bfa0c5739/opentelemetry_exporter_otlp_proto_http-1.41.1-py3-none-any.whl" + "hash": "00a16da1b312a1d6c7233d600d557c91df71125af73020f3b9a7765bd699d59d", + "url": "https://files.pythonhosted.org/packages/d3/96/82cb223a1502f0787d4bbff12907f5f8d870a50731febcd5818d93ef9555/opentelemetry_exporter_otlp_proto_http-1.42.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4747a9604c8550ab38c6fd6180e2fcb80de3267060bef2c306bad3cb443302bc", - "url": "https://files.pythonhosted.org/packages/33/5b/9d3c7f70cca10136ba82a81e738dee626c8e7fc61c6887ea9a58bf34c606/opentelemetry_exporter_otlp_proto_http-1.41.1.tar.gz" + "hash": "bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", + "url": "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz" } ], "project_name": "opentelemetry-exporter-otlp-proto-http", @@ -4309,192 +4315,169 @@ "googleapis-common-protos~=1.52", "opentelemetry-api~=1.15", "opentelemetry-exporter-credential-provider-gcp>=0.59b0; extra == \"gcp-auth\"", - "opentelemetry-exporter-otlp-proto-common==1.41.1", - "opentelemetry-proto==1.41.1", - "opentelemetry-sdk~=1.41.1", + "opentelemetry-exporter-otlp-proto-common==1.42.1", + "opentelemetry-proto==1.42.1", + "opentelemetry-sdk~=1.42.1", "requests~=2.7", "typing-extensions>=4.5.0" ], - "requires_python": ">=3.9", - "version": "1.41.1" + "requires_python": ">=3.10", + "version": "1.42.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", - "url": "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl" + "hash": "f1986716d52cc316ea5f60189098726a9071d8ecc0eee96c9ed110be08bade9c", + "url": "https://files.pythonhosted.org/packages/35/a1/9314e621c143e4d82a5bf7a43c2ff7a745d31023506336857607c8c543cc/opentelemetry_instrumentation-0.63b1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "90e92a905ba4f84db06ac3aec96701df6c079b2d66e9379f8739f0a1bdcc7f45", - "url": "https://files.pythonhosted.org/packages/52/cb/0523b92c112a6cc70be43724343dc45225d3af134419844d7879a07755d4/opentelemetry_instrumentation-0.62b1.tar.gz" + "hash": "32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", + "url": "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz" } ], "project_name": "opentelemetry-instrumentation", "requires_dists": [ "opentelemetry-api~=1.4", - "opentelemetry-semantic-conventions==0.62b1", + "opentelemetry-semantic-conventions==0.63b1", "packaging>=18.0", "wrapt<3.0.0,>=1.0.0" ], - "requires_python": ">=3.9", - "version": "0.62b1" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "50567a47b7adc4ea552d09709de4d73fea7b4ff24ab0e9d38739d03fcd3f95ef", - "url": "https://files.pythonhosted.org/packages/24/51/f38a31ac8f8e3bd365f301f697661679addaf548d52a05cfdde4448a5493/opentelemetry_instrumentation_celery-0.62b1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "f0035abd464a2989414a9c5ecdd79a25c87bd8c43f96c7f39e07000c6f25dfef", - "url": "https://files.pythonhosted.org/packages/35/86/9e78c174b2f6ea92af3f99aa7488807b74290a5cd44a8e05bfbfd7b109be/opentelemetry_instrumentation_celery-0.62b1.tar.gz" - } - ], - "project_name": "opentelemetry-instrumentation-celery", - "requires_dists": [ - "celery<6.0,>=4.0; extra == \"instruments\"", - "opentelemetry-api~=1.12", - "opentelemetry-instrumentation==0.62b1", - "opentelemetry-semantic-conventions==0.62b1" - ], - "requires_python": ">=3.9", - "version": "0.62b1" + "requires_python": ">=3.10", + "version": "0.63b1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "5ac96ef25019fbea5e2739c9b4b7ce17e4379e2b1ec9fdce6b696a5c09ceabbe", - "url": "https://files.pythonhosted.org/packages/67/7c/306249a95e88ce4de1491d7bf49280c72e297b207c1da6967d0ec0078d9a/opentelemetry_instrumentation_django-0.62b1-py3-none-any.whl" + "hash": "909ea4afbb18f16eb811d7ac330ed0f6fda7765191d49bccb8cca38c4fbc005f", + "url": "https://files.pythonhosted.org/packages/c8/14/3efdfb34393f6780ab3dc916dba9221e7be5f07eaa768c4575e14cea98fa/opentelemetry_instrumentation_django-0.63b1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "268c9b3d234510ab835acea0524ba65361389f6e2c291b495ba8072c7bb59003", - "url": "https://files.pythonhosted.org/packages/dc/82/659d2192cb076bc836639c23876f67cc9afc85629694666479c189975cd4/opentelemetry_instrumentation_django-0.62b1.tar.gz" + "hash": "f2071d2f92e4779c5a14dd452b0dfe426343599e6efa9d888304fb639a9f3101", + "url": "https://files.pythonhosted.org/packages/f7/37/90fd5f0dc4a2042dfdb609a9dbe42fde26b3690f9c33ece529b024251015/opentelemetry_instrumentation_django-0.63b1.tar.gz" } ], "project_name": "opentelemetry-instrumentation-django", "requires_dists": [ "django>=2.0; extra == \"instruments\"", "opentelemetry-api~=1.12", - "opentelemetry-instrumentation-asgi==0.62b1; extra == \"asgi\"", - "opentelemetry-instrumentation-wsgi==0.62b1", - "opentelemetry-instrumentation==0.62b1", - "opentelemetry-semantic-conventions==0.62b1", - "opentelemetry-util-http==0.62b1" + "opentelemetry-instrumentation-asgi==0.63b1; extra == \"asgi\"", + "opentelemetry-instrumentation-wsgi==0.63b1", + "opentelemetry-instrumentation==0.63b1", + "opentelemetry-semantic-conventions==0.63b1", + "opentelemetry-util-http==0.63b1" ], - "requires_python": ">=3.9", - "version": "0.62b1" + "requires_python": ">=3.10", + "version": "0.63b1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "a2df11de0113f504043e2b0fa0288238a93ee49ff607bd5100cb2d3a75bc771f", - "url": "https://files.pythonhosted.org/packages/f7/0e/60fec0780e16929c821df7c55c4f0bea45d6ef562e662c5f27f47d0ff195/opentelemetry_instrumentation_wsgi-0.62b1-py3-none-any.whl" + "hash": "86779715262227d3436bdfb16aabd1c524b0f236725a69e8754ceda76c6d79dc", + "url": "https://files.pythonhosted.org/packages/14/b6/0854591a78960f376c7f943a6927c2863e1f8b0c93003aafe03aa49c089b/opentelemetry_instrumentation_wsgi-0.63b1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "02a364fd9c940a46b19c825c5bfe386b007d5292ef91573894164836953fe831", - "url": "https://files.pythonhosted.org/packages/36/db/19f1d66cead56e52291fccaa235b07ad45a5c24be1c740301a840c68235a/opentelemetry_instrumentation_wsgi-0.62b1.tar.gz" + "hash": "03d61c4678ce82402e7f37b6a3dbd84cb97b85b3cb416a78c2e74c7c6d9451fa", + "url": "https://files.pythonhosted.org/packages/0b/55/832f287fb153adc25c05bc2594d00ac4d1dbeca8b19388b2666d5154c912/opentelemetry_instrumentation_wsgi-0.63b1.tar.gz" } ], "project_name": "opentelemetry-instrumentation-wsgi", "requires_dists": [ "opentelemetry-api~=1.12", - "opentelemetry-instrumentation==0.62b1", - "opentelemetry-semantic-conventions==0.62b1", - "opentelemetry-util-http==0.62b1" + "opentelemetry-instrumentation==0.63b1", + "opentelemetry-semantic-conventions==0.63b1", + "opentelemetry-util-http==0.63b1" ], - "requires_python": ">=3.9", - "version": "0.62b1" + "requires_python": ">=3.10", + "version": "0.63b1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "0496713b804d127a4147e32849fbaf5683fac8ee98550e8e7679cd706c289720", - "url": "https://files.pythonhosted.org/packages/e4/1e/5cd77035e3e82070e2265a63a760f715aacd3cb16dddc7efee913f297fcc/opentelemetry_proto-1.41.1-py3-none-any.whl" + "hash": "dedb74cba2886c59c7789b227a7a670613025a07489040050aedff6e5c0fb43c", + "url": "https://files.pythonhosted.org/packages/41/9d/171c02c84a76940b7e601805b3bb536985aded9168fbcc9ba52f0a730fa2/opentelemetry_proto-1.42.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4b9d2eb631237ea43b80e16c073af438554e32bc7e9e3f8ca4a9582f900020e5", - "url": "https://files.pythonhosted.org/packages/99/e8/633c6d8a9c8840338b105907e55c32d3da1983abab5e52f899f72a82c3d1/opentelemetry_proto-1.41.1.tar.gz" + "hash": "c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", + "url": "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz" } ], "project_name": "opentelemetry-proto", "requires_dists": [ "protobuf<7.0,>=5.0" ], - "requires_python": ">=3.9", - "version": "1.41.1" + "requires_python": ">=3.10", + "version": "1.42.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "edee379c126c1bce952b0c812b48fe8ff35b30df0eecf17e98afa4d598b7d85d", - "url": "https://files.pythonhosted.org/packages/b4/e7/a1420b698aad018e1cf60fdbaaccbe49021fb415e2a0d81c242f4c518f54/opentelemetry_sdk-1.41.1-py3-none-any.whl" + "hash": "083cd4bbfaa5aa7b5a9e552430d9951219967cfb27aa61feb13a77aba1fc839d", + "url": "https://files.pythonhosted.org/packages/8f/6b/4287766cfbde577ae2272e8884abac325aeaac0d64f41c61d5b8cc595105/opentelemetry_sdk-1.42.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "724b615e1215b5aeacda0abb8a6a8922c9a1853068948bd0bd225a56d0c792e6", - "url": "https://files.pythonhosted.org/packages/58/d0/54ee30dab82fb0acda23d144502771ff76ef8728459c83c3e89ef9fb1825/opentelemetry_sdk-1.41.1.tar.gz" + "hash": "8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", + "url": "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz" } ], "project_name": "opentelemetry-sdk", "requires_dists": [ "jsonschema>=4.0; extra == \"file-configuration\"", - "opentelemetry-api==1.41.1", - "opentelemetry-semantic-conventions==0.62b1", + "opentelemetry-api==1.42.1", + "opentelemetry-semantic-conventions==0.63b1", "pyyaml>=6.0; extra == \"file-configuration\"", "typing-extensions>=4.5.0" ], - "requires_python": ">=3.9", - "version": "1.41.1" + "requires_python": ">=3.10", + "version": "1.42.1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "cf506938103d331fbb78eded0d9788095f7fd59016f2bda813c3324e5a74a93c", - "url": "https://files.pythonhosted.org/packages/eb/a6/83dc2ab6fa397ee66fba04fe2e74bdf7be3b3870005359ceb7689103c058/opentelemetry_semantic_conventions-0.62b1-py3-none-any.whl" + "hash": "dfe5ef4dee82586b746f522b818ceb298d00b3d59f660042bd79404bff8d0682", + "url": "https://files.pythonhosted.org/packages/cb/7a/7fe66f5f3682b1dd47d88cc4e11f1c6c0966b737de2d16671146e23c39a5/opentelemetry_semantic_conventions-0.63b1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "c5cc6e04a7f8c7cdd30be2ed81499fa4e75bfbd52c9cb70d40af1f9cd3619802", - "url": "https://files.pythonhosted.org/packages/9e/de/911ac9e309052aca1b20b2d5549d3db45d1011e1a610e552c6ccdd1b64f8/opentelemetry_semantic_conventions-0.62b1.tar.gz" + "hash": "3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", + "url": "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz" } ], "project_name": "opentelemetry-semantic-conventions", "requires_dists": [ - "opentelemetry-api==1.41.1", + "opentelemetry-api==1.42.1", "typing-extensions>=4.5.0" ], - "requires_python": ">=3.9", - "version": "0.62b1" + "requires_python": ">=3.10", + "version": "0.63b1" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "c57e8a6c19fc422c288e6074e882f506f85030b69b7376182f74f9257b9261f0", - "url": "https://files.pythonhosted.org/packages/5d/85/a9d9d32161c1ced61346267db4c9702da54f81ec5dc88214bc65c23f4e9d/opentelemetry_util_http-0.62b1-py3-none-any.whl" + "hash": "6284194028c59cd439f8acfe388145069a6127f11dc077e1344a2094adacc3f8", + "url": "https://files.pythonhosted.org/packages/e5/f1/34e047e8f6a3c67e5220acf1af7b9f62868c25d77791bca74457bd2180a6/opentelemetry_util_http-0.63b1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "adf6facbb89aef8f8bc566e2f04624942ba08a7b678b3479a91051a8f4dc70a3", - "url": "https://files.pythonhosted.org/packages/24/1b/aa71b63e18d30a8384036b9937f40f7618f8030a7aa213155fb54f6f2b47/opentelemetry_util_http-0.62b1.tar.gz" + "hash": "ba1268f00922ee522dba2ae38458060f99486e7385a8056985901ca9685adfff", + "url": "https://files.pythonhosted.org/packages/6c/d8/7bf5e4cec0578ac3c28c18eb7b88f34279139cbc8c568d6aa02b9c5ae53e/opentelemetry_util_http-0.63b1.tar.gz" } ], "project_name": "opentelemetry-util-http", "requires_dists": [], - "requires_python": ">=3.9", - "version": "0.62b1" + "requires_python": ">=3.10", + "version": "0.63b1" }, { "artifacts": [ @@ -4635,6 +4618,38 @@ "requires_python": ">=3.8", "version": "26.2" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", + "url": "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", + "url": "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz" + } + ], + "project_name": "pamqp", + "requires_dists": [ + "coverage; extra == \"testing\"", + "flake8-comprehensions; extra == \"testing\"", + "flake8-deprecated; extra == \"testing\"", + "flake8-import-order; extra == \"testing\"", + "flake8-print; extra == \"testing\"", + "flake8-quotes; extra == \"testing\"", + "flake8-rst-docstrings; extra == \"testing\"", + "flake8-tuple; extra == \"testing\"", + "flake8; extra == \"testing\"", + "lxml; extra == \"codegen\"", + "requests; extra == \"codegen\"", + "yapf; extra == \"codegen\"", + "yapf; extra == \"testing\"" + ], + "requires_python": ">=3.7", + "version": "3.3.0" + }, { "artifacts": [ { @@ -4944,21 +4959,159 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", - "url": "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl" + "hash": "be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", + "url": "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", + "url": "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", + "url": "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", + "url": "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", + "url": "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", + "url": "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", + "url": "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", + "url": "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", + "url": "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", + "url": "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", + "url": "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", + "url": "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", + "url": "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", + "url": "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", + "url": "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", + "url": "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", + "url": "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", + "url": "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", + "url": "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", + "url": "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", + "url": "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", + "url": "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", + "url": "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", + "url": "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", + "url": "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", + "url": "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", + "url": "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", + "url": "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl" }, { "algorithm": "sha256", - "hash": "28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", - "url": "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz" + "hash": "6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", + "url": "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", + "url": "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl" } ], - "project_name": "prompt-toolkit", - "requires_dists": [ - "wcwidth" - ], - "requires_python": ">=3.8", - "version": "3.0.52" + "project_name": "propcache", + "requires_dists": [], + "requires_python": ">=3.10", + "version": "0.5.2" }, { "artifacts": [ @@ -5175,6 +5328,24 @@ "requires_python": ">=3.10", "version": "3.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "6d2349746270bd642b71b9f7187cf13f4d9ee2412b4710396a507b5fe4f60dac", + "url": "https://files.pythonhosted.org/packages/f0/76/caf316909f4545e7158e0e1defd8956a1da49f4af04f5d16b18c358dfeac/pycron-3.2.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "e125a28aca0295769541a40633f70b602579df48c9cb357c36c28d2628ba2b13", + "url": "https://files.pythonhosted.org/packages/8e/5d/340be12ae4a69c33102dfb6ddc1dc6e53e69b2d504fa26b5d34a472c3057/pycron-3.2.0.tar.gz" + } + ], + "project_name": "pycron", + "requires_dists": [], + "requires_python": "<4.0,>=3.9", + "version": "3.2.0" + }, { "artifacts": [ { @@ -5667,61 +5838,39 @@ "numpy>=1.21; python_version == \"3.11\"", "numpy>=1.26; python_version == \"3.12\"", "numpy>=2.1.0; python_version == \"3.13\"", - "numpy>=2.3.0; python_version >= \"3.14\"", - "portalocker<4.0,>=2.7.0", - "protobuf>=3.20.0", - "pydantic!=2.0.*,!=2.1.*,!=2.2.0,>=1.10.8", - "urllib3<3,>=1.26.14" - ], - "requires_python": ">=3.10", - "version": "1.18.0" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", - "url": "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", - "url": "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz" - } - ], - "project_name": "redis", - "requires_dists": [ - "async-timeout>=4.0.3; python_full_version < \"3.11.3\"", - "cryptography>=36.0.1; extra == \"ocsp\"", - "hiredis>=3.2.0; extra == \"hiredis\"", - "pyjwt>=2.9.0; extra == \"jwt\"", - "pyopenssl>=20.0.1; extra == \"ocsp\"", - "requests>=2.31.0; extra == \"ocsp\"" + "numpy>=2.3.0; python_version >= \"3.14\"", + "portalocker<4.0,>=2.7.0", + "protobuf>=3.20.0", + "pydantic!=2.0.*,!=2.1.*,!=2.2.0,>=1.10.8", + "urllib3<3,>=1.26.14" ], - "requires_python": ">=3.9", - "version": "6.4.0" + "requires_python": ">=3.10", + "version": "1.18.0" }, { "artifacts": [ { "algorithm": "sha256", - "hash": "381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", - "url": "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl" + "hash": "dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97", + "url": "https://files.pythonhosted.org/packages/7f/26/5c5fa0e83c3621db835cfc1f1d789b37e7fa99ed54423b5f519beb931aa7/redis-5.3.1-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", - "url": "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz" + "hash": "ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c", + "url": "https://files.pythonhosted.org/packages/6a/cf/128b1b6d7086200c9f387bd4be9b2572a30b90745ef078bd8b235042dc9f/redis-5.3.1.tar.gz" } ], - "project_name": "referencing", + "project_name": "redis", "requires_dists": [ - "attrs>=22.2.0", - "rpds-py>=0.7.0", - "typing-extensions>=4.4.0; python_version < \"3.13\"" + "PyJWT>=2.9.0", + "async-timeout>=4.0.3; python_full_version < \"3.11.3\"", + "cryptography>=36.0.1; extra == \"ocsp\"", + "hiredis>=3.0.0; extra == \"hiredis\"", + "pyopenssl==23.2.1; extra == \"ocsp\"", + "requests>=2.31.0; extra == \"ocsp\"" ], - "requires_python": ">=3.10", - "version": "0.37.0" + "requires_python": ">=3.8", + "version": "5.3.1" }, { "artifacts": [ @@ -5933,139 +6082,6 @@ "requires_python": ">=3.9.0", "version": "15.0.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", - "url": "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", - "url": "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", - "url": "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", - "url": "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", - "url": "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", - "url": "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz" - }, - { - "algorithm": "sha256", - "hash": "39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", - "url": "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", - "url": "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", - "url": "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", - "url": "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", - "url": "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", - "url": "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", - "url": "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", - "url": "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", - "url": "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl" - }, - { - "algorithm": "sha256", - "hash": "9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", - "url": "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl" - }, - { - "algorithm": "sha256", - "hash": "c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", - "url": "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl" - }, - { - "algorithm": "sha256", - "hash": "1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", - "url": "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl" - }, - { - "algorithm": "sha256", - "hash": "9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", - "url": "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", - "url": "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl" - }, - { - "algorithm": "sha256", - "hash": "ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", - "url": "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl" - }, - { - "algorithm": "sha256", - "hash": "692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", - "url": "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl" - }, - { - "algorithm": "sha256", - "hash": "f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", - "url": "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl" - }, - { - "algorithm": "sha256", - "hash": "e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", - "url": "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl" - }, - { - "algorithm": "sha256", - "hash": "eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", - "url": "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl" - } - ], - "project_name": "rpds-py", - "requires_dists": [], - "requires_python": ">=3.10", - "version": "0.30.0" - }, { "artifacts": [ { @@ -6867,6 +6883,105 @@ "requires_python": ">=3.9", "version": "3.9.0" }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "b910d3676e45b317678b4a8a1865ba6c261f696a02541461c765a7773cf4fa4b", + "url": "https://files.pythonhosted.org/packages/28/0a/35b7b2673f923fd57bda8820afee03a6bb57883c1a61ee9e04e31e37ca50/taskiq-0.12.4-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "887e6acc72eea322a1515d9863551f00be7c81c6a0f72b2961569e58d68edc34", + "url": "https://files.pythonhosted.org/packages/fd/7d/32bdfddd09fe3520bfb4978618dd87292201ca3f0bf9b9c413eb4ee946eb/taskiq-0.12.4.tar.gz" + } + ], + "project_name": "taskiq", + "requires_dists": [ + "aiohttp>=3", + "anyio>=4", + "cbor2>=5; extra == \"cbor\"", + "gitignore-parser>=0; extra == \"reload\"", + "msgpack>=1.0.7; extra == \"msgpack\"", + "opentelemetry-api<2.0.0,>=1.38.0; extra == \"opentelemetry\"", + "opentelemetry-instrumentation<1,>=0.59b0; extra == \"opentelemetry\"", + "opentelemetry-semantic-conventions<1,>=0.59b0; extra == \"opentelemetry\"", + "orjson>=3; extra == \"orjson\"", + "packaging>=19", + "prometheus-client>=0; extra == \"metrics\"", + "pycron>=3.0.0", + "pydantic<=3.0,>=1.0", + "pyzmq>=26; extra == \"zmq\"", + "taskiq-dependencies<2,>=1.3.1", + "typing-extensions>=3.10.0.0; python_version < \"3.11\"", + "uvloop<1,>=0.16.0; sys_platform != \"win32\" and extra == \"uv\"", + "watchdog>=4; extra == \"reload\"" + ], + "requires_python": "<4,>=3.10", + "version": "0.12.4" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "a8eb80f9ada920d7384635c3853799b6f20e6123e453aa5929e5d35f39890878", + "url": "https://files.pythonhosted.org/packages/de/11/ded35d67affe8acb5c5e48aaedcc29202995bc84a1449ed7672ff6c37a11/taskiq_aio_pika-0.4.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "d62f4956ef3cb3deed3847bd70c381249f24186ac5c0cb60fb4bc609e61c2268", + "url": "https://files.pythonhosted.org/packages/b9/af/f534056b2164eea181c6e975850bc74f92b1d7509a4cd15edad088ad3e9d/taskiq_aio_pika-0.4.2.tar.gz" + } + ], + "project_name": "taskiq-aio-pika", + "requires_dists": [ + "aio-pika<10.0,>=9.0", + "taskiq<1,>=0.6.0" + ], + "requires_python": "<4.0.0,>=3.8.1", + "version": "0.4.2" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", + "url": "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4", + "url": "https://files.pythonhosted.org/packages/47/90/47a627696e53bfdcacabc3e8c05b73bf1424685bcb5f17209cb8b12da1bf/taskiq_dependencies-1.5.7.tar.gz" + } + ], + "project_name": "taskiq-dependencies", + "requires_dists": [ + "typing-extensions>=4.6.3; python_version < \"3.10\"" + ], + "requires_python": "<4.0,>=3.9", + "version": "1.5.7" + }, + { + "artifacts": [ + { + "algorithm": "sha256", + "hash": "867949594e63402bdb8378fcc9f1e4e1a18360cb7d3da30fe06d8c33cb74822f", + "url": "https://files.pythonhosted.org/packages/96/5d/f101e0277122e9c064716ce2707cca191c337a362fbec7029759d824f8ac/taskiq_redis-1.0.0-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "45b2ac312a61725b4a5bdc5595f160d83986eac21eb5bc080ede5e6272d87d61", + "url": "https://files.pythonhosted.org/packages/1f/39/7c066c604f55d5dc5f632bd24d265a72cbed278740c12d7aa91f4015b735/taskiq_redis-1.0.0.tar.gz" + } + ], + "project_name": "taskiq-redis", + "requires_dists": [ + "redis<6,>=5", + "taskiq<1,>=0.11.1" + ], + "requires_python": "<4.0.0,>=3.8.1", + "version": "1.0.0" + }, { "artifacts": [ { @@ -7104,13 +7219,13 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "5340fb95962162cdfdae5cc91d7f8fedd92ed75216c1154c5e1f590fcf56dd0e", - "url": "https://files.pythonhosted.org/packages/fc/b1/8be7e7ef0b5200491312201918b6125ef9c9df9dd0f0240ccef9ac824e6b/transformers-5.8.1-py3-none-any.whl" + "hash": "1d19509bcff7028ebc6b277d71caa712e8353778463d38764237d14b42b52788", + "url": "https://files.pythonhosted.org/packages/02/ca/2eaa5359f2ccb8c2e1656bc26305ad0cf438aa392ce4b29ae67a315c186e/transformers-5.9.0-py3-none-any.whl" }, { "algorithm": "sha256", - "hash": "4dd5b6de4105725104d84fd6abd74b305f4debfc251b38c648ee5dd087cf543b", - "url": "https://files.pythonhosted.org/packages/e7/e6/4134ea2fbea322cddc7ffc94a0d8ee47fe32ce8e876b320cd37d88edfc4d/transformers-5.8.1.tar.gz" + "hash": "25997cb8fa6053533171634b6162d7df54346530ec2aa9b42bb834e63668c842", + "url": "https://files.pythonhosted.org/packages/51/58/7f843608f2e8421f86bb97060b54649be6239ec612b82bf9d41e65c26c00/transformers-5.9.0.tar.gz" } ], "project_name": "transformers", @@ -7213,9 +7328,6 @@ "openai>=1.98.0; extra == \"dev\"", "openai>=1.98.0; extra == \"serving\"", "openai>=1.98.0; extra == \"testing\"", - "opentelemetry-api; extra == \"open-telemetry\"", - "opentelemetry-exporter-otlp; extra == \"open-telemetry\"", - "opentelemetry-sdk; extra == \"open-telemetry\"", "optimum-benchmark>=0.3.0; extra == \"benchmark\"", "optuna; extra == \"deepspeed-testing\"", "optuna; extra == \"integrations\"", @@ -7374,7 +7486,7 @@ "uvicorn; extra == \"testing\"" ], "requires_python": ">=3.10.0", - "version": "5.8.1" + "version": "5.9.0" }, { "artifacts": [ @@ -7783,49 +7895,6 @@ "requires_python": ">=3.9", "version": "0.4.2" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", - "url": "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", - "url": "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz" - } - ], - "project_name": "tzdata", - "requires_dists": [], - "requires_python": ">=2", - "version": "2026.2" - }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", - "url": "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", - "url": "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz" - } - ], - "project_name": "tzlocal", - "requires_dists": [ - "check-manifest; extra == \"devenv\"", - "pytest-cov; extra == \"devenv\"", - "pytest-mock>=3.3; extra == \"devenv\"", - "pytest>=4.3; extra == \"devenv\"", - "tzdata; platform_system == \"Windows\"", - "zest.releaser; extra == \"devenv\"" - ], - "requires_python": ">=3.9", - "version": "5.3.1" - }, { "artifacts": [ { @@ -8070,24 +8139,6 @@ "requires_python": ">=3.9", "version": "0.15.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", - "url": "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", - "url": "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz" - } - ], - "project_name": "vine", - "requires_dists": [], - "requires_python": ">=3.6", - "version": "5.1.0" - }, { "artifacts": [ { @@ -8179,24 +8230,6 @@ "requires_python": ">=3.9", "version": "6.0.0" }, - { - "artifacts": [ - { - "algorithm": "sha256", - "hash": "5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", - "url": "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl" - }, - { - "algorithm": "sha256", - "hash": "90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", - "url": "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz" - } - ], - "project_name": "wcwidth", - "requires_dists": [], - "requires_python": ">=3.8", - "version": "0.7.0" - }, { "artifacts": [ { @@ -8564,38 +8597,98 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", - "url": "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl" + "hash": "2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", + "url": "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl" + }, + { + "algorithm": "sha256", + "hash": "b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", + "url": "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", + "url": "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", + "url": "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl" + }, + { + "algorithm": "sha256", + "hash": "a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", + "url": "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", + "url": "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", + "url": "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz" + }, + { + "algorithm": "sha256", + "hash": "491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", + "url": "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl" + }, + { + "algorithm": "sha256", + "hash": "f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", + "url": "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl" + }, + { + "algorithm": "sha256", + "hash": "e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", + "url": "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl" + }, + { + "algorithm": "sha256", + "hash": "374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", + "url": "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl" + }, + { + "algorithm": "sha256", + "hash": "8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", + "url": "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl" + }, + { + "algorithm": "sha256", + "hash": "cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", + "url": "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl" + }, + { + "algorithm": "sha256", + "hash": "e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", + "url": "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", + "url": "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" }, { "algorithm": "sha256", - "hash": "32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", - "url": "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz" + "hash": "810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", + "url": "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl" + }, + { + "algorithm": "sha256", + "hash": "4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", + "url": "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl" } ], - "project_name": "zipp", + "project_name": "yarl", "requires_dists": [ - "big-O; extra == \"test\"", - "furo; extra == \"doc\"", - "jaraco.functools; extra == \"test\"", - "jaraco.itertools; extra == \"test\"", - "jaraco.packaging>=9.3; extra == \"doc\"", - "jaraco.test; extra == \"test\"", - "jaraco.tidelift>=1.4; extra == \"doc\"", - "more_itertools; extra == \"test\"", - "pytest!=8.1.*,>=6; extra == \"test\"", - "pytest-checkdocs>=2.4; extra == \"check\"", - "pytest-cov; extra == \"cover\"", - "pytest-enabler>=2.2; extra == \"enabler\"", - "pytest-ignore-flaky; extra == \"test\"", - "pytest-mypy; extra == \"type\"", - "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", - "rst.linker>=1.9; extra == \"doc\"", - "sphinx-lint; extra == \"doc\"", - "sphinx>=3.5; extra == \"doc\"" + "idna>=2.0", + "multidict>=4.0", + "propcache>=0.2.1" ], - "requires_python": ">=3.9", - "version": "3.23.1" + "requires_python": ">=3.10", + "version": "1.24.2" }, { "artifacts": [ @@ -8747,13 +8840,14 @@ "Deprecated==1.3.1", "Django==6.0.5", "Mastodon.py==2.2.1", + "PyJWT==2.12.1", + "asgiref==3.11.1", "atproto==0.0.65", "autobahn==25.12.2", "automat==25.4.16", "boto3==1.43.8", "botocore==1.43.8", "cbor2==6.1.1", - "celery[redis]==5.6.3", "cfgv==3.5.0", "channels-redis==4.3.0", "channels==4.3.2", @@ -8763,25 +8857,19 @@ "cssbeautifier==1.15.4", "daphne==4.2.1", "dj-database-url==3.1.2", - "dj-rest-auth==7.2.0", "django-allauth==65.16.1", "django-anymail==15.0", "django-cors-headers==4.9.0", "django-import-export==4.4.1", + "django-ninja>=1.6.2", "django-storages==1.14.6", "django-stubs-ext==6.0.4", "django-stubs==6.0.4", "django-types==0.24.0", "django-unfold==0.93.0", - "djangochannelsrestframework==1.3.0", - "djangorestframework-simplejwt==5.5.1", - "djangorestframework==3.17.1", "djlint==1.36.4", "dnspython==2.8.0", "docutils==0.22.4", - "drf-nested-routers==0.95.0", - "drf-spectacular==0.29.0", - "drf-standardized-errors==0.16.0", "editorconfig==0.17.1", "feedparser==6.0.12", "gunicorn==26.0.0", @@ -8806,7 +8894,6 @@ "nodeenv==1.10.0", "opentelemetry-api>=1.41.1", "opentelemetry-exporter-otlp-proto-http<2,>=1.41.1", - "opentelemetry-instrumentation-celery<1,>=0.62b1", "opentelemetry-instrumentation-django<1,>=0.62b1", "opentelemetry-sdk>=1.41.1", "ormsgpack==1.12.2", @@ -8834,6 +8921,9 @@ "standardwebhooks==1.0.1", "structlog==25.5.0", "svix==1.93.0", + "taskiq-aio-pika==0.4.2", + "taskiq-redis==1.0.0", + "taskiq==0.12.4", "twisted==26.4.0", "txaio==25.12.2", "types-Deprecated==1.3.1.20260508", diff --git a/BUILD b/BUILD index 338caf5b..dc579642 100644 --- a/BUILD +++ b/BUILD @@ -26,7 +26,6 @@ python_requirements( source="pyproject.toml", module_mapping={ "dj-database-url": ["dj_database_url"], - "dj-rest-auth": ["dj_rest_auth"], "django-allauth": ["allauth"], "django-anymail": ["anymail"], "django-cors-headers": ["corsheaders"], @@ -34,20 +33,17 @@ python_requirements( "django-storages": ["storages"], "django-stubs-ext": ["django_stubs_ext"], "django-unfold": ["unfold"], - "djangochannelsrestframework": ["djangochannelsrestframework"], - "djangorestframework": ["rest_framework"], - "djangorestframework-simplejwt": ["rest_framework_simplejwt"], - "drf-nested-routers": ["rest_framework_nested"], - "drf-spectacular": ["drf_spectacular"], - "drf-standardized-errors": ["drf_standardized_errors"], "Mastodon.py": ["mastodon"], "pylint-django": ["pylint_django"], "pylint-plugin-utils": ["pylint_plugin_utils"], + "PyJWT": ["jwt"], "psycopg": ["psycopg"], "psycopg-binary": ["psycopg"], "py-ubjson": ["ubjson"], "python-dotenv": ["dotenv"], "qdrant-client": ["qdrant_client"], + "taskiq-aio-pika": ["taskiq_aio_pika"], + "taskiq-redis": ["taskiq_redis"], "sentence-transformers": ["sentence_transformers"], "types-Deprecated": ["Deprecated"], "types-psycopg2": ["psycopg2"], diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md index 2fb7f938..e0c1c5b8 100644 --- a/INTEGRATIONS.md +++ b/INTEGRATIONS.md @@ -33,4 +33,4 @@ - Email - Reddit -Need a crawler, where you can add a URL for sites that don't have RSS or it's not maintained \ No newline at end of file +Need a crawler, where you can add a URL for sites that don't have RSS or it's not maintained diff --git a/README.md b/README.md index 02f5abe0..524c4880 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The system is organized into projects: each newsletter project has its own track ### Fresh Clone Bootstrap -- Linux and macOS: run `./scripts/bootstrap_dev.sh` from the repo root. The script installs `uv` and `pants` if either is missing, syncs the locked Python environment, installs the frontend dependencies, and sets up git hooks. +- Linux and macOS: run `./scripts/bootstrap_dev.sh` from the repo root. The script installs `uv` and `pants` if either is missing, syncs the locked Python environment, installs the locked workspace JavaScript dependencies including the root Turbo runner, repairs the local Turbo link if needed, and sets up git hooks. - Windows: install `uv`, `pants`, and `just`, then run `just install` from the repo root. **Linux:** @@ -76,7 +76,7 @@ kubectl port-forward svc/digest-engine-digest-engine-nginx 8080:80 - Admin UI: - Frontend UI: -- Swagger schema: +- API docs: - Storybook (`just storybook-dev`): > [!NOTE] @@ -169,7 +169,7 @@ The system is designed for graceful failure, not silent corruption. Unparseable ## Tech Stack -**Backend:** Django + DRF · Celery + Redis · PostgreSQL · Qdrant (vector DB) +**Backend:** Django + Ninja API · Taskiq + RabbitMQ/Redis · PostgreSQL · Qdrant (vector DB) **AI Pipeline:** LangGraph · Claude Skills format · Multi-model via OpenRouter (Llama 3.1, Gemma 3, DeepSeek V3, Qwen 2.5; Command R+ for production) · Ollama for self-hosting · Sentence embeddings diff --git a/SESSION.md b/SESSION.md index 3977f3f3..1180ddfa 100644 --- a/SESSION.md +++ b/SESSION.md @@ -1 +1,238 @@ # Session Restore Point + +## Completed Refactor: Celery To Taskiq + +Goal: replace Celery with Taskiq across the Django runtime, scheduled jobs, deployment manifests, and tests without changing the user-facing behavior of ingestion, AI pipeline execution, newsletter drafting, trend jobs, or avatar processing. This migration should prefer async-native task implementations rather than keeping the existing synchronous task bodies and merely adapting the queue layer. + +### Current Status + +- Python runtime task modules now use Taskiq end to end. The old Celery bootstrap and Celery settings modules have been deleted. +- Telemetry and dependency manifests no longer carry Celery runtime wiring. `uv.lock` and the Pants lockfile were regenerated after removing the Celery packages. +- Local and Helm runtime manifests now use Taskiq worker and scheduler processes and provision RabbitMQ explicitly for the Taskiq broker. +- Test and env harnesses now use `TASKIQ_ALWAYS_EAGER` instead of `CELERY_TASK_ALWAYS_EAGER` across the executable repo surfaces. +- A targeted repo grep across executable code, tests, deploy manifests, env examples, and scripts no longer finds Celery-specific runtime references. +- Public top-level docs now describe the Taskiq-based stack. Remaining Celery mentions are confined to historical planning text in this file. + +### Validated In This Session + +- Regenerated dependency locks with `uv lock` and `pants generate-lockfiles --resolve=python-default`. +- Rendered local and deployed runtime config successfully with `docker compose config` and `helm template digest-engine ./deploy/helm/digest-engine`. +- Re-ran `python manage.py check` after the telemetry, dependency, and deployment edits. +- Re-ran focused pytest slices covering newsletters, users profile API, core eager paths, trends, entities, ingestion, and pipeline retry behavior after replacing the Celery-era eager test harness. + +### Remaining Follow-Up + +- Optional documentation cleanup only: historical Celery terminology remains in this planning note and any external runbooks or dashboards that are not stored in this repo. +- No executable repo surfaces still depend on Celery. + +### Current Queue Surface + +- Taskiq bootstrap lives in [digest_engine/taskiq.py](/home/kevin/Repos/digest-engine/digest_engine/taskiq.py). +- Taskiq settings live in [digest_engine/settings/taskiq.py](/home/kevin/Repos/digest-engine/digest_engine/settings/taskiq.py) and are re-exported from [digest_engine/settings/__init__.py](/home/kevin/Repos/digest-engine/digest_engine/settings/__init__.py). +- Task modules now migrated to Taskiq live in: + - [core/tasks.py](/home/kevin/Repos/digest-engine/core/tasks.py) + - [ingestion/tasks.py](/home/kevin/Repos/digest-engine/ingestion/tasks.py) + - [newsletters/tasks.py](/home/kevin/Repos/digest-engine/newsletters/tasks.py) + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) + - [trends/tasks.py](/home/kevin/Repos/digest-engine/trends/tasks.py) + - [users/ninja_api.py](/home/kevin/Repos/digest-engine/users/ninja_api.py) +- Task dispatch from synchronous Django entrypoints now goes through the shared enqueue seam in [digest_engine/taskiq.py](/home/kevin/Repos/digest-engine/digest_engine/taskiq.py). +- Local and deployed worker scheduling now depends on Taskiq worker and Taskiq scheduler in: + - [docker-compose.yml](/home/kevin/Repos/digest-engine/docker-compose.yml) + - [justfile](/home/kevin/Repos/digest-engine/justfile) + - [deploy/helm/digest-engine/values.yaml](/home/kevin/Repos/digest-engine/deploy/helm/digest-engine/values.yaml) + - [deploy/helm/digest-engine/templates/taskiq-worker-deployment.yaml](/home/kevin/Repos/digest-engine/deploy/helm/digest-engine/templates/taskiq-worker-deployment.yaml) + - [deploy/helm/digest-engine/templates/taskiq-scheduler-deployment.yaml](/home/kevin/Repos/digest-engine/deploy/helm/digest-engine/templates/taskiq-scheduler-deployment.yaml) + - [deploy/helm/digest-engine/templates/keda-scaledobject.yaml](/home/kevin/Repos/digest-engine/deploy/helm/digest-engine/templates/keda-scaledobject.yaml) +- Current entrypoint coverage assumes Taskiq broker and scheduler bootstrap in [core/tests/test_entrypoints.py](/home/kevin/Repos/digest-engine/core/tests/test_entrypoints.py). +- RabbitMQ is now provisioned in local and Helm deployment surfaces alongside Redis result storage. + +### Design Constraints + +- Preserve the current eager-mode ergonomics used in tests and local control flow, where some orchestration tasks run inline when background execution is disabled. +- Preserve task names and call semantics long enough to migrate callers incrementally instead of forcing a big-bang rename. +- Preserve existing schedule cadence from [digest_engine/settings/celery.py](/home/kevin/Repos/digest-engine/digest_engine/settings/celery.py) until behavior is intentionally changed. +- Keep the AI pipeline boundaries intact: persisted state in models, orchestration in task modules, pipeline logic in app-owned helpers. +- Treat async-first task execution as the default target. Only keep synchronous execution where a dependency is fundamentally blocking or CPU-bound. +- Be explicit about sync boundaries in Django and library code. Moving to Taskiq does not make Django ORM, Pillow/image manipulation, or CPU-heavy transforms automatically async. + +### Phase 1 Decisions + +- Adopt `taskiq` as the core queue runtime. +- Use `taskiq-aio-pika` for the primary broker and RabbitMQ as the production queue transport. +- Use `taskiq-redis` for Redis-backed result storage and any Redis-backed scheduling helpers we later decide to add. +- Start with core `TaskiqScheduler` plus `LabelScheduleSource` for the current fixed recurring schedules. Revisit dynamic Redis-backed schedules only if product requirements appear. +- Keep one shared broker/bootstrap module under `digest_engine/`, analogous to the current Celery entrypoint, but Taskiq-native. +- Replace `CELERY_TASK_ALWAYS_EAGER` with a repo-owned Taskiq-era setting and test harness, rather than preserving Celery naming. +- Plan for an async-first rewrite of task definitions. A sync-safe enqueue helper may still exist for synchronous Django call sites, but that helper should bridge caller context to Taskiq and not be treated as justification to keep task implementations mostly synchronous. + +### Explicit Sync Boundaries + +These boundaries are the agreed exceptions to the async-first direction. Async Taskiq handlers should treat these as explicit handoff points instead of letting blocking work leak through the task layer. + +#### 1. Sync Django Entrypoints That Need A Sync-Safe Enqueue Seam + +- Admin actions: + - [projects/admin.py](/home/kevin/Repos/digest-engine/projects/admin.py) `ProjectConfigAdmin.recompute_selected_authority_models` + - [content/admin.py](/home/kevin/Repos/digest-engine/content/admin.py) `ContentAdmin.generate_newsletter_ideas` + - [pipeline/admin.py](/home/kevin/Repos/digest-engine/pipeline/admin.py) review-queue retry admin action +- Management commands: + - [core/management/commands/bootstrap_live_sources.py](/home/kevin/Repos/digest-engine/core/management/commands/bootstrap_live_sources.py) source bootstrap ingestion dispatch +- Sync Ninja or Django request handlers: + - [newsletters/ninja_api.py](/home/kevin/Repos/digest-engine/newsletters/ninja_api.py) `generate_newsletter_draft_route` + - [newsletters/ninja_api.py](/home/kevin/Repos/digest-engine/newsletters/ninja_api.py) `regenerate_newsletter_draft_section_route` + - [pipeline/ninja_api.py](/home/kevin/Repos/digest-engine/pipeline/ninja_api.py) `retry_review_queue_item_route` + - [trends/ninja_api.py](/home/kevin/Repos/digest-engine/trends/ninja_api.py) original-content idea generation route + - [projects/ninja_project_configs_api.py](/home/kevin/Repos/digest-engine/projects/ninja_project_configs_api.py) authority and source-quality recompute trigger + - [users/ninja_api.py](/home/kevin/Repos/digest-engine/users/ninja_api.py) local task enqueue helper for avatar thumbnail generation + - [newsletters/intake.py](/home/kevin/Repos/digest-engine/newsletters/intake.py) `queue_newsletter_intake` +- Scope rule: these call sites should remain synchronous request or command code, but all queue submission should go through one repo-owned Taskiq enqueue helper rather than direct broker calls spread across the codebase. + +#### 2. ORM-Heavy Sync Units + +- The following task modules are dominated by synchronous Django ORM work and should be treated as sync data-access units even when their Taskiq entrypoints become `async def`: + - [core/tasks.py](/home/kevin/Repos/digest-engine/core/tasks.py) + - [ingestion/tasks.py](/home/kevin/Repos/digest-engine/ingestion/tasks.py) + - [newsletters/tasks.py](/home/kevin/Repos/digest-engine/newsletters/tasks.py) + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) + - [trends/tasks.py](/home/kevin/Repos/digest-engine/trends/tasks.py) + - [users/tasks.py](/home/kevin/Repos/digest-engine/users/tasks.py) +- The heaviest downstream ORM helpers that should stay behind explicit sync boundaries are: + - [core/pipeline.py](/home/kevin/Repos/digest-engine/core/pipeline.py) + - [newsletters/composition.py](/home/kevin/Repos/digest-engine/newsletters/composition.py) + - [entities/extraction.py](/home/kevin/Repos/digest-engine/entities/extraction.py) +- Scope rule: async Taskiq handlers may orchestrate these calls, but database-intensive units should be isolated with `sync_to_async`, thread offloading, or dedicated synchronous helper functions rather than mixing raw ORM calls directly throughout async control flow. + +#### 3. CPU-Bound Or File-Bound Work + +- [users/tasks.py](/home/kevin/Repos/digest-engine/users/tasks.py) `build_avatar_thumbnail` + - Uses Pillow image decoding, EXIF transpose, resize, WebP encoding, and storage writes. + - This should remain a sync helper invoked via thread or executor offload. +- [newsletters/extraction.py](/home/kevin/Repos/digest-engine/newsletters/extraction.py) `_extract_newsletter_items_heuristically` + - Uses `HTMLParser` and regex-based extraction over raw newsletter bodies. + - This is a reasonable sync parsing unit and does not need to be forced async. +- [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) candidate clustering and identity-enrichment helpers + - Includes string-similarity clustering and content normalization work alongside ORM access. + - Keep the compute-heavy portions behind explicit sync boundaries unless a clear async benefit appears. +- [core/embeddings.py](/home/kevin/Repos/digest-engine/core/embeddings.py) sentence-transformer embedding generation + - Local model inference is blocking compute work even if the surrounding task becomes async. + +#### 4. Blocking Third-Party Clients And Libraries + +- HTTP clients currently used in synchronous mode: + - [core/llm.py](/home/kevin/Repos/digest-engine/core/llm.py) `httpx.post` for OpenRouter chat completions + - [core/embeddings.py](/home/kevin/Repos/digest-engine/core/embeddings.py) `httpx.post` for Ollama and OpenRouter embeddings + - [content/deduplication.py](/home/kevin/Repos/digest-engine/content/deduplication.py) `httpx.head` for short-link expansion + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) `httpx.get` for identity probing +- Sync SDKs and parsers that should be assumed blocking unless replaced: + - [ingestion/plugins/linkedin.py](/home/kevin/Repos/digest-engine/ingestion/plugins/linkedin.py) `requests.post` + - [ingestion/plugins/rss.py](/home/kevin/Repos/digest-engine/ingestion/plugins/rss.py) `feedparser.parse` + - [ingestion/plugins/reddit.py](/home/kevin/Repos/digest-engine/ingestion/plugins/reddit.py) `praw.Reddit` + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) `atproto.Client` + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) `mastodon.Mastodon` + - [core/embeddings.py](/home/kevin/Repos/digest-engine/core/embeddings.py) `QdrantClient` +- Scope rule: + - Convert `httpx` usage to `httpx.AsyncClient` where practical. + - Keep obviously sync-only libraries behind thread offload unless we replace them with async-capable alternatives. + - Treat Qdrant and embedding provider access as explicit boundaries inside the embedding layer, not as direct async logic scattered across tasks. + +#### 5. Existing Sync-To-Async Precedent + +- [messaging/signals.py](/home/kevin/Repos/digest-engine/messaging/signals.py) and [notifications/signals.py](/home/kevin/Repos/digest-engine/notifications/signals.py) already use `async_to_sync` to bridge sync Django signals into async channel-layer operations. +- Scope rule: use the same style of narrow bridge for Taskiq enqueueing from sync Django entrypoints, but do not use that bridge as a reason to keep task implementations themselves predominantly synchronous. + +### Migration Record + +The original phase plan below is preserved as a historical record of the migration approach that landed. The runtime, dependency, deploy, and test-harness work described there is now complete. + +### Proposed Implementation Plan + +#### Phase 1: Choose The Taskiq Runtime Shape + +- Implement the selected Taskiq stack for this repo: + - `taskiq` + - `taskiq-aio-pika` + - `taskiq-redis` +- Define the Django bootstrap module and Taskiq broker wiring under `digest_engine/`. +- Define the initial scheduler shape using core `TaskiqScheduler` and `LabelScheduleSource` for code-owned recurring jobs. +- Define the replacement for `CELERY_TASK_ALWAYS_EAGER`, likely a Taskiq-specific eager/immediate execution setting plus a local helper seam for tests. + +#### Phase 2: Introduce A Taskiq Compatibility Layer + +- Add Taskiq dependencies to [pyproject.toml](/home/kevin/Repos/digest-engine/pyproject.toml) and regenerate [3rdparty/python/default.lock](/home/kevin/Repos/digest-engine/3rdparty/python/default.lock). +- Create a new broker/bootstrap module under `digest_engine/` to initialize Taskiq against Django settings, RabbitMQ, and Redis result storage. +- Add a small dispatch abstraction that replaces direct `.delay(...)` knowledge with one repo-owned seam, so synchronous Django callers can enqueue Taskiq work safely while the internals move to async-first execution. +- Add settings for broker URL, Redis result backend, eager mode, timeout/retry defaults, and scheduler configuration without removing Celery yet. +- Keep this first slice additive so the repo can compile and tests can run before task modules are ported. + +#### Phase 3: Port Task Definitions Module By Module + +- Migrate [core/tasks.py](/home/kevin/Repos/digest-engine/core/tasks.py) first because it is the largest cross-cutting orchestration surface and defines helper patterns reused elsewhere. +- Port the remaining task modules in dependency order: + - [ingestion/tasks.py](/home/kevin/Repos/digest-engine/ingestion/tasks.py) + - [newsletters/tasks.py](/home/kevin/Repos/digest-engine/newsletters/tasks.py) + - [trends/tasks.py](/home/kevin/Repos/digest-engine/trends/tasks.py) + - [entities/tasks.py](/home/kevin/Repos/digest-engine/entities/tasks.py) + - [users/tasks.py](/home/kevin/Repos/digest-engine/users/tasks.py) +- Replace `@shared_task(...)` with the Taskiq task decorator strategy selected in Phase 1. +- Convert task entrypoints to `async def` where the surrounding work can be made async with reasonable effort. +- Replace synchronous external I/O inside tasks with async-capable clients where available. +- Move unavoidable synchronous work behind explicit boundaries such as `sync_to_async`, `asyncio.to_thread`, or executor-based helpers rather than leaving whole task modules synchronous by default. +- Replace local `DelayedTask` / `_enqueue_task` helpers with a broker-agnostic enqueue helper that supports immediate execution where required. +- Verify that task return values, ignored results, retries, logging behavior, and async error handling remain acceptable for each converted module. + +#### Phase 4: Replace Call Sites And Scheduling + +- Replace `.delay(...)` dispatch from runtime callers with the new enqueue seam in these high-signal surfaces first. Prefer async-native call sites when already in async contexts; use a narrow sync-to-async bridge only where Django entrypoints remain synchronous: + - [newsletters/intake.py](/home/kevin/Repos/digest-engine/newsletters/intake.py) + - [newsletters/ninja_api.py](/home/kevin/Repos/digest-engine/newsletters/ninja_api.py) + - [projects/admin.py](/home/kevin/Repos/digest-engine/projects/admin.py) + - [projects/ninja_project_configs_api.py](/home/kevin/Repos/digest-engine/projects/ninja_project_configs_api.py) + - [content/admin.py](/home/kevin/Repos/digest-engine/content/admin.py) + - [pipeline/admin.py](/home/kevin/Repos/digest-engine/pipeline/admin.py) + - [pipeline/ninja_api.py](/home/kevin/Repos/digest-engine/pipeline/ninja_api.py) + - [trends/ninja_api.py](/home/kevin/Repos/digest-engine/trends/ninja_api.py) + - [core/management/commands/bootstrap_live_sources.py](/home/kevin/Repos/digest-engine/core/management/commands/bootstrap_live_sources.py) + - [entities/extraction.py](/home/kevin/Repos/digest-engine/entities/extraction.py) +- Recreate the current recurring schedules from [digest_engine/settings/celery.py](/home/kevin/Repos/digest-engine/digest_engine/settings/celery.py) in the Taskiq scheduler layer. +- Keep schedule names and cadence recognizable so operational diffs are easy to review. + +#### Phase 5: Replace Worker, Scheduler, And Deployment Wiring + +- Update [digest_engine/settings/__init__.py](/home/kevin/Repos/digest-engine/digest_engine/settings/__init__.py) to export Taskiq settings instead of Celery settings. +- Remove [digest_engine/celery.py](/home/kevin/Repos/digest-engine/digest_engine/celery.py) once all imports are gone and replace it with the Taskiq broker entrypoint. +- Update local development commands in [justfile](/home/kevin/Repos/digest-engine/justfile) to start Taskiq worker and scheduler processes instead of Celery worker and beat. +- Update [docker-compose.yml](/home/kevin/Repos/digest-engine/docker-compose.yml) service names and commands for Taskiq runtime processes. +- Update Helm values, worker/scheduler deployments, KEDA config, and network policy references under [deploy/helm/digest-engine](/home/kevin/Repos/digest-engine/deploy/helm/digest-engine). +- Replace Celery-specific OpenTelemetry instrumentation in [pyproject.toml](/home/kevin/Repos/digest-engine/pyproject.toml) with the Taskiq equivalent if one is available; otherwise document the temporary observability gap. + +#### Phase 6: Remove Celery And Finish Cleanup + +- Delete Celery settings module and any remaining `CELERY_*` environment variables once Taskiq config is live. +- Remove Celery dependency and Celery instrumentation from [pyproject.toml](/home/kevin/Repos/digest-engine/pyproject.toml). +- Regenerate Pants lockfiles after dependency changes. +- Update tests that currently assert Celery app bootstrapping and beat schedule behavior, starting with [core/tests/test_entrypoints.py](/home/kevin/Repos/digest-engine/core/tests/test_entrypoints.py). +- Search for remaining `celery`, `shared_task`, `.delay(`, `apply_async`, `celery-worker`, and `celery-beat` references and remove or rename them. +- Update operator and contributor docs once the runtime commands and deployment terminology change. + +### Suggested Validation Per Phase + +- After Phase 2: run a narrow import/config check for the new broker bootstrap plus `python manage.py check`. +- After each task-module migration: run the nearest focused pytest module plus `python manage.py check`. +- After scheduler migration: add or update targeted tests for scheduled job registration and cadence. +- After compose/helm changes: run `just lint` and the existing Helm lint target. +- Before deleting Celery: run `just test` and confirm no repo matches for Celery runtime imports remain outside historical notes. + +### Risks To Watch + +- Taskiq scheduling semantics may not match Celery beat exactly, especially for mixed interval and cron jobs. +- Taskiq retry and result-handling behavior may differ from Celery for long-running AI or ingestion jobs. +- Existing eager-mode assumptions may be embedded in tests or orchestration helpers and need an explicit compatibility shim. +- A fully async rewrite will expose blocking dependencies that were previously hidden inside synchronous Celery workers, especially ORM-heavy code paths and CPU-bound helpers. +- Some code paths may only become partially async because Django ORM usage, image processing, or third-party SDKs still require thread or process offloading. +- Telemetry coverage may regress if Celery instrumentation is removed before a Taskiq replacement is wired. +- Deployment naming changes may require coordinated updates in dashboards, scaling policies, and operator expectations. + +### Immediate Next Step + +- No runtime migration work remains in-repo. If a future pass is needed, limit it to historical wording cleanup and external operator collateral. diff --git a/content/admin.py b/content/admin.py index 38bc874a..f0558843 100644 --- a/content/admin.py +++ b/content/admin.py @@ -9,6 +9,7 @@ from content.models import Content, UserFeedback from core.settings_types import CoreSettings +from digest_engine.taskiq import enqueue_task, run_task_inline, task_always_eager def _score_to_percent(value): @@ -312,7 +313,10 @@ def generate_newsletter_ideas(self, request, queryset): content_ids = list(queryset.values_list("id", flat=True)) for content_id in content_ids: - process_content.delay(content_id) + if task_always_eager(): + run_task_inline(process_content, content_id) + else: + enqueue_task(process_content, content_id) self.message_user( request, f"Successfully queued the pipeline for {len(content_ids)} items.", diff --git a/content/api.py b/content/api.py deleted file mode 100644 index f1de0ab0..00000000 --- a/content/api.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Content-domain API viewsets kept under the existing nested project routes.""" - -from drf_spectacular.utils import extend_schema -from rest_framework import serializers, status, viewsets -from rest_framework.decorators import action -from rest_framework.permissions import BasePermission -from rest_framework.response import Response - -from content.models import Content, UserFeedback -from content.serializers import ContentSerializer, UserFeedbackSerializer -from core.api import ( - AUTHENTICATION_REQUIRED_RESPONSE, - CONTENT_CREATE_REQUEST_EXAMPLE, - CONTENT_RESPONSE_EXAMPLE, - PROJECT_ID_PARAMETER, - SKILL_NAME_PARAMETER, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import ( - IsProjectAdmin, - IsProjectFeedbackEditor, - IsProjectMember, - IsProjectMemberWritable, -) -from pipeline.serializers import SkillResultSerializer - -CLASSIFICATION_SKILL_NAME = "content_classification" -RELEVANCE_SKILL_NAME = "relevance_scoring" -SUMMARIZATION_SKILL_NAME = "summarization" -RELATED_CONTENT_SKILL_NAME = "find_related" - - -@document_project_owned_viewset( - resource_plural="content items", - resource_singular="content item", - create_description="Create a new content item for the selected project. Any related entity must belong to the same project.", - tag="Content Library", - action_overrides=build_crud_action_overrides( - ContentSerializer, - resource_plural="content items for the selected project", - resource_singular="content item", - create_examples=[CONTENT_CREATE_REQUEST_EXAMPLE, CONTENT_RESPONSE_EXAMPLE], - create_response_examples=[CONTENT_RESPONSE_EXAMPLE], - retrieve_examples=[CONTENT_RESPONSE_EXAMPLE], - ), -) -class ContentViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Browse project content and trigger ad hoc AI processing for it.""" - - serializer_class = ContentSerializer - queryset = Content.objects.select_related("project", "entity") - - def get_permissions(self): - """Allow all members to read content, contributors to edit, and admins to delete.""" - - permission_classes: list[type[BasePermission]] - if self.action == "destroy": - permission_classes = [IsProjectAdmin] - elif self.action in {"create", "update", "partial_update", "run_skill"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - @extend_schema( - summary="Run content skill", - description=( - "Run one ad hoc skill for the selected content item and persist the outcome as a SkillResult. " - "Supported skill names are content_classification, relevance_scoring, summarization, and find_related." - ), - tags=["AI Processing"], - parameters=[PROJECT_ID_PARAMETER, SKILL_NAME_PARAMETER], - request=None, - responses={ - 201: SkillResultSerializer, - 202: SkillResultSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path=r"skills/(?P[^/.]+)") - def run_skill(self, request, *args, **kwargs): - """Execute one supported ad hoc skill for a content item.""" - - from core.pipeline import execute_ad_hoc_skill - from core.tasks import queue_content_skill - - skill_name = str(kwargs["skill_name"]) - if skill_name not in { - CLASSIFICATION_SKILL_NAME, - RELEVANCE_SKILL_NAME, - SUMMARIZATION_SKILL_NAME, - RELATED_CONTENT_SKILL_NAME, - }: - raise serializers.ValidationError( - { - "skill_name": ( - "Unsupported skill. Choose one of: content_classification, relevance_scoring, " - "summarization, find_related." - ) - } - ) - - content = self.get_object() - if skill_name in {RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME}: - skill_result = queue_content_skill(content, skill_name) - serializer = SkillResultSerializer( - skill_result, context=self.get_serializer_context() - ) - return Response(serializer.data, status=status.HTTP_202_ACCEPTED) - - skill_result = execute_ad_hoc_skill(content, skill_name) - serializer = SkillResultSerializer( - skill_result, context=self.get_serializer_context() - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - -@document_project_owned_viewset( - resource_plural="user feedback entries", - resource_singular="user feedback entry", - create_description="Create a new feedback entry for content in the selected project. The authenticated user is recorded automatically.", - tag="Feedback", - action_overrides=build_crud_action_overrides( - UserFeedbackSerializer, - resource_plural="user feedback entries for the selected project", - resource_singular="user feedback entry", - ), -) -class UserFeedbackViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Capture editor feedback on project content items.""" - - serializer_class = UserFeedbackSerializer - queryset = UserFeedback.objects.select_related("content", "project", "user") - - def get_permissions(self): - """Allow all members to read feedback and owners or admins to modify it.""" - - return [IsProjectFeedbackEditor()] - - def perform_create(self, serializer): - """Attach the authenticated user automatically to new feedback rows.""" - - serializer.save(project=self.get_project(), user=self.request.user) diff --git a/content/api_urls.py b/content/api_urls.py deleted file mode 100644 index 78111e6b..00000000 --- a/content/api_urls.py +++ /dev/null @@ -1,14 +0,0 @@ -"""API route registration for content resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from content.api import ContentViewSet, UserFeedbackViewSet - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested content and feedback endpoints.""" - - project_router.register(r"contents", ContentViewSet, basename="project-content") - project_router.register( - r"feedback", UserFeedbackViewSet, basename="project-feedback" - ) diff --git a/content/ninja_api.py b/content/ninja_api.py new file mode 100644 index 00000000..3bd288f8 --- /dev/null +++ b/content/ninja_api.py @@ -0,0 +1,607 @@ +"""Django Ninja endpoints for project-scoped content resources.""" + +from __future__ import annotations + +import datetime +from typing import Any + +from django.core.exceptions import ValidationError +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from content.models import Content, UserFeedback +from core.ninja_api import api_authenticate +from entities.models import Entity +from projects.models import ProjectMembership, ProjectRole +from projects.ninja_helpers import ( + _get_project_or_404, + _require_project_admin, + _require_project_writable, +) + +content_router = Router(tags=["Content Library"]) +feedback_router = Router(tags=["Feedback"]) + +CLASSIFICATION_SKILL_NAME = "content_classification" +RELEVANCE_SKILL_NAME = "relevance_scoring" +SUMMARIZATION_SKILL_NAME = "summarization" +RELATED_CONTENT_SKILL_NAME = "find_related" + +SUPPORTED_SKILL_NAMES = { + CLASSIFICATION_SKILL_NAME, + RELEVANCE_SKILL_NAME, + SUMMARIZATION_SKILL_NAME, + RELATED_CONTENT_SKILL_NAME, +} +QUEUED_SKILL_NAMES = {RELEVANCE_SKILL_NAME, SUMMARIZATION_SKILL_NAME} + + +class ContentSchema(Schema): + """Serialized content payload.""" + + id: int + project: int + url: str + title: str + author: str + entity: int | None = None + source_plugin: str + content_type: str + canonical_url: str + published_date: datetime.datetime + ingested_at: datetime.datetime + content_text: str + summary_text: str + relevance_score: float | None = None + authority_adjusted_score: float | None = None + embedding_id: str + source_metadata: dict[str, Any] + duplicate_of: int | None = None + duplicate_signal_count: int + is_reference: bool + is_active: bool + newsletter_promotion_at: datetime.datetime | None = None + newsletter_promotion_by: int | None = None + newsletter_promotion_theme: int | None = None + + +class ContentCreateInput(Schema): + """Writable content fields for create requests.""" + + url: str + title: str + author: str = "" + entity: int | None = None + source_plugin: str + content_type: str = "" + published_date: datetime.datetime + content_text: str + source_metadata: dict[str, Any] | None = None + is_reference: bool = False + is_active: bool = True + project: int | None = None + + +class ContentUpdateInput(Schema): + """Writable content fields for update requests.""" + + url: str | None = None + title: str | None = None + author: str | None = None + entity: int | None = None + source_plugin: str | None = None + content_type: str | None = None + published_date: datetime.datetime | None = None + content_text: str | None = None + source_metadata: dict[str, Any] | None = None + is_reference: bool | None = None + is_active: bool | None = None + project: int | None = None + + +class SkillResultSchema(Schema): + """Serialized ad hoc skill execution payload.""" + + id: int + content: int + project: int + skill_name: str + status: str + result_data: dict[str, Any] | list[Any] | None = None + error_message: str + model_used: str + latency_ms: int | None = None + confidence: float | None = None + invocation_id: str + created_at: datetime.datetime + superseded_by: int | None = None + + +class UserFeedbackSchema(Schema): + """Serialized content feedback payload.""" + + id: int + content: int + project: int + user: int + feedback_type: str + created_at: datetime.datetime + + +class UserFeedbackCreateInput(Schema): + """Writable feedback fields for create requests.""" + + content: int + feedback_type: str + + +class UserFeedbackUpdateInput(Schema): + """Writable feedback fields for update requests.""" + + content: int | None = None + feedback_type: str | None = None + + +def _serialize_content(content: Content) -> dict[str, Any]: + """Return one content response body.""" + + return { + "id": int(content.pk), + "project": content.project_id, + "url": content.url, + "title": content.title, + "author": content.author, + "entity": content.entity_id, + "source_plugin": content.source_plugin, + "content_type": content.content_type, + "canonical_url": content.canonical_url, + "published_date": content.published_date, + "ingested_at": content.ingested_at, + "content_text": content.content_text, + "summary_text": content.summary_text, + "relevance_score": content.relevance_score, + "authority_adjusted_score": content.authority_adjusted_score, + "embedding_id": content.embedding_id, + "source_metadata": content.source_metadata, + "duplicate_of": content.duplicate_of_id, + "duplicate_signal_count": content.duplicate_signal_count, + "is_reference": content.is_reference, + "is_active": content.is_active, + "newsletter_promotion_at": content.newsletter_promotion_at, + "newsletter_promotion_by": content.newsletter_promotion_by_id, + "newsletter_promotion_theme": content.newsletter_promotion_theme_id, + } + + +def _serialize_skill_result(skill_result: Any) -> dict[str, Any]: + """Return one persisted skill result response body.""" + + return { + "id": int(skill_result.pk), + "content": skill_result.content_id, + "project": skill_result.project_id, + "skill_name": skill_result.skill_name, + "status": skill_result.status, + "result_data": skill_result.result_data, + "error_message": skill_result.error_message, + "model_used": skill_result.model_used, + "latency_ms": skill_result.latency_ms, + "confidence": skill_result.confidence, + "invocation_id": str(skill_result.invocation_id), + "created_at": skill_result.created_at, + "superseded_by": skill_result.superseded_by_id, + } + + +def _serialize_feedback(feedback: UserFeedback) -> dict[str, Any]: + """Return one feedback response body.""" + + return { + "id": int(feedback.pk), + "content": feedback.content_id, + "project": feedback.project_id, + "user": feedback.user_id, + "feedback_type": feedback.feedback_type, + "created_at": feedback.created_at, + } + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _error_payload(field: str, message: str) -> dict[str, list[str]]: + """Return the native Ninja error payload shape.""" + + return {field: [message]} + + +def _resolve_entity_for_project( + entity_id: int | None, + *, + project_id: int, +) -> tuple[Entity | None, dict[str, list[str]] | None]: + """Resolve one entity reference and enforce project ownership.""" + + if entity_id is None: + return None, None + entity = Entity.objects.filter(pk=entity_id).first() + if entity is None or entity.project_id != project_id: + return None, _error_payload( + "entity", "Entity must belong to the selected project." + ) + return entity, None + + +def _validated_content_payload( + payload: dict[str, Any], + *, + project, + instance: Content | None = None, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate one content payload.""" + + validated_payload = dict(payload) + validated_payload.pop("project", None) + + entity = instance.entity if instance is not None else None + if "entity" in validated_payload: + entity, entity_errors = _resolve_entity_for_project( + validated_payload.pop("entity"), + project_id=project.id, + ) + if entity_errors is not None: + return validated_payload, entity_errors + + content = instance or Content(project=project) + for field_name, value in validated_payload.items(): + setattr(content, field_name, value) + content.entity = entity + try: + content.full_clean() + except ValidationError as exc: + return validated_payload, _validation_error_payload(exc) + + validated_payload["entity"] = entity + return validated_payload, None + + +def _resolve_feedback_content( + content_id: int, + *, + project_id: int, +) -> tuple[Content | None, dict[str, list[str]] | None]: + """Resolve one content reference for feedback and enforce project ownership.""" + + content = Content.objects.filter(pk=content_id).first() + if content is None or content.project_id != project_id: + return None, _error_payload( + "content", "Content must belong to the selected project." + ) + return content, None + + +def _validated_feedback_payload( + payload: dict[str, Any], + *, + project, + user, + instance: UserFeedback | None = None, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate one feedback payload.""" + + validated_payload = dict(payload) + content = instance.content if instance is not None else None + if "content" in validated_payload: + content, content_errors = _resolve_feedback_content( + validated_payload.pop("content"), + project_id=project.id, + ) + if content_errors is not None: + return validated_payload, content_errors + + feedback = instance or UserFeedback(project=project, user=user) + for field_name, value in validated_payload.items(): + setattr(feedback, field_name, value) + feedback.project = project + feedback.user = user + feedback.content = content + try: + feedback.full_clean() + except ValidationError as exc: + return validated_payload, _validation_error_payload(exc) + + validated_payload["content"] = content + return validated_payload, None + + +def _get_content_or_404(project_id: int, content_id: int) -> Content: + """Load one content row for the selected project.""" + + content = ( + Content.objects.select_related("project", "entity") + .filter(project_id=project_id, pk=content_id) + .first() + ) + if not content: + raise HttpError(404, "Not found.") + return content + + +def _get_feedback_or_404(project_id: int, feedback_id: int) -> UserFeedback: + """Load one feedback row for the selected project.""" + + feedback = ( + UserFeedback.objects.select_related("content", "project", "user") + .filter(project_id=project_id, pk=feedback_id) + .first() + ) + if not feedback: + raise HttpError(404, "Not found.") + return feedback + + +def _require_feedback_editor(request: Any, feedback: UserFeedback) -> None: + """Require admin access or ownership for one feedback mutation.""" + + membership = ProjectMembership.objects.filter( + project=feedback.project, + user=request.user, + ).first() + if not membership: + raise HttpError(403, "You do not have permission to perform this action.") + if membership.role == ProjectRole.ADMIN: + return + if membership.role == ProjectRole.MEMBER and feedback.user_id == request.user.id: + return + raise HttpError(403, "You do not have permission to perform this action.") + + +@content_router.get("/", response=list[ContentSchema], auth=api_authenticate) +def list_contents(request: Any, project_id: int = Path(...)): + """List content rows visible to the current project member.""" + + _get_project_or_404(request, project_id) + contents = Content.objects.select_related("project", "entity").filter( + project_id=project_id + ) + return [_serialize_content(content) for content in contents] + + +@content_router.post( + "/", + response={201: ContentSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_content( + request: Any, + payload: ContentCreateInput, + project_id: int = Path(...), +): + """Create one content row under the selected project.""" + + project = _require_project_writable(request, project_id) + validated_payload, errors = _validated_content_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=project, + ) + if errors is not None: + return Status(400, errors) + content = Content.objects.create(project=project, **validated_payload) + return Status(201, _serialize_content(content)) + + +@content_router.get( + "/{content_id}/", + response=ContentSchema, + auth=api_authenticate, +) +def get_content( + request: Any, + project_id: int = Path(...), + content_id: int = Path(...), +): + """Return one content row.""" + + _get_project_or_404(request, project_id) + return _serialize_content(_get_content_or_404(project_id, content_id)) + + +@content_router.patch( + "/{content_id}/", + response={200: ContentSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_content( + request: Any, + payload: ContentUpdateInput, + project_id: int = Path(...), + content_id: int = Path(...), +): + """Update one content row.""" + + _require_project_writable(request, project_id) + content = _get_content_or_404(project_id, content_id) + validated_payload, errors = _validated_content_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=content.project, + instance=content, + ) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(content, field_name, value) + content.save() + return _serialize_content(content) + + +@content_router.delete( + "/{content_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_content( + request: Any, + project_id: int = Path(...), + content_id: int = Path(...), +): + """Delete one content row.""" + + _require_project_admin(request, project_id) + content = _get_content_or_404(project_id, content_id) + content.delete() + return Status(204, None) + + +@content_router.post( + "/{content_id}/skills/{skill_name}/", + response={ + 201: SkillResultSchema, + 202: SkillResultSchema, + 400: dict[str, list[str]], + }, + auth=api_authenticate, +) +def run_content_skill( + request: Any, + project_id: int = Path(...), + content_id: int = Path(...), + skill_name: str = Path(...), +): + """Run one supported ad hoc skill for a content row.""" + + _require_project_writable(request, project_id) + if skill_name not in SUPPORTED_SKILL_NAMES: + return Status( + 400, + { + "skill_name": [ + "Unsupported skill. Choose one of: content_classification, relevance_scoring, summarization, find_related." + ] + }, + ) + + content = _get_content_or_404(project_id, content_id) + if skill_name in QUEUED_SKILL_NAMES: + from core.tasks import queue_content_skill + + skill_result = queue_content_skill(content, skill_name) + return Status(202, _serialize_skill_result(skill_result)) + + from core.pipeline import execute_ad_hoc_skill + + skill_result = execute_ad_hoc_skill(content, skill_name) + return Status(201, _serialize_skill_result(skill_result)) + + +@feedback_router.get("/", response=list[UserFeedbackSchema], auth=api_authenticate) +def list_feedback(request: Any, project_id: int = Path(...)): + """List feedback rows visible to the current project member.""" + + _get_project_or_404(request, project_id) + feedback_rows = UserFeedback.objects.select_related( + "content", "project", "user" + ).filter(project_id=project_id) + return [_serialize_feedback(feedback) for feedback in feedback_rows] + + +@feedback_router.post( + "/", + response={201: UserFeedbackSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_feedback( + request: Any, + payload: UserFeedbackCreateInput, + project_id: int = Path(...), +): + """Create one feedback row and attach the authenticated user.""" + + project = _require_project_writable(request, project_id) + validated_payload, errors = _validated_feedback_payload( + payload.model_dump(exclude_unset=True), + project=project, + user=request.user, + ) + if errors is not None: + return Status(400, errors) + feedback = UserFeedback.objects.create( + project=project, + user=request.user, + **validated_payload, + ) + return Status(201, _serialize_feedback(feedback)) + + +@feedback_router.get( + "/{feedback_id}/", + response=UserFeedbackSchema, + auth=api_authenticate, +) +def get_feedback( + request: Any, + project_id: int = Path(...), + feedback_id: int = Path(...), +): + """Return one feedback row.""" + + _get_project_or_404(request, project_id) + return _serialize_feedback(_get_feedback_or_404(project_id, feedback_id)) + + +@feedback_router.patch( + "/{feedback_id}/", + response={200: UserFeedbackSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_feedback( + request: Any, + payload: UserFeedbackUpdateInput, + project_id: int = Path(...), + feedback_id: int = Path(...), +): + """Update one feedback row, requiring admin access or ownership.""" + + feedback = _get_feedback_or_404(project_id, feedback_id) + _require_feedback_editor(request, feedback) + validated_payload, errors = _validated_feedback_payload( + payload.model_dump(exclude_unset=True), + project=feedback.project, + user=feedback.user, + instance=feedback, + ) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(feedback, field_name, value) + feedback.save() + return _serialize_feedback(feedback) + + +@feedback_router.delete( + "/{feedback_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_feedback( + request: Any, + project_id: int = Path(...), + feedback_id: int = Path(...), +): + """Delete one feedback row, requiring admin access or ownership.""" + + feedback = _get_feedback_or_404(project_id, feedback_id) + _require_feedback_editor(request, feedback) + feedback.delete() + return Status(204, None) + + +__all__ = ["content_router", "feedback_router"] diff --git a/content/serializers.py b/content/serializers.py deleted file mode 100644 index ce3284ec..00000000 --- a/content/serializers.py +++ /dev/null @@ -1,94 +0,0 @@ -"""DRF serializers for content-domain models.""" - -from rest_framework import serializers - -from content.models import Content, UserFeedback -from core.serializer_mixins import ProjectScopedSerializerMixin - - -class ContentSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize ingested content items and enforce project/entity consistency.""" - - class Meta: - model = Content - fields = [ - "id", - "project", - "url", - "title", - "author", - "entity", - "source_plugin", - "content_type", - "canonical_url", - "published_date", - "ingested_at", - "content_text", - "summary_text", - "relevance_score", - "authority_adjusted_score", - "embedding_id", - "source_metadata", - "duplicate_of", - "duplicate_signal_count", - "is_reference", - "is_active", - "newsletter_promotion_at", - "newsletter_promotion_by", - "newsletter_promotion_theme", - ] - read_only_fields = [ - "id", - "project", - "canonical_url", - "ingested_at", - "summary_text", - "authority_adjusted_score", - "embedding_id", - "duplicate_of", - "duplicate_signal_count", - "newsletter_promotion_at", - "newsletter_promotion_by", - "newsletter_promotion_theme", - ] - - def validate(self, attrs): - """Reject entity assignments that point at a different project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - entity = attrs.get("entity") or getattr(self.instance, "entity", None) - if project and entity and entity.project_id != project.id: - raise serializers.ValidationError( - {"entity": "Entity must belong to the selected project."} - ) - return attrs - - -class UserFeedbackSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize editor feedback attached to a content item.""" - - user = serializers.PrimaryKeyRelatedField(read_only=True) - - class Meta: - model = UserFeedback - fields = ["id", "content", "project", "user", "feedback_type", "created_at"] - read_only_fields = ["id", "project", "user", "created_at"] - - def validate(self, attrs): - """Reject feedback that targets content outside the active project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - content = attrs.get("content") or getattr(self.instance, "content", None) - if project and content and content.project_id != project.id: - raise serializers.ValidationError( - {"content": "Content must belong to the selected project."} - ) - return attrs diff --git a/content/tests/test_admin.py b/content/tests/test_admin.py index b66537d6..88db149e 100644 --- a/content/tests/test_admin.py +++ b/content/tests/test_admin.py @@ -1,6 +1,6 @@ from types import SimpleNamespace from typing import Any, cast -from unittest.mock import ANY, Mock +from unittest.mock import ANY, AsyncMock, Mock import pytest from django.contrib import messages @@ -247,7 +247,10 @@ def test_generate_newsletter_ideas_queues_selected_content( published_date=timezone.now(), content_text="Queue two.", ) - delay_mock = mocker.patch("core.tasks.process_content.delay") + queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) admin_instance = ContentAdmin(Content, AdminSite()) message_user_mock = _message_user_mock(admin_instance, mocker) @@ -258,9 +261,9 @@ def test_generate_newsletter_ideas_queues_selected_content( ).order_by("id"), ) - delay_mock.assert_any_call(_require_pk(first_content)) - delay_mock.assert_any_call(_require_pk(second_content)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(_require_pk(first_content)) + queue_mock.assert_any_await(_require_pk(second_content)) + assert queue_mock.await_count == 2 message_user_mock.assert_called_once_with( ANY, "Successfully queued the pipeline for 2 items.", diff --git a/content/tests/test_api.py b/content/tests/test_ninja_api.py similarity index 60% rename from content/tests/test_api.py rename to content/tests/test_ninja_api.py index 44264d9c..6dcc2544 100644 --- a/content/tests/test_api.py +++ b/content/tests/test_ninja_api.py @@ -1,18 +1,19 @@ +from __future__ import annotations + +from http import HTTPStatus from types import SimpleNamespace from typing import Any, cast -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from django.contrib.auth import get_user_model from django.db.models import Model +from django.test import TestCase from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase from content.models import Content, FeedbackType, UserFeedback from entities.models import Entity from pipeline.models import SkillResult, SkillStatus from projects.models import Project, ProjectMembership, ProjectRole -from trends.models import ThemeSuggestion, ThemeSuggestionStatus, TopicCluster def _require_pk(instance: Model) -> int: @@ -24,24 +25,23 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - def _create_user(user_model: type[Any], **kwargs: object): """Create a user through the custom manager with a typed escape hatch.""" return cast(Any, user_model.objects).create_user(**kwargs) -class ContentApiTests(APITestCase): - """Exercise content- and feedback-owned project API endpoints.""" +class ContentNinjaApiTests(TestCase): + """Exercise project-scoped Ninja content and feedback endpoints.""" def setUp(self): user_model = get_user_model() self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member = _create_user( + user_model, + username="member", + password="testpass123", + ) self.other_user = _create_user( user_model, username="other", @@ -60,6 +60,11 @@ def setUp(self): project=self.owner_project, role=ProjectRole.ADMIN, ) + ProjectMembership.objects.create( + user=self.member, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) ProjectMembership.objects.create( user=self.other_user, project=self.other_project, @@ -95,7 +100,7 @@ def setUp(self): published_date="2026-04-21T00:00:00Z", content_text="Other content text", ) - _typed_client(self.client).force_authenticate(self.owner) + self.client.force_login(self.owner) def assert_standardized_validation_error( self, payload: dict[str, object], attr: str @@ -106,64 +111,52 @@ def assert_standardized_validation_error( errors = cast(list[dict[str, object]], payload["errors"]) self.assertTrue(any(error["attr"] == attr for error in errors)) - @patch("content.signals.queue_topic_centroid_recompute") - def test_feedback_create_assigns_current_user(self, queue_centroid_mock): - response = self.client.post( - reverse( - "v1:project-feedback-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "content": _require_pk(self.owner_content), - "feedback_type": FeedbackType.UPVOTE, - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - feedback = UserFeedback.objects.get() - self.assertEqual(feedback.user, self.owner) - self.assertEqual(feedback.feedback_type, FeedbackType.UPVOTE) - queue_centroid_mock.assert_called_once_with(_require_pk(self.owner_project)) - - def test_feedback_rejects_cross_project_content(self): + def test_content_create_uses_project_from_url(self): response = self.client.post( reverse( - "v1:project-feedback-list", + "ninja-api:list_contents", kwargs={"project_id": _require_pk(self.owner_project)}, ), { - "content": _require_pk(self.other_content), - "feedback_type": FeedbackType.DOWNVOTE, + "url": "https://example.com/new", + "title": "New Content", + "author": "Owner Author", + "entity": _require_pk(self.owner_entity), + "source_plugin": "rss", + "published_date": "2026-04-22T00:00:00Z", + "content_text": "Nested content text", + "project": _require_pk(self.other_project), }, - format="json", + content_type="application/json", ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "content") + self.assertEqual(response.status_code, HTTPStatus.CREATED) + created_content = Content.objects.get(title="New Content") + self.assertEqual(created_content.project, self.owner_project) - def test_content_create_uses_project_from_url(self): + def test_content_create_rejects_cross_project_entity(self): response = self.client.post( reverse( - "v1:project-content-list", + "ninja-api:list_contents", kwargs={"project_id": _require_pk(self.owner_project)}, ), { - "url": "https://example.com/new", - "title": "New Content", + "url": "https://example.com/new-cross-project-entity", + "title": "Cross Project Entity", "author": "Owner Author", - "entity": _require_pk(self.owner_entity), + "entity": _require_pk(self.other_entity), "source_plugin": "rss", "published_date": "2026-04-22T00:00:00Z", "content_text": "Nested content text", - "project": _require_pk(self.other_project), }, - format="json", + content_type="application/json", ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - created_content = Content.objects.get(title="New Content") - self.assertEqual(created_content.project, self.owner_project) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["entity"][0], + "Entity must belong to the selected project.", + ) def test_content_detail_includes_summary_text(self): self.owner_content.summary_text = "A concise summary ready for editors." @@ -171,35 +164,34 @@ def test_content_detail_includes_summary_text(self): response = self.client.get( reverse( - "v1:project-content-detail", + "ninja-api:get_content", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_content), + "content_id": _require_pk(self.owner_content), }, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual( response.json()["summary_text"], "A concise summary ready for editors." ) - @patch("core.tasks.run_relevance_scoring_skill.delay") + @patch("core.tasks.run_relevance_scoring_skill.kiq", new_callable=AsyncMock) def test_content_skill_action_queues_relevance_scoring( - self, run_relevance_scoring_delay_mock + self, run_relevance_scoring_queue_mock ): response = self.client.post( f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/relevance_scoring/", - format="json", ) - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) pending_result = SkillResult.objects.get( content=self.owner_content, skill_name="relevance_scoring", superseded_by__isnull=True, ) - run_relevance_scoring_delay_mock.assert_called_once_with( + run_relevance_scoring_queue_mock.assert_awaited_once_with( _require_pk(pending_result) ) self.owner_content.refresh_from_db() @@ -207,30 +199,6 @@ def test_content_skill_action_queues_relevance_scoring( self.assertEqual(response.json()["skill_name"], "relevance_scoring") self.assertEqual(response.json()["status"], SkillStatus.PENDING) - @patch("core.tasks.run_summarization_skill.delay") - def test_content_skill_action_queues_summarization( - self, run_summarization_delay_mock - ): - self.owner_content.relevance_score = 0.25 - self.owner_content.save(update_fields=["relevance_score"]) - - response = self.client.post( - f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/summarization/", - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) - pending_result = SkillResult.objects.get( - content=self.owner_content, - skill_name="summarization", - superseded_by__isnull=True, - ) - run_summarization_delay_mock.assert_called_once_with( - _require_pk(pending_result) - ) - self.assertEqual(response.json()["skill_name"], "summarization") - self.assertEqual(response.json()["status"], SkillStatus.PENDING) - @patch("core.pipeline.search_similar_content") def test_content_skill_action_runs_find_related(self, search_similar_content_mock): search_similar_content_mock.return_value = [ @@ -248,10 +216,9 @@ def test_content_skill_action_runs_find_related(self, search_similar_content_moc response = self.client.post( f"/api/v1/projects/{_require_pk(self.owner_project)}/contents/{_require_pk(self.owner_content)}/skills/find_related/", - format="json", ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.status_code, HTTPStatus.CREATED) self.assertEqual(response.json()["skill_name"], "find_related") self.assertEqual(response.json()["status"], SkillStatus.COMPLETED) self.assertEqual( @@ -259,89 +226,94 @@ def test_content_skill_action_runs_find_related(self, search_similar_content_moc _require_pk(self.other_content), ) - def test_content_detail_includes_newsletter_promotion_state(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=1, - dominant_entity=self.owner_entity, + @patch("content.signals.queue_topic_centroid_recompute") + def test_feedback_create_assigns_current_user(self, queue_centroid_mock): + response = self.client.post( + reverse( + "ninja-api:list_feedback", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.owner_content), + "feedback_type": FeedbackType.UPVOTE, + }, + content_type="application/json", ) - suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Promoted Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.9, - novelty_score=0.8, - status=ThemeSuggestionStatus.ACCEPTED, - decided_by=self.owner, + + self.assertEqual(response.status_code, HTTPStatus.CREATED) + feedback = UserFeedback.objects.get() + self.assertEqual(feedback.user, self.owner) + self.assertEqual(feedback.feedback_type, FeedbackType.UPVOTE) + queue_centroid_mock.assert_called_once_with(_require_pk(self.owner_project)) + + def test_feedback_rejects_cross_project_content(self): + response = self.client.post( + reverse( + "ninja-api:list_feedback", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.other_content), + "feedback_type": FeedbackType.DOWNVOTE, + }, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["content"][0], + "Content must belong to the selected project.", ) - self.owner_content.newsletter_promotion_theme = suggestion - self.owner_content.newsletter_promotion_by = self.owner - self.owner_content.newsletter_promotion_at = "2026-04-24T00:00:00Z" - self.owner_content.save( - update_fields=[ - "newsletter_promotion_theme", - "newsletter_promotion_by", - "newsletter_promotion_at", - ] + + @patch("content.signals.queue_topic_centroid_recompute") + def test_feedback_owner_can_update_own_feedback(self, queue_centroid_mock): + feedback = UserFeedback.objects.create( + content=self.owner_content, + project=self.owner_project, + user=self.member, + feedback_type=FeedbackType.UPVOTE, ) + queue_centroid_mock.reset_mock() + self.client.force_login(self.member) - response = self.client.get( + response = self.client.patch( reverse( - "v1:project-content-detail", + "ninja-api:update_feedback", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_content), + "feedback_id": _require_pk(feedback), }, - ) + ), + {"feedback_type": FeedbackType.DOWNVOTE}, + content_type="application/json", ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual( - response.json()["newsletter_promotion_theme"], - _require_pk(suggestion), - ) - self.assertEqual( - response.json()["newsletter_promotion_by"], - _require_pk(self.owner), - ) - self.assertIsNotNone(response.json()["newsletter_promotion_at"]) - - def test_content_detail_includes_duplicate_state(self): - canonical = self.owner_content - canonical.canonical_url = "https://example.com/owner" - canonical.duplicate_signal_count = 1 - canonical.save(update_fields=["canonical_url", "duplicate_signal_count"]) - duplicate = Content.objects.create( + self.assertEqual(response.status_code, HTTPStatus.OK) + feedback.refresh_from_db() + self.assertEqual(feedback.feedback_type, FeedbackType.DOWNVOTE) + queue_centroid_mock.assert_called_once_with(_require_pk(self.owner_project)) + + @patch("content.signals.queue_topic_centroid_recompute") + def test_feedback_member_cannot_delete_another_users_feedback( + self, queue_centroid_mock + ): + feedback = UserFeedback.objects.create( + content=self.owner_content, project=self.owner_project, - url="https://example.com/owner?utm_source=reddit", - canonical_url="https://example.com/owner", - title="Duplicate Owner Content", - author="Owner Author", - entity=self.owner_entity, - source_plugin="reddit", - published_date="2026-04-22T00:00:00Z", - content_text="Duplicate content text", - duplicate_of=canonical, - is_active=False, + user=self.owner, + feedback_type=FeedbackType.UPVOTE, ) + queue_centroid_mock.reset_mock() + self.client.force_login(self.member) - response = self.client.get( + response = self.client.delete( reverse( - "v1:project-content-detail", + "ninja-api:delete_feedback", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(duplicate), + "feedback_id": _require_pk(feedback), }, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["canonical_url"], "https://example.com/owner") - self.assertEqual(response.json()["duplicate_of"], _require_pk(canonical)) - self.assertEqual(response.json()["duplicate_signal_count"], 0) + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) diff --git a/core/api.py b/core/api.py deleted file mode 100644 index 3e9ad698..00000000 --- a/core/api.py +++ /dev/null @@ -1,671 +0,0 @@ -"""REST API viewsets and OpenAPI documentation helpers for the core app. - -This module exposes the project-scoped CRUD surface used by the frontend and by -external clients. It also centralizes the drf-spectacular helpers that keep the -generated schema consistent across similar viewsets. -""" - -import logging -from typing import Any, cast - -from drf_spectacular.utils import ( - OpenApiExample, - OpenApiParameter, - OpenApiResponse, - extend_schema, - extend_schema_view, - inline_serializer, -) -from rest_framework import serializers -from rest_framework.exceptions import NotFound - -from core.permissions import ( - get_visible_projects_queryset, -) -from projects.models import Project - -logger = logging.getLogger(__name__) - -PROJECT_ID_PARAMETER = OpenApiParameter( - name="project_id", - type=int, - location=OpenApiParameter.PATH, - description="The unique ID of the project that owns this nested resource.", -) - -SKILL_NAME_PARAMETER = OpenApiParameter( - name="skill_name", - type=str, - location=OpenApiParameter.PATH, - description=( - "The skill to run for this content item. Supported values: " - "content_classification, relevance_scoring, summarization, find_related." - ), -) - -PROJECT_CREATE_REQUEST_EXAMPLE = OpenApiExample( - "Create Project Request", - value={ - "name": "AI Weekly", - "topic_description": "Coverage of developer tools, model releases, and applied AI workflows.", - "content_retention_days": 180, - }, - request_only=True, -) - -PROJECT_RESPONSE_EXAMPLE = OpenApiExample( - "Project Response", - value={ - "id": 1, - "name": "AI Weekly", - "topic_description": "Coverage of developer tools, model releases, and applied AI workflows.", - "content_retention_days": 180, - "intake_token": "project-token-123", - "intake_enabled": True, - "user_role": "admin", - "has_bluesky_credentials": True, - "bluesky_handle": "aiweekly.bsky.social", - "bluesky_is_active": True, - "bluesky_last_verified_at": "2026-04-26T13:00:00Z", - "bluesky_last_error": "", - "created_at": "2026-04-26T12:00:00Z", - }, - response_only=True, -) - -BLUESKY_CREDENTIALS_RESPONSE_EXAMPLE = OpenApiExample( - "Bluesky Credentials Response", - value={ - "id": 1, - "project": 1, - "handle": "aiweekly.bsky.social", - "pds_url": "", - "is_active": True, - "has_stored_credential": True, - "last_verified_at": "2026-04-26T13:00:00Z", - "last_error": "", - "created_at": "2026-04-26T12:30:00Z", - "updated_at": "2026-04-26T13:00:00Z", - }, - response_only=True, -) - -MASTODON_CREDENTIALS_RESPONSE_EXAMPLE = OpenApiExample( - "Mastodon Credentials Response", - value={ - "id": 1, - "project": 1, - "instance_url": "https://hachyderm.io", - "account_acct": "alice@hachyderm.io", - "is_active": True, - "has_stored_credential": True, - "last_verified_at": "2026-04-26T13:00:00Z", - "last_error": "", - "created_at": "2026-04-26T12:30:00Z", - "updated_at": "2026-04-26T13:00:00Z", - }, - response_only=True, -) - -LINKEDIN_CREDENTIALS_RESPONSE_EXAMPLE = OpenApiExample( - "LinkedIn Credentials Response", - value={ - "id": 1, - "project": 1, - "member_urn": "urn:li:person:abc123", - "expires_at": "2026-04-27T13:00:00Z", - "is_active": True, - "has_stored_credential": True, - "last_verified_at": "2026-04-26T13:00:00Z", - "last_error": "", - "created_at": "2026-04-26T12:30:00Z", - "updated_at": "2026-04-26T13:00:00Z", - }, - response_only=True, -) - -LINKEDIN_OAUTH_AUTHORIZE_RESPONSE_EXAMPLE = OpenApiExample( - "LinkedIn OAuth Authorize Response", - value={ - "authorize_url": "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=linkedin-client-id&redirect_uri=http%3A%2F%2F127.0.0.1%3A8080%2Fapi%2Fv1%2Flinkedin%2Foauth%2Fcallback%2F&scope=openid+profile+email+offline_access&state=signed-state-token", - }, - response_only=True, -) - -SOURCE_CONFIG_CREATE_REQUEST_EXAMPLE = OpenApiExample( - "Create RSS Source Request", - value={ - "plugin_name": "rss", - "config": { - "feed_url": "https://example.com/feed.xml", - }, - "is_active": True, - }, - request_only=True, -) - -SOURCE_CONFIG_REDDIT_REQUEST_EXAMPLE = OpenApiExample( - "Create Reddit Source Request", - value={ - "plugin_name": "reddit", - "config": { - "subreddit": "MachineLearning", - "listing": "both", - "limit": 25, - }, - "is_active": True, - }, - request_only=True, -) - -SOURCE_CONFIG_BLUESKY_REQUEST_EXAMPLE = OpenApiExample( - "Create Bluesky Source Request", - value={ - "plugin_name": "bluesky", - "config": { - "author_handle": "alice.bsky.social", - "include_replies": False, - "max_posts_per_fetch": 100, - }, - "is_active": True, - }, - request_only=True, -) - -SOURCE_CONFIG_MASTODON_REQUEST_EXAMPLE = OpenApiExample( - "Create Mastodon Source Request", - value={ - "plugin_name": "mastodon", - "config": { - "instance_url": "https://hachyderm.io", - "hashtag": "platformengineering", - "include_replies": False, - "include_reblogs": True, - "max_statuses_per_fetch": 100, - }, - "is_active": True, - }, - request_only=True, -) - -SOURCE_CONFIG_LINKEDIN_REQUEST_EXAMPLE = OpenApiExample( - "Create LinkedIn Source Request", - value={ - "plugin_name": "linkedin", - "config": { - "organization_urn": "urn:li:organization:1337", - "include_reshares": False, - "max_posts_per_fetch": 50, - }, - "is_active": True, - }, - request_only=True, -) - -SOURCE_CONFIG_RESPONSE_EXAMPLE = OpenApiExample( - "Source Configuration Response", - value={ - "id": 12, - "project": 1, - "plugin_name": "rss", - "config": { - "feed_url": "https://example.com/feed.xml", - }, - "is_active": True, - "last_fetched_at": "2026-04-26T12:30:00Z", - }, - response_only=True, -) - -CONTENT_CREATE_REQUEST_EXAMPLE = OpenApiExample( - "Create Content Request", - value={ - "url": "https://example.com/posts/agent-memory-patterns", - "title": "Practical Agent Memory Patterns", - "author": "Jane Doe", - "entity": 4, - "source_plugin": "rss", - "content_type": "article", - "published_date": "2026-04-25T14:00:00Z", - "content_text": "A walkthrough of short-term and long-term memory patterns for production agents.", - "relevance_score": 0.92, - "is_reference": False, - "is_active": True, - }, - request_only=True, -) - -CONTENT_RESPONSE_EXAMPLE = OpenApiExample( - "Content Response", - value={ - "id": 44, - "project": 1, - "url": "https://example.com/posts/agent-memory-patterns", - "title": "Practical Agent Memory Patterns", - "author": "Jane Doe", - "entity": 4, - "source_plugin": "rss", - "content_type": "article", - "canonical_url": "https://example.com/posts/agent-memory-patterns", - "published_date": "2026-04-25T14:00:00Z", - "ingested_at": "2026-04-26T12:05:00Z", - "content_text": "A walkthrough of short-term and long-term memory patterns for production agents.", - "summary_text": "The piece explains practical patterns for combining short-term context with durable memory in production agents.", - "relevance_score": 0.92, - "authority_adjusted_score": 0.95, - "embedding_id": "emb_01jabcxyz", - "duplicate_of": None, - "duplicate_signal_count": 2, - "is_reference": False, - "is_active": True, - }, - response_only=True, -) - -SKILL_RESULT_RESPONSE_EXAMPLE = OpenApiExample( - "Skill Result Response", - value={ - "id": 91, - "content": 44, - "project": 1, - "skill_name": "relevance_classifier", - "status": "completed", - "result_data": { - "label": "high_relevance", - "reasoning": "The article directly covers agent memory design patterns.", - }, - "error_message": "", - "model_used": "gpt-4.1-mini", - "latency_ms": 842, - "confidence": 0.97, - "created_at": "2026-04-26T12:06:00Z", - "superseded_by": None, - }, - response_only=True, -) - -AUTHENTICATION_REQUIRED_EXAMPLE = OpenApiExample( - "Authentication Required", - value={ - "type": "client_error", - "errors": [ - { - "code": "not_authenticated", - "detail": "Authentication credentials were not provided.", - "attr": None, - } - ], - }, - response_only=True, - status_codes=["403"], -) - -AUTHENTICATION_REQUIRED_RESPONSE = OpenApiResponse( - response=inline_serializer( - name="AuthenticationRequiredResponse", - fields={ - "type": serializers.CharField(), - "errors": inline_serializer( - name="AuthenticationRequiredError", - fields={ - "code": serializers.CharField(), - "detail": serializers.CharField(), - "attr": serializers.CharField(allow_null=True), - }, - many=True, - ), - }, - ), - description="Authentication credentials are required to access this endpoint.", - examples=[AUTHENTICATION_REQUIRED_EXAMPLE], -) - -BLUESKY_CREDENTIALS_VERIFY_RESPONSE = inline_serializer( - name="BlueskyCredentialsVerifyResponse", - fields={ - "status": serializers.CharField(), - "handle": serializers.CharField(), - "last_verified_at": serializers.DateTimeField(allow_null=True), - "last_error": serializers.CharField(allow_blank=True), - }, -) - -MASTODON_CREDENTIALS_VERIFY_RESPONSE = inline_serializer( - name="MastodonCredentialsVerifyResponse", - fields={ - "status": serializers.CharField(), - "account_acct": serializers.CharField(allow_blank=True), - "instance_url": serializers.URLField(), - "last_verified_at": serializers.DateTimeField(allow_null=True), - "last_error": serializers.CharField(allow_blank=True), - }, -) - -LINKEDIN_CREDENTIALS_VERIFY_RESPONSE = inline_serializer( - name="LinkedInCredentialsVerifyResponse", - fields={ - "status": serializers.CharField(), - "member_urn": serializers.CharField(allow_blank=True), - "expires_at": serializers.DateTimeField(allow_null=True), - "last_verified_at": serializers.DateTimeField(allow_null=True), - "last_error": serializers.CharField(allow_blank=True), - }, -) - -LINKEDIN_OAUTH_AUTHORIZE_RESPONSE = inline_serializer( - name="LinkedInOAuthAuthorizeResponse", - fields={ - "authorize_url": serializers.URLField(), - }, -) - - -def build_success_response( - response, description: str, examples: list[OpenApiExample] | None = None -): - """Build a reusable OpenAPI success response object. - - Args: - response: Serializer, inline serializer, or response object for the schema. - description: Human-readable description shown in the generated docs. - examples: Optional example payloads to attach to the response. - - Returns: - A configured ``OpenApiResponse`` instance. - """ - - response_kwargs = { - "response": response, - "description": description, - } - if examples is not None: - response_kwargs["examples"] = examples - return OpenApiResponse(**response_kwargs) - - -def build_crud_action_overrides( - serializer_class, - resource_plural: str, - resource_singular: str, - *, - list_examples: list[OpenApiExample] | None = None, - retrieve_examples: list[OpenApiExample] | None = None, - create_examples: list[OpenApiExample] | None = None, - create_response_examples: list[OpenApiExample] | None = None, -): - """Generate common schema overrides for CRUD-style viewset actions. - - Args: - serializer_class: Serializer used by the viewset. - resource_plural: Human-readable plural name for the resource. - resource_singular: Human-readable singular name for the resource. - list_examples: Optional examples for list responses. - retrieve_examples: Optional examples for retrieve responses. - create_examples: Optional request examples for create actions. - create_response_examples: Optional examples for create responses. - - Returns: - A mapping suitable for ``action_overrides`` on the documentation helpers - below. - """ - - overrides: dict[str, dict[str, Any]] = { - "list": { - "responses": { - 200: build_success_response( - serializer_class(many=True), - f"A list of {resource_plural}.", - examples=list_examples if list_examples is not None else [], - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - "retrieve": { - "responses": { - 200: build_success_response( - serializer_class, - f"The requested {resource_singular}.", - examples=retrieve_examples, - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - "create": { - "responses": { - 201: build_success_response( - serializer_class, - f"The newly created {resource_singular}.", - examples=create_response_examples, - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - "update": { - "responses": { - 200: build_success_response( - serializer_class, f"The updated {resource_singular}." - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - "partial_update": { - "responses": { - 200: build_success_response( - serializer_class, f"The updated {resource_singular}." - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - "destroy": { - "responses": { - 204: OpenApiResponse( - description=f"The {resource_singular} was deleted." - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - } - }, - } - if create_examples: - overrides["create"]["examples"] = create_examples - return overrides - - -def document_group_access_viewset( - resource_plural: str, - resource_singular: str, - create_description: str, - tag: str, - action_overrides: dict[str, dict] | None = None, -): - """Decorate a viewset with schema metadata for membership-scoped resources. - - Args: - resource_plural: Human-readable plural label for the resource. - resource_singular: Human-readable singular label for the resource. - create_description: Detailed description for the create action. - tag: OpenAPI tag applied to each action. - action_overrides: Optional per-action schema overrides. - - Returns: - A class decorator produced by ``extend_schema_view``. - """ - - action_overrides = action_overrides or {} - - def schema(action: str, **kwargs): - schema_kwargs = {"tags": [tag], **kwargs} - action_override = action_overrides.get(action, {}) - override_responses = action_override.get("responses", {}) - if override_responses: - responses = dict(schema_kwargs.get("responses", {})) - responses.update(override_responses) - schema_kwargs["responses"] = responses - schema_kwargs.update( - {key: value for key, value in action_override.items() if key != "responses"} - ) - return extend_schema(**schema_kwargs) - - return extend_schema_view( - list=schema( - "list", - summary=f"List {resource_plural}", - description=f"Return all {resource_plural} available to the authenticated user through project membership.", - ), - retrieve=schema( - "retrieve", - summary=f"Get {resource_singular}", - description=f"Return a single {resource_singular} available to the authenticated user through project membership.", - ), - create=schema( - "create", - summary=f"Create {resource_singular}", - description=create_description, - ), - update=schema( - "update", - summary=f"Replace {resource_singular}", - description=f"Replace an existing {resource_singular} available to the authenticated user through project membership.", - ), - partial_update=schema( - "partial_update", - summary=f"Update {resource_singular}", - description=f"Update one or more fields on an existing {resource_singular} available to the authenticated user through project membership.", - ), - destroy=schema( - "destroy", - summary=f"Delete {resource_singular}", - description=f"Delete an existing {resource_singular} available to the authenticated user through project membership.", - ), - ) - - -def document_project_owned_viewset( - resource_plural: str, - resource_singular: str, - create_description: str, - tag: str, - action_overrides: dict[str, dict] | None = None, -): - """Decorate a nested project-scoped viewset with consistent schema metadata. - - Args: - resource_plural: Human-readable plural label for the resource. - resource_singular: Human-readable singular label for the resource. - create_description: Detailed description for the create action. - tag: OpenAPI tag applied to each action. - action_overrides: Optional per-action schema overrides. - - Returns: - A class decorator produced by ``extend_schema_view``. - """ - - parameters = [PROJECT_ID_PARAMETER] - action_overrides = action_overrides or {} - - def schema(action: str, **kwargs): - schema_kwargs = {"tags": [tag], **kwargs} - action_override = action_overrides.get(action, {}) - override_responses = action_override.get("responses", {}) - if override_responses: - responses = dict(schema_kwargs.get("responses", {})) - responses.update(override_responses) - schema_kwargs["responses"] = responses - schema_kwargs.update( - {key: value for key, value in action_override.items() if key != "responses"} - ) - return extend_schema(**schema_kwargs) - - return extend_schema_view( - list=schema( - "list", - summary=f"List {resource_plural}", - description=f"Return all {resource_plural} for the selected project.", - parameters=parameters, - ), - retrieve=schema( - "retrieve", - summary=f"Get {resource_singular}", - description=f"Return a single {resource_singular} for the selected project.", - parameters=parameters, - ), - create=schema( - "create", - summary=f"Create {resource_singular}", - description=create_description, - parameters=parameters, - ), - update=schema( - "update", - summary=f"Replace {resource_singular}", - description=f"Replace an existing {resource_singular} for the selected project.", - parameters=parameters, - ), - partial_update=schema( - "partial_update", - summary=f"Update {resource_singular}", - description=f"Update one or more fields on an existing {resource_singular} for the selected project.", - parameters=parameters, - ), - destroy=schema( - "destroy", - summary=f"Delete {resource_singular}", - description=f"Delete an existing {resource_singular} for the selected project.", - parameters=parameters, - ), - ) - - -class ProjectOwnedQuerysetMixin: - """Scope nested viewsets to the authenticated user's selected project.""" - - queryset: Any = None - - def _kwargs(self) -> dict[str, Any]: - """Return the DRF route kwargs for typed nested-project lookups.""" - - return cast(dict[str, Any], getattr(self, "kwargs")) - - def _request(self) -> Any: - """Return the DRF request object for typed access checks.""" - - return getattr(self, "request") - - def get_project(self): - """Return the project referenced by ``project_id`` after access checks. - - Raises: - AssertionError: If the nested route does not supply ``project_id``. - NotFound: If the project does not exist or the user lacks access. - """ - - project_id = self._kwargs().get("project_id") - if project_id is None: - raise AssertionError( - "project_id must be present in nested project-scoped routes" - ) - try: - return get_visible_projects_queryset(self._request().user).get( - pk=project_id - ) - except Project.DoesNotExist as exc: - raise NotFound("Project not found.") from exc - - def get_queryset(self): - """Filter the configured queryset down to the current project.""" - - queryset = self.queryset - if queryset is None: - raise AssertionError("queryset must be set on project-scoped viewsets") - return queryset.filter(project=self.get_project()) - - def get_serializer_context(self): - """Inject the resolved project into serializer context.""" - - context = cast(dict[str, Any], cast(Any, super()).get_serializer_context()) - context["project"] = self.get_project() - return context - - def perform_create(self, serializer): - """Ensure nested resources are always created under the current project.""" - - serializer.save(project=self.get_project()) diff --git a/core/api_urls.py b/core/api_urls.py deleted file mode 100644 index 9d8197ba..00000000 --- a/core/api_urls.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Aggregate app-owned API route registrations under the public v1 surface.""" - -from django.urls import path -from rest_framework.routers import DefaultRouter -from rest_framework_nested.routers import NestedSimpleRouter - -from content.api_urls import register_project_routes as register_content_project_routes -from entities.api_urls import ( - register_project_routes as register_entities_project_routes, -) -from ingestion.api_urls import ( - register_project_routes as register_ingestion_project_routes, -) -from messaging.api_urls import register_root_routes as register_messaging_root_routes -from newsletters.api_urls import ( - register_project_routes as register_newsletters_project_routes, -) -from notifications.api_urls import ( - register_root_routes as register_notifications_root_routes, -) -from pipeline.api_urls import ( - register_project_routes as register_pipeline_project_routes, -) -from projects.api_urls import ( - register_project_routes as register_projects_project_routes, -) -from projects.api_urls import ( - register_root_routes as register_projects_root_routes, -) -from projects.linkedin_oauth import linkedin_oauth_callback_view -from trends.api_urls import register_project_routes as register_trends_project_routes - -app_name = "api" - -router = DefaultRouter() -register_projects_root_routes(router) -register_notifications_root_routes(router) -register_messaging_root_routes(router) - -project_router = NestedSimpleRouter(router, r"projects", lookup="project") -register_projects_project_routes(project_router) -register_entities_project_routes(project_router) -register_content_project_routes(project_router) -register_pipeline_project_routes(project_router) -register_ingestion_project_routes(project_router) -register_newsletters_project_routes(project_router) -register_trends_project_routes(project_router) - -urlpatterns = [ - path( - "linkedin/oauth/callback/", - linkedin_oauth_callback_view, - name="linkedin-oauth-callback", - ), - *router.urls, - *project_router.urls, -] diff --git a/core/auth_views.py b/core/auth_views.py index bceacbd8..b735feb9 100644 --- a/core/auth_views.py +++ b/core/auth_views.py @@ -1,11 +1,130 @@ +"""Non-DRF social login views for GitHub and Google.""" + +from __future__ import annotations + +import json + +from allauth.account import app_settings as allauth_account_settings +from allauth.account.adapter import get_adapter as get_account_adapter +from allauth.socialaccount.helpers import complete_social_login from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from dj_rest_auth.registration.views import SocialLoginView -from rest_framework.permissions import AllowAny +from django.contrib.auth import get_user_model +from django.db import IntegrityError +from django.http import HttpRequest, HttpResponseBadRequest, JsonResponse +from django.utils.decorators import method_decorator +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from requests.exceptions import HTTPError + +from core.jwt import issue_auth_tokens + +SocialUserModel = get_user_model() + + +def _social_login_error(message: str, *, status_code: int = 400) -> JsonResponse: + """Return a normalized social-login error payload.""" + + return JsonResponse({"detail": message}, status=status_code) + + +def _social_login_response(user) -> JsonResponse: + """Return the frontend auth payload for one authenticated social user.""" + + tokens = issue_auth_tokens(user) + return JsonResponse( + { + "access": tokens["access"], + "refresh": tokens["refresh"], + "user": { + "id": user.id, + "username": user.username, + "email": user.email, + }, + } + ) + + +def _authenticate_social_access_token( + request: HttpRequest, + *, + adapter_class, + access_token: str, + id_token: str | None = None, +): + """Resolve one provider access token into an authenticated Django user.""" + + adapter = adapter_class(request) + app = adapter.get_provider().app + tokens_to_parse = {"access_token": access_token} + if id_token: + tokens_to_parse["id_token"] = id_token + social_token = adapter.parse_token(tokens_to_parse) + social_token.app = app -class BaseSocialLoginView(SocialLoginView): - permission_classes = [AllowAny] + response = {"id_token": id_token} if id_token else access_token + social_login = adapter.complete_login(request, app, social_token, response=response) + social_login.token = social_token + + result = complete_social_login(request, social_login) + if isinstance(result, HttpResponseBadRequest): + raise ValueError(result.content.decode("utf-8") or "Authentication failed.") + + if not social_login.is_existing: + email = getattr(social_login.user, "email", "") + if ( + allauth_account_settings.UNIQUE_EMAIL + and email + and SocialUserModel.objects.filter(email=email).exists() + ): + raise ValueError("User is already registered with this e-mail address.") + + social_login.lookup() + try: + social_login.save(request, connect=True) + except IntegrityError as exc: + raise ValueError( + "User is already registered with this e-mail address." + ) from exc + + return social_login.account.user + + +@method_decorator(csrf_exempt, name="dispatch") +class BaseSocialLoginView(View): + """Authenticate a provider access token and return JWT auth data.""" + + adapter_class = None + + def post(self, request: HttpRequest): + if self.adapter_class is None: + return _social_login_error( + "Social login is not configured.", status_code=500 + ) + + try: + payload = json.loads(request.body.decode("utf-8") or "{}") + except json.JSONDecodeError: + return _social_login_error("Request body must be valid JSON.") + + access_token = str(payload.get("access_token", "")).strip() + id_token = payload.get("id_token") + if not access_token: + return _social_login_error("access_token is required.") + + try: + user = _authenticate_social_access_token( + request, + adapter_class=self.adapter_class, + access_token=access_token, + id_token=str(id_token) if isinstance(id_token, str) else None, + ) + except (HTTPError, ValueError): + return _social_login_error("Provider rejected token.") + + get_account_adapter(request).login(request, user) + return _social_login_response(user) class GitHubLoginView(BaseSocialLoginView): @@ -14,3 +133,10 @@ class GitHubLoginView(BaseSocialLoginView): class GoogleLoginView(BaseSocialLoginView): adapter_class = GoogleOAuth2Adapter + + +__all__ = [ + "GitHubLoginView", + "GoogleLoginView", + "_authenticate_social_access_token", +] diff --git a/core/cron.py b/core/cron.py new file mode 100644 index 00000000..8f5ec541 --- /dev/null +++ b/core/cron.py @@ -0,0 +1,171 @@ +"""Shared cron parsing helpers for project-owned scheduling.""" + +from __future__ import annotations + +from datetime import datetime +from functools import lru_cache + +CronFields = tuple[set[int], set[int], set[int], set[int], set[int]] + + +def validate_cron_expression(cron_expression: str) -> str: + """Return a normalized 5-part cron expression or raise ``ValueError``.""" + + normalized = normalize_cron_expression(cron_expression) + parse_cron_expression(normalized) + return normalized + + +def normalize_cron_expression(cron_expression: str) -> str: + """Collapse whitespace and require exactly five cron fields.""" + + normalized = " ".join(str(cron_expression).split()) + if not normalized: + raise ValueError("Cron expression must not be empty.") + if len(normalized.split(" ")) != 5: + raise ValueError("Cron expression must contain exactly five fields.") + return normalized + + +def cron_matches_now(cron_expression: str, *, now: datetime) -> bool: + """Return whether one local timestamp satisfies a 5-part cron expression.""" + + minute_set, hour_set, day_set, month_set, weekday_set = parse_cron_expression( + cron_expression + ) + current = now + weekday = current.isoweekday() % 7 + return ( + current.minute in minute_set + and current.hour in hour_set + and current.day in day_set + and current.month in month_set + and weekday in weekday_set + ) + + +@lru_cache(maxsize=256) +def parse_cron_expression(cron_expression: str) -> CronFields: + """Parse one normalized 5-part cron expression into comparable field sets.""" + + normalized = normalize_cron_expression(cron_expression) + minute, hour, day_of_month, month_of_year, day_of_week = normalized.split(" ") + return ( + _parse_cron_field(minute, 0, 59), + _parse_cron_field(hour, 0, 23), + _parse_cron_field(day_of_month, 1, 31), + _parse_cron_field(month_of_year, 1, 12), + _parse_cron_field(day_of_week, 0, 7, sunday_alias=True), + ) + + +def _parse_cron_field( + field_expression: str, + minimum: int, + maximum: int, + *, + sunday_alias: bool = False, +) -> set[int]: + """Expand one cron field into its allowed integer values.""" + + values: set[int] = set() + for raw_segment in field_expression.split(","): + segment = raw_segment.strip() + if not segment: + raise ValueError("Cron field contains an empty segment.") + values.update( + _expand_cron_segment( + segment, + minimum, + maximum, + sunday_alias=sunday_alias, + ) + ) + if not values: + raise ValueError("Cron field must include at least one value.") + return values + + +def _expand_cron_segment( + segment: str, + minimum: int, + maximum: int, + *, + sunday_alias: bool, +) -> set[int]: + """Expand one cron segment, including optional step syntax.""" + + step = 1 + base = segment + if "/" in segment: + base, step_value = segment.split("/", 1) + step = int(step_value) + if step <= 0: + raise ValueError("Cron step must be greater than zero.") + + if base == "*": + start = minimum + end = maximum + elif "-" in base: + start_text, end_text = base.split("-", 1) + start = _parse_cron_value( + start_text, + minimum, + maximum, + sunday_alias=sunday_alias, + ) + end = _parse_cron_value( + end_text, + minimum, + maximum, + sunday_alias=sunday_alias, + ) + if start > end: + raise ValueError("Cron range start must not exceed its end.") + else: + value = _parse_cron_value( + base, + minimum, + maximum, + sunday_alias=sunday_alias, + ) + if "/" not in segment: + return {value} + start = value + end = maximum + + return { + _normalize_cron_value(value, sunday_alias=sunday_alias) + for value in range(start, end + 1, step) + } + + +def _parse_cron_value( + raw_value: str, + minimum: int, + maximum: int, + *, + sunday_alias: bool, +) -> int: + """Parse and range-check one cron integer value.""" + + value = int(raw_value) + if value < minimum or value > maximum: + raise ValueError(f"Cron value {value} must be between {minimum} and {maximum}.") + return _normalize_cron_value(value, sunday_alias=sunday_alias) + + +def _normalize_cron_value(value: int, *, sunday_alias: bool) -> int: + """Map weekday alias ``7`` onto Sunday when that alias is enabled.""" + + if sunday_alias and value == 7: + return 0 + return value + + +__all__ = [ + "cron_matches_now", + "normalize_cron_expression", + "parse_cron_expression", + "validate_cron_expression", +] diff --git a/core/jwt.py b/core/jwt.py new file mode 100644 index 00000000..9d5b41b0 --- /dev/null +++ b/core/jwt.py @@ -0,0 +1,77 @@ +"""Local JWT helpers for issuing and validating backend auth tokens.""" + +from __future__ import annotations + +from datetime import UTC, datetime, timedelta +from typing import Any + +import jwt +from django.conf import settings +from django.contrib.auth import get_user_model + +ACCESS_TOKEN_LIFETIME = timedelta(minutes=5) +REFRESH_TOKEN_LIFETIME = timedelta(days=1) +JWT_ALGORITHM = "HS256" + + +def _build_token(*, user_id: int, token_type: str, lifetime: timedelta) -> str: + """Return one signed JWT for the given user and token type.""" + + issued_at = datetime.now(UTC) + payload = { + "token_type": token_type, + "user_id": user_id, + "iat": issued_at, + "exp": issued_at + lifetime, + } + return str(jwt.encode(payload, settings.SECRET_KEY, algorithm=JWT_ALGORITHM)) + + +def issue_auth_tokens(user: Any) -> dict[str, str]: + """Return the access and refresh tokens for one authenticated user.""" + + user_id = int(user.pk) + return { + "access": _build_token( + user_id=user_id, + token_type="access", + lifetime=ACCESS_TOKEN_LIFETIME, + ), + "refresh": _build_token( + user_id=user_id, + token_type="refresh", + lifetime=REFRESH_TOKEN_LIFETIME, + ), + } + + +def authenticate_access_token( + raw_token: str, +) -> tuple[object | None, dict[str, Any] | None]: + """Resolve one bearer access token to a Django user and decoded payload.""" + + try: + payload = jwt.decode( + raw_token, + settings.SECRET_KEY, + algorithms=[JWT_ALGORITHM], + ) + except jwt.PyJWTError: + return None, None + + if payload.get("token_type") != "access": + return None, None + + user_id = payload.get("user_id") + if not isinstance(user_id, int): + return None, None + + user_model = get_user_model() + user = user_model.objects.filter(pk=user_id, is_active=True).first() + if user is None: + return None, None + + return user, payload + + +__all__ = ["authenticate_access_token", "issue_auth_tokens"] diff --git a/core/management/commands/bootstrap_live_sources.py b/core/management/commands/bootstrap_live_sources.py index a6847d98..5656d8e1 100644 --- a/core/management/commands/bootstrap_live_sources.py +++ b/core/management/commands/bootstrap_live_sources.py @@ -2,9 +2,9 @@ from __future__ import annotations -from django.conf import settings from django.core.management.base import BaseCommand, CommandError +from digest_engine.taskiq import enqueue_task, task_always_eager from ingestion.tasks import run_ingestion from projects.model_support import SourcePluginName from projects.models import Project, SourceConfig @@ -104,10 +104,10 @@ def handle(self, *args, **options): queued_count = 0 if options["run_now"]: for source_config_id in touched_source_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): run_ingestion(source_config_id) else: - run_ingestion.delay(source_config_id) + enqueue_task(run_ingestion, source_config_id) queued_count += 1 self.stdout.write( diff --git a/core/ninja_api.py b/core/ninja_api.py new file mode 100644 index 00000000..ee0eb211 --- /dev/null +++ b/core/ninja_api.py @@ -0,0 +1,90 @@ +"""Shared Django Ninja auth helpers that avoid DRF runtime coupling.""" + +from __future__ import annotations + +import base64 +import binascii + +from django.contrib.auth import authenticate +from django.http import HttpRequest + +from core.jwt import authenticate_access_token + + +def _session_user(request: HttpRequest) -> object | None: + """Return the authenticated Django session user, if present.""" + + user = getattr(request, "user", None) + if user is not None and getattr(user, "is_authenticated", False): + request.auth = getattr(request, "auth", None) + return user + return None + + +def _authorization_header(request: HttpRequest) -> str: + """Return the raw Authorization header value for one request.""" + + return request.META.get("HTTP_AUTHORIZATION", "").strip() + + +def _bearer_user(request: HttpRequest) -> object | None: + """Authenticate one request from a Bearer JWT authorization header.""" + + header = _authorization_header(request) + if not header: + return None + + scheme, _, raw_token = header.partition(" ") + if scheme.lower() != "bearer" or not raw_token: + return None + + user, payload = authenticate_access_token(raw_token) + if user is None or payload is None: + return None + + request.user = user + request.auth = payload + return user + + +def _basic_user(request: HttpRequest) -> object | None: + """Authenticate one request from an HTTP Basic authorization header.""" + + header = _authorization_header(request) + if not header: + return None + + scheme, _, value = header.partition(" ") + if scheme.lower() != "basic" or not value: + return None + + try: + decoded = base64.b64decode(value).decode("utf-8") + except (ValueError, binascii.Error, UnicodeDecodeError): + return None + + username, separator, password = decoded.partition(":") + if not separator: + return None + + user = authenticate(request, username=username, password=password) + if user is None: + return None + + request.user = user + request.auth = None + return user + + +def api_authenticate(request: HttpRequest) -> object | None: + """Authenticate one Ninja request with session, bearer, or basic auth.""" + + for resolver in (_session_user, _bearer_user, _basic_user): + user = resolver(request) + if user is None: + continue + return user + return None + + +__all__ = ["api_authenticate"] diff --git a/core/permissions.py b/core/permissions.py index f6f9c729..bb05d3e4 100644 --- a/core/permissions.py +++ b/core/permissions.py @@ -2,25 +2,19 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -from rest_framework import permissions - from projects.models import Project, ProjectMembership, ProjectRole -if TYPE_CHECKING: +SAFE_METHODS = frozenset({"GET", "HEAD", "OPTIONS"}) - class PermissionBase(permissions.BasePermission): - """Typed shim for DRF permissions whose default methods return bool.""" - def has_permission(self, request, view) -> bool: - raise NotImplementedError() +class PermissionBase: + """Minimal permission interface shared by DRF viewsets and Ninja helpers.""" - def has_object_permission(self, request, view, obj) -> bool: - raise NotImplementedError() + def has_permission(self, request, view) -> bool: + return True -else: - PermissionBase = permissions.BasePermission + def has_object_permission(self, request, view, obj) -> bool: + return True def get_visible_projects_queryset(user): @@ -117,13 +111,13 @@ def has_permission(self, request, view) -> bool: return True role = get_user_role(user, project) - if request.method in permissions.SAFE_METHODS: + if request.method in SAFE_METHODS: return role is not None return role in self.writable_roles def has_object_permission(self, request, view, obj) -> bool: role = get_user_role(request.user, _resolve_project(obj)) - if request.method in permissions.SAFE_METHODS: + if request.method in SAFE_METHODS: return role is not None return role in self.writable_roles @@ -161,13 +155,13 @@ def has_permission(self, request, view) -> bool: return True role = get_user_role(user, project) - if request.method in permissions.SAFE_METHODS: + if request.method in SAFE_METHODS: return role is not None return role in self.contributor_roles def has_object_permission(self, request, view, obj) -> bool: role = get_user_role(request.user, _resolve_project(obj)) - if request.method in permissions.SAFE_METHODS: + if request.method in SAFE_METHODS: return role is not None if role == ProjectRole.ADMIN: return True diff --git a/core/pipeline.py b/core/pipeline.py index ba3db59c..4035fd7e 100644 --- a/core/pipeline.py +++ b/core/pipeline.py @@ -1143,7 +1143,7 @@ def create_pending_skill_result(content: Content, skill_name: str) -> SkillResul skill_name: Supported async skill name. Returns: - A pending skill-result row that can be updated by a Celery worker. + A pending skill-result row that can be updated by a Taskiq worker. Raises: ValueError: If the skill cannot be executed asynchronously. diff --git a/core/serializer_mixins.py b/core/serializer_mixins.py deleted file mode 100644 index 7983a0c0..00000000 --- a/core/serializer_mixins.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Shared DRF serializer mixins used across app-owned serializer modules.""" - -from typing import Any, cast - -from rest_framework import serializers - -from content.models import Content -from core.permissions import get_visible_projects_queryset -from entities.models import Entity -from pipeline.models import SkillResult - - -class ProjectScopedSerializerMixin: - """Limit serializer relationship fields to objects the current user can access.""" - - def _serializer(self) -> serializers.Serializer: - """Return ``self`` as a DRF serializer for typed mixin access.""" - - return cast(serializers.Serializer, self) - - def _filter_related_queryset(self, request): - """Constrain related-field querysets using the request user and project context.""" - - serializer = self._serializer() - fields = cast(dict[str, Any], serializer.fields) - user = request.user - project = serializer.context.get("project") - if "project" in fields: - fields["project"].queryset = get_visible_projects_queryset(user) - if "entity" in fields: - entity_queryset = ( - Entity.objects.filter(project=project) - if project - else Entity.objects.filter(project__memberships__user=user).distinct() - ) - fields["entity"].queryset = entity_queryset - if "merged_into" in fields: - merged_into_queryset = ( - Entity.objects.filter(project=project) - if project - else Entity.objects.filter(project__memberships__user=user).distinct() - ) - fields["merged_into"].queryset = merged_into_queryset - if "content" in fields: - content_queryset = ( - Content.objects.filter(project=project) - if project - else Content.objects.filter(project__memberships__user=user).distinct() - ) - fields["content"].queryset = content_queryset - if "superseded_by" in fields: - skill_result_queryset = ( - SkillResult.objects.filter(project=project) - if project - else SkillResult.objects.filter( - project__memberships__user=user - ).distinct() - ) - fields["superseded_by"].queryset = skill_result_queryset - - def __init__(self, *args, **kwargs): - """Initialize the serializer and scope relation fields when authenticated.""" - - super().__init__(*args, **kwargs) - request = self._serializer().context.get("request") - if request and request.user.is_authenticated: - self._filter_related_queryset(request) diff --git a/core/settings_types.py b/core/settings_types.py index 8315e0e9..4fd79db5 100644 --- a/core/settings_types.py +++ b/core/settings_types.py @@ -4,7 +4,6 @@ class CoreSettings(Protocol): AI_TRACE_URL_TEMPLATE: str BLUESKY_CREDENTIALS_ENCRYPTION_KEY: str - CELERY_TASK_ALWAYS_EAGER: bool DEFAULT_FROM_EMAIL: str LINKEDIN_CLIENT_ID: str LINKEDIN_CLIENT_SECRET: str @@ -15,6 +14,7 @@ class CoreSettings(Protocol): NEWSLETTER_API_INTERNAL_URL: str NEWSLETTER_PUBLIC_URL: str QDRANT_URL: str + TASKIQ_ALWAYS_EAGER: bool EMBEDDING_MODEL: str EMBEDDING_PROVIDER: str EMBEDDING_TRUST_REMOTE_CODE: bool diff --git a/core/tasks.py b/core/tasks.py index fcce0037..1f5e93a2 100644 --- a/core/tasks.py +++ b/core/tasks.py @@ -1,12 +1,10 @@ -"""Celery tasks that drive ingestion, AI processing, and newsletter extraction.""" +"""Background tasks that drive ingestion, AI processing, and newsletter extraction.""" import logging import math from collections import defaultdict from datetime import timedelta -from typing import Protocol, cast -from celery import shared_task from django.conf import settings from django.db import transaction from django.db.models import Count, Model, Q @@ -24,6 +22,12 @@ from core.pipeline import ( retry_review_queue_item as retry_review_queue_item_from_pipeline, ) +from digest_engine.taskiq import ( + broker, + enqueue_task, + run_task_inline, + task_always_eager, +) from entities.models import ( Entity, EntityAuthoritySnapshot, @@ -65,22 +69,6 @@ ] -class DelayedTask(Protocol): - """Protocol for Celery tasks that can run eagerly or via ``delay``.""" - - def __call__(self, *args: object, **kwargs: object) -> object: - pass - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - -def _enqueue_task(task: object, *args: object) -> None: - """Dispatch a Celery task through a typed ``delay`` seam.""" - - cast(DelayedTask, task).delay(*args) - - def _require_pk(instance: Model) -> int: """Return a saved model primary key as an ``int``.""" @@ -98,7 +86,7 @@ def _retention_cutoff(days: int): return timezone.now() - timedelta(days=days) -@shared_task(name="core.tasks.run_all_authority_recomputations") +@broker.task(task_name="core.tasks.run_all_authority_recomputations") def run_all_authority_recomputations(): """Queue authority recomputation for every project. @@ -108,48 +96,48 @@ def run_all_authority_recomputations(): project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_authority_scores(project_id) else: - _enqueue_task(recompute_authority_scores, project_id) + enqueue_task(recompute_authority_scores, project_id) return len(project_ids) -@shared_task(name="core.tasks.run_all_source_quality_recomputations") +@broker.task(task_name="core.tasks.run_all_source_quality_recomputations") def run_all_source_quality_recomputations() -> int: """Queue source-quality recomputation for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_source_quality(project_id) else: - _enqueue_task(recompute_source_quality, project_id) + enqueue_task(recompute_source_quality, project_id) return len(project_ids) -@shared_task(name="core.tasks.run_all_retention_policies") +@broker.task(task_name="core.tasks.run_all_retention_policies") def run_all_retention_policies() -> int: """Queue retention-policy cleanup for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): apply_retention_policies(project_id) else: - _enqueue_task(apply_retention_policies, project_id) + enqueue_task(apply_retention_policies, project_id) return len(project_ids) -@shared_task(name="core.tasks.process_content") +@broker.task(task_name="core.tasks.process_content") def process_content(content_id: int): """Run the main AI pipeline for a stored content item.""" return process_content_pipeline(content_id) -@shared_task(name="core.tasks.circuit_breaker_health_check") +@broker.task(task_name="core.tasks.circuit_breaker_health_check") def circuit_breaker_health_check() -> dict[str, object]: """Probe opened circuit breakers and close the ones that recover.""" @@ -169,7 +157,7 @@ def circuit_breaker_health_check() -> dict[str, object]: } -@shared_task(name="core.tasks.retry_pipeline_review_item") +@broker.task(task_name="core.tasks.retry_pipeline_review_item") def retry_pipeline_review_item(review_item_id: int) -> dict[str, object]: """Retry one pipeline review item from its failed node.""" @@ -179,7 +167,7 @@ def retry_pipeline_review_item(review_item_id: int) -> dict[str, object]: return retry_review_queue_item_from_pipeline(review_item) -@shared_task(name="core.tasks.apply_retention_policies") +@broker.task(task_name="core.tasks.apply_retention_policies") def apply_retention_policies(project_id: int) -> dict[str, object]: """Delete old observability records for one project.""" @@ -231,7 +219,7 @@ def apply_retention_policies(project_id: int) -> dict[str, object]: } -@shared_task(name="core.tasks.recompute_source_quality") +@broker.task(task_name="core.tasks.recompute_source_quality") def recompute_source_quality(project_id: int) -> dict[str, object]: """Recompute source-quality scores used by entity authority scoring.""" @@ -268,7 +256,7 @@ def recompute_source_quality(project_id: int) -> dict[str, object]: } -@shared_task(name="core.tasks.recompute_authority_scores") +@broker.task(task_name="core.tasks.recompute_authority_scores") def recompute_authority_scores(project_id: int): """Rebuild authority scores and snapshots for one project's entities.""" @@ -502,14 +490,14 @@ def recompute_authority_scores(project_id: int): return {"project_id": project_id, "entities_updated": len(entity_updates)} -@shared_task(name="core.tasks.run_relevance_scoring_skill", ignore_result=True) +@broker.task(task_name="core.tasks.run_relevance_scoring_skill") def run_relevance_scoring_skill(skill_result_id: int): """Execute a pending ad hoc relevance skill result in the background.""" return execute_background_skill_result(skill_result_id, RELEVANCE_SKILL_NAME) -@shared_task(name="core.tasks.run_summarization_skill", ignore_result=True) +@broker.task(task_name="core.tasks.run_summarization_skill") def run_summarization_skill(skill_result_id: int): """Execute a pending ad hoc summarization skill result in the background.""" @@ -532,16 +520,16 @@ def queue_content_skill(content: Content, skill_name: str): if skill_name == RELEVANCE_SKILL_NAME: skill_result_pk = _require_pk(skill_result) - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): run_relevance_scoring_skill(skill_result_pk) else: - _enqueue_task(run_relevance_scoring_skill, skill_result_pk) + enqueue_task(run_relevance_scoring_skill, skill_result_pk) elif skill_name == SUMMARIZATION_SKILL_NAME: skill_result_pk = _require_pk(skill_result) - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): run_summarization_skill(skill_result_pk) else: - _enqueue_task(run_summarization_skill, skill_result_pk) + enqueue_task(run_summarization_skill, skill_result_pk) else: raise ValueError(f"Unsupported async skill name: {skill_name}") @@ -832,7 +820,7 @@ def _schedule_content_processing(content: Content) -> None: upsert_content_embedding(content) content_pk = _require_pk(content) - if settings.CELERY_TASK_ALWAYS_EAGER: - process_content(content_pk) + if task_always_eager(): + run_task_inline(process_content, content_pk) else: - _enqueue_task(process_content, content_pk) + enqueue_task(process_content, content_pk) diff --git a/core/tests/test_api.py b/core/tests/test_api.py deleted file mode 100644 index 58201423..00000000 --- a/core/tests/test_api.py +++ /dev/null @@ -1,357 +0,0 @@ -from typing import Any, cast -from unittest.mock import patch - -from django.contrib.auth import get_user_model -from django.db.models import Model -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from content.models import Content, FeedbackType, UserFeedback -from entities.models import Entity, EntityCandidate -from ingestion.models import IngestionRun, RunStatus -from newsletters.models import IntakeAllowlist, NewsletterIntake, NewsletterIntakeStatus -from pipeline.models import ReviewQueue, ReviewReason, SkillResult, SkillStatus -from projects.model_support import SourcePluginName -from projects.models import ( - BlueskyCredentials, - Project, - ProjectConfig, - ProjectMembership, - ProjectRole, - SourceConfig, -) -from trends.models import TopicCentroidSnapshot - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed API test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create a user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -class ProjectScopedApiTests(APITestCase): - def setUp(self): - user_model = get_user_model() - self.owner = _create_user(user_model, username="owner", password="testpass123") - self.other_user = _create_user( - user_model, username="other", password="testpass123" - ) - self.owner_project = Project.objects.create( - name="Owner Project", - topic_description="Platform engineering", - ) - self.other_project = Project.objects.create( - name="Other Project", - topic_description="Frontend", - ) - ProjectMembership.objects.create( - user=self.owner, - project=self.owner_project, - role=ProjectRole.ADMIN, - ) - ProjectMembership.objects.create( - user=self.other_user, - project=self.other_project, - role=ProjectRole.ADMIN, - ) - self.owner_entity = Entity.objects.create( - project=self.owner_project, - name="Owner Entity", - type="individual", - ) - self.other_entity = Entity.objects.create( - project=self.other_project, - name="Other Entity", - type="vendor", - ) - self.owner_content = Content.objects.create( - project=self.owner_project, - url="https://example.com/owner", - title="Owner Content", - author="Owner Author", - entity=self.owner_entity, - source_plugin="rss", - published_date="2026-04-21T00:00:00Z", - content_text="Owner content text", - ) - self.other_content = Content.objects.create( - project=self.other_project, - url="https://example.com/other", - title="Other Content", - author="Other Author", - entity=self.other_entity, - source_plugin="rss", - published_date="2026-04-21T00:00:00Z", - content_text="Other content text", - ) - self.owner_config = ProjectConfig.objects.create(project=self.owner_project) - self.owner_skill_result = SkillResult.objects.create( - project=self.owner_project, - content=self.owner_content, - skill_name="summarization", - status=SkillStatus.COMPLETED, - result_data={"summary": "Owner summary"}, - ) - self.owner_ingestion_run = IngestionRun.objects.create( - project=self.owner_project, - plugin_name="rss", - status=RunStatus.SUCCESS, - items_fetched=5, - items_ingested=4, - ) - self.owner_intake_allowlist = IntakeAllowlist.objects.create( - project=self.owner_project, - sender_email="sender@example.com", - ) - self.owner_newsletter_intake = NewsletterIntake.objects.create( - project=self.owner_project, - sender_email="sender@example.com", - subject="Owner Digest", - raw_text="See https://example.com/post", - message_id="owner-intake-1", - status=NewsletterIntakeStatus.EXTRACTED, - extraction_result={ - "method": "heuristic", - "items": [ - { - "url": "https://example.com/post", - "title": "Example Post", - "excerpt": "A short preview", - "position": 1, - } - ], - }, - ) - self.owner_review_queue = ReviewQueue.objects.create( - project=self.owner_project, - content=self.owner_content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.51, - ) - self.owner_source_config = SourceConfig.objects.create( - project=self.owner_project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ) - self.owner_topic_centroid_snapshot = TopicCentroidSnapshot.objects.create( - project=self.owner_project, - centroid_active=True, - centroid_vector=[1.0, 0.0], - feedback_count=10, - upvote_count=8, - downvote_count=2, - drift_from_previous=0.1, - drift_from_week_ago=0.2, - ) - _typed_client(self.client).force_authenticate(self.owner) - - def assert_standardized_validation_error(self, payload, attr): - self.assertEqual(payload["type"], "validation_error") - self.assertTrue(any(error["attr"] == attr for error in payload["errors"])) - - def test_authenticated_nested_list_endpoints_smoke(self): - list_endpoints = [ - reverse( - "v1:project-config-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-entity-candidate-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-content-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-skill-result-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-feedback-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-ingestion-run-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-mastodon-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-intake-allowlist-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-newsletter-intake-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-source-config-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-topic-centroid-snapshot-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - reverse( - "v1:project-review-queue-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - ] - - for endpoint in list_endpoints: - with self.subTest(endpoint=endpoint): - response = self.client.get(endpoint) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - @patch("content.signals.queue_topic_centroid_recompute") - def test_authenticated_nested_detail_endpoints_smoke(self, queue_centroid_mock): - detail_endpoints = [ - reverse( - "v1:project-config-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_config), - }, - ), - reverse( - "v1:project-entity-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), - }, - ), - reverse( - "v1:project-content-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_content), - }, - ), - reverse( - "v1:project-skill-result-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_skill_result), - }, - ), - reverse( - "v1:project-ingestion-run-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_ingestion_run), - }, - ), - reverse( - "v1:project-intake-allowlist-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_intake_allowlist), - }, - ), - reverse( - "v1:project-newsletter-intake-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_newsletter_intake), - }, - ), - reverse( - "v1:project-source-config-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_source_config), - }, - ), - reverse( - "v1:project-topic-centroid-snapshot-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_topic_centroid_snapshot), - }, - ), - reverse( - "v1:project-review-queue-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_review_queue), - }, - ), - ] - - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Smoke Candidate", - suggested_type="organization", - first_seen_in=self.owner_content, - ) - detail_endpoints.append( - reverse( - "v1:project-entity-candidate-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ) - ) - - feedback = UserFeedback.objects.create( - project=self.owner_project, - content=self.owner_content, - user=self.owner, - feedback_type=FeedbackType.UPVOTE, - ) - detail_endpoints.append( - reverse( - "v1:project-feedback-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(feedback), - }, - ) - ) - - credentials = BlueskyCredentials.objects.create( - project=self.owner_project, - handle="owner-project.bsky.social", - ) - detail_endpoints.append( - reverse( - "v1:project-bluesky-credentials-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(credentials), - }, - ) - ) - - for endpoint in detail_endpoints: - with self.subTest(endpoint=endpoint): - response = self.client.get(endpoint) - self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/core/tests/test_auth_views.py b/core/tests/test_auth_views.py new file mode 100644 index 00000000..f5a69864 --- /dev/null +++ b/core/tests/test_auth_views.py @@ -0,0 +1,79 @@ +"""Focused tests for the social login views.""" + +from __future__ import annotations + +import json +from unittest.mock import patch + +import pytest +from django.test import Client + +from users.models import AppUser + +pytestmark = pytest.mark.django_db + + +def test_github_login_view_returns_jwt_credentials_and_logs_the_user_in(): + client = Client() + user = AppUser.objects.create_user( + username="github-social-user", + email="github@example.com", + password="testpass123", + ) + + with patch( + "core.auth_views._authenticate_social_access_token", return_value=user + ) as auth_mock: + response = client.post( + "/api/auth/github/", + data=json.dumps({"access_token": "github-provider-token"}), + content_type="application/json", + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["access"] + assert payload["refresh"] + assert payload["user"]["username"] == user.username + assert client.session.get("_auth_user_id") == str(user.id) + auth_mock.assert_called_once() + assert auth_mock.call_args.kwargs["access_token"] == "github-provider-token" + + +def test_google_login_view_forwards_id_token_and_returns_jwt_credentials(): + client = Client() + user = AppUser.objects.create_user( + username="google-social-user", + email="google@example.com", + password="testpass123", + ) + + with patch( + "core.auth_views._authenticate_social_access_token", return_value=user + ) as auth_mock: + response = client.post( + "/api/auth/google/", + data=json.dumps( + {"access_token": "google-provider-token", "id_token": "google-id-token"} + ), + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json()["user"]["email"] == user.email + auth_mock.assert_called_once() + assert auth_mock.call_args.kwargs["access_token"] == "google-provider-token" + assert auth_mock.call_args.kwargs["id_token"] == "google-id-token" + + +def test_social_login_view_rejects_missing_access_token(): + client = Client() + + response = client.post( + "/api/auth/github/", + data=json.dumps({}), + content_type="application/json", + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "access_token is required." diff --git a/core/tests/test_bootstrap_live_sources.py b/core/tests/test_bootstrap_live_sources.py index 92692fdd..5794ea3b 100644 --- a/core/tests/test_bootstrap_live_sources.py +++ b/core/tests/test_bootstrap_live_sources.py @@ -1,4 +1,5 @@ from io import StringIO +from unittest.mock import AsyncMock import pytest from django.core.management import CommandError, call_command @@ -99,3 +100,24 @@ def test_bootstrap_live_sources_requires_one_project_selector(): call_command( "bootstrap_live_sources", rss_feed=["https://example.com/feed.xml"] ) + + +def test_bootstrap_live_sources_queues_ingestion_when_run_now(mocker): + project = Project.objects.create( + name="Bootstrap Project", + topic_description="Platform engineering", + ) + queue_mock = mocker.patch( + "ingestion.tasks.run_ingestion.kiq", + new_callable=AsyncMock, + ) + + call_command( + "bootstrap_live_sources", + project_id=project.id, + rss_feed=["https://example.com/feed.xml"], + run_now=True, + ) + + source_config = SourceConfig.objects.get(project=project) + queue_mock.assert_awaited_once_with(source_config.id) diff --git a/core/tests/test_cron.py b/core/tests/test_cron.py new file mode 100644 index 00000000..f4296fae --- /dev/null +++ b/core/tests/test_cron.py @@ -0,0 +1,36 @@ +from datetime import datetime + +import pytest +from django.utils import timezone + +from core.cron import cron_matches_now, validate_cron_expression + + +def test_validate_cron_expression_normalizes_ranges_lists_and_steps(): + assert validate_cron_expression(" 0,15-30/15 9 * * 1-5 ") == "0,15-30/15 9 * * 1-5" + + +@pytest.mark.parametrize( + "expression", + [ + "", + "* * * *", + "not a cron", + "61 9 * * *", + "0 24 * * *", + "0 9 * 13 *", + "0 9 * * 9", + "0 9 * * */0", + ], +) +def test_validate_cron_expression_rejects_invalid_values(expression): + with pytest.raises(ValueError): + validate_cron_expression(expression) + + +def test_cron_matches_now_accepts_sunday_alias(): + sunday_morning = timezone.make_aware(datetime(2026, 5, 24, 9, 17, 0)) + + assert cron_matches_now("17 9 * * 0", now=sunday_morning) + assert cron_matches_now("17 9 * * 7", now=sunday_morning) + assert not cron_matches_now("18 9 * * 7", now=sunday_morning) diff --git a/core/tests/test_entrypoints.py b/core/tests/test_entrypoints.py index 3f3959a4..72483b21 100644 --- a/core/tests/test_entrypoints.py +++ b/core/tests/test_entrypoints.py @@ -1,10 +1,12 @@ import importlib import os import sys +from urllib.parse import urlparse from channels.routing import ProtocolTypeRouter -from digest_engine.celery import app +from digest_engine.settings import RABBITMQ_URL, TASKIQ_RESULT_BACKEND_URL +from digest_engine.taskiq import broker, result_backend, scheduler def _import_fresh(module_name: str): @@ -45,22 +47,21 @@ def test_wsgi_module_sets_default_settings_and_builds_application(mocker): assert module.application == "wsgi-app" -def test_celery_app_redirects_worker_stdout_at_info_level(): - assert app.conf.worker_redirect_stdouts_level == "INFO" - - -def test_celery_app_schedules_source_quality_before_authority_recompute(): - beat_schedule = app.conf.beat_schedule +def test_taskiq_bootstrap_uses_rabbitmq_and_label_schedule_source(): + parsed_result_backend_url = urlparse(TASKIQ_RESULT_BACKEND_URL) + assert broker.url == RABBITMQ_URL assert ( - beat_schedule["run-all-source-quality-recomputations-nightly"]["task"] - == "core.tasks.run_all_source_quality_recomputations" + result_backend.redis_pool.connection_kwargs["host"] + == parsed_result_backend_url.hostname ) assert ( - beat_schedule["run-all-scheduled-newsletter-drafts-every-minute"]["task"] - == "core.tasks.run_all_scheduled_newsletter_drafts" + result_backend.redis_pool.connection_kwargs["port"] + == parsed_result_backend_url.port ) - assert ( - beat_schedule["run-all-authority-recomputations-nightly"]["task"] - == "core.tasks.run_all_authority_recomputations" + assert result_backend.redis_pool.connection_kwargs["db"] == int( + parsed_result_backend_url.path.removeprefix("/") or "0" ) + assert scheduler.broker is broker + assert len(scheduler.sources) == 1 + assert scheduler.sources[0].__class__.__name__ == "LabelScheduleSource" diff --git a/core/tests/test_permissions.py b/core/tests/test_permissions.py deleted file mode 100644 index 3b027163..00000000 --- a/core/tests/test_permissions.py +++ /dev/null @@ -1,418 +0,0 @@ -from typing import Any, cast -from unittest.mock import patch - -from django.contrib.auth import get_user_model -from django.db.models import Model -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from content.models import Content, FeedbackType, UserFeedback -from entities.models import Entity, EntityCandidate -from pipeline.models import ReviewQueue, ReviewReason -from projects.model_support import SourcePluginName -from projects.models import BlueskyCredentials, Project, ProjectMembership, ProjectRole -from trends.models import ThemeSuggestion, TopicCentroidSnapshot, TopicCluster - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed permission test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient methods.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create a user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -class ProjectRolePermissionTests(APITestCase): - def setUp(self): - queue_centroid_patcher = patch("content.signals.queue_topic_centroid_recompute") - queue_centroid_patcher.start() - self.addCleanup(queue_centroid_patcher.stop) - - user_model = get_user_model() - self.admin_user = _create_user( - user_model, - username="project-admin", - password="testpass123", - ) - self.member_user = _create_user( - user_model, - username="project-member", - password="testpass123", - ) - self.reader_user = _create_user( - user_model, - username="project-reader", - password="testpass123", - ) - self.outsider_user = _create_user( - user_model, - username="outsider", - password="testpass123", - ) - - self.project = Project.objects.create( - name="Permissions Project", - topic_description="Platform engineering", - ) - - ProjectMembership.objects.bulk_create( - [ - ProjectMembership( - user=self.admin_user, - project=self.project, - role=ProjectRole.ADMIN, - ), - ProjectMembership( - user=self.member_user, - project=self.project, - role=ProjectRole.MEMBER, - ), - ProjectMembership( - user=self.reader_user, - project=self.project, - role=ProjectRole.READER, - ), - ] - ) - - self.entity = Entity.objects.create( - project=self.project, - name="Permissions Entity", - type="vendor", - ) - self.content = Content.objects.create( - project=self.project, - url="https://example.com/permissions-content", - title="Permissions Content", - author="Author", - entity=self.entity, - source_plugin=SourcePluginName.RSS, - published_date="2026-04-29T00:00:00Z", - content_text="Permissions content body.", - ) - self.entity_candidate = EntityCandidate.objects.create( - project=self.project, - name="Candidate Vendor", - suggested_type="vendor", - first_seen_in=self.content, - ) - self.review_queue_item = ReviewQueue.objects.create( - project=self.project, - content=self.content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.55, - ) - self.topic_centroid_snapshot = TopicCentroidSnapshot.objects.create( - project=self.project, - centroid_active=True, - centroid_vector=[1.0, 0.0], - feedback_count=3, - upvote_count=2, - downvote_count=1, - drift_from_previous=0.1, - drift_from_week_ago=0.2, - ) - self.topic_cluster = TopicCluster.objects.create( - project=self.project, - first_seen_at="2026-04-28T00:00:00Z", - last_seen_at="2026-04-29T00:00:00Z", - is_active=True, - member_count=1, - dominant_entity=self.entity, - ) - self.theme_suggestion = ThemeSuggestion.objects.create( - project=self.project, - cluster=self.topic_cluster, - title="Permissions Theme", - pitch="Pitch", - why_it_matters="Why", - suggested_angle="Angle", - velocity_at_creation=0.8, - novelty_score=0.7, - ) - self.member_feedback = UserFeedback.objects.create( - project=self.project, - content=self.content, - user=self.member_user, - feedback_type=FeedbackType.UPVOTE, - ) - self.admin_feedback = UserFeedback.objects.create( - project=self.project, - content=self.content, - user=self.admin_user, - feedback_type=FeedbackType.DOWNVOTE, - ) - self.bluesky_credentials = BlueskyCredentials.objects.create( - project=self.project, - handle="permissions-project.bsky.social", - ) - - def test_project_list_includes_resolved_reader_role(self): - _typed_client(self.client).force_authenticate(self.reader_user) - - response = self.client.get(reverse("v1:project-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()[0]["user_role"], ProjectRole.READER) - - def test_reader_is_denied_contributor_and_admin_endpoints(self): - _typed_client(self.client).force_authenticate(self.reader_user) - - cases = [ - ( - "patch", - reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}), - {"name": "Reader Update"}, - ), - ( - "post", - reverse( - "v1:project-rotate-intake-token", - kwargs={"id": _require_pk(self.project)}, - ), - None, - ), - ( - "get", - reverse( - "v1:project-review-queue-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - None, - ), - ( - "post", - reverse( - "v1:project-source-config-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - { - "plugin_name": SourcePluginName.RSS, - "config": {"feed_url": "https://example.com/feed.xml"}, - "is_active": True, - }, - ), - ( - "get", - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - None, - ), - ( - "post", - reverse( - "v1:project-feedback-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - { - "content": _require_pk(self.content), - "feedback_type": FeedbackType.UPVOTE, - }, - ), - ( - "post", - reverse( - "v1:project-entity-candidate-accept", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.entity_candidate), - }, - ), - None, - ), - ( - "get", - reverse( - "v1:project-topic-centroid-snapshot-summary", - kwargs={"project_id": _require_pk(self.project)}, - ), - None, - ), - ( - "post", - reverse( - "v1:project-theme-suggestion-accept", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.theme_suggestion), - }, - ), - None, - ), - ] - - for method, url, payload in cases: - with self.subTest(method=method, url=url): - response = getattr(self.client, method)(url, payload, format="json") - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_member_can_use_contributor_endpoints_but_not_admin_only_ones(self): - with patch("entities.extraction.queue_entity_identity_enrichment"): - _typed_client(self.client).force_authenticate(self.member_user) - - review_queue_response = self.client.get( - reverse( - "v1:project-review-queue-list", - kwargs={"project_id": _require_pk(self.project)}, - ) - ) - self.assertEqual(review_queue_response.status_code, status.HTTP_200_OK) - - source_config_response = self.client.post( - reverse( - "v1:project-source-config-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - { - "plugin_name": SourcePluginName.RSS, - "config": {"feed_url": "https://example.com/feed.xml"}, - "is_active": True, - }, - format="json", - ) - self.assertEqual( - source_config_response.status_code, status.HTTP_201_CREATED - ) - - topic_summary_response = self.client.get( - reverse( - "v1:project-topic-centroid-snapshot-summary", - kwargs={"project_id": _require_pk(self.project)}, - ) - ) - self.assertEqual(topic_summary_response.status_code, status.HTTP_200_OK) - - accept_candidate_response = self.client.post( - reverse( - "v1:project-entity-candidate-accept", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.entity_candidate), - }, - ), - format="json", - ) - self.assertEqual(accept_candidate_response.status_code, status.HTTP_200_OK) - - delete_own_feedback_response = self.client.delete( - reverse( - "v1:project-feedback-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.member_feedback), - }, - ) - ) - self.assertEqual( - delete_own_feedback_response.status_code, status.HTTP_204_NO_CONTENT - ) - - update_project_response = self.client.patch( - reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}), - {"name": "Member Update"}, - format="json", - ) - self.assertEqual( - update_project_response.status_code, status.HTTP_403_FORBIDDEN - ) - - list_credentials_response = self.client.get( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.project)}, - ) - ) - self.assertEqual( - list_credentials_response.status_code, status.HTTP_403_FORBIDDEN - ) - - rotate_token_response = self.client.post( - reverse( - "v1:project-rotate-intake-token", - kwargs={"id": _require_pk(self.project)}, - ), - format="json", - ) - self.assertEqual( - rotate_token_response.status_code, status.HTTP_403_FORBIDDEN - ) - - def test_member_cannot_delete_other_users_feedback(self): - _typed_client(self.client).force_authenticate(self.member_user) - - response = self.client.delete( - reverse( - "v1:project-feedback-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.admin_feedback), - }, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_admin_can_access_admin_endpoints_and_delete_other_feedback(self): - _typed_client(self.client).force_authenticate(self.admin_user) - - update_project_response = self.client.patch( - reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}), - {"name": "Admin Updated Project"}, - format="json", - ) - self.assertEqual(update_project_response.status_code, status.HTTP_200_OK) - - list_credentials_response = self.client.get( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.project)}, - ) - ) - self.assertEqual(list_credentials_response.status_code, status.HTTP_200_OK) - - rotate_token_response = self.client.post( - reverse( - "v1:project-rotate-intake-token", - kwargs={"id": _require_pk(self.project)}, - ), - format="json", - ) - self.assertEqual(rotate_token_response.status_code, status.HTTP_200_OK) - - delete_feedback_response = self.client.delete( - reverse( - "v1:project-feedback-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.member_feedback), - }, - ) - ) - self.assertEqual( - delete_feedback_response.status_code, status.HTTP_204_NO_CONTENT - ) - - def test_outsider_cannot_access_project_resources(self): - _typed_client(self.client).force_authenticate(self.outsider_user) - - response = self.client.get( - reverse("v1:project-detail", kwargs={"id": _require_pk(self.project)}) - ) - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/core/tests/test_serializers.py b/core/tests/test_serializers.py deleted file mode 100644 index 13244f48..00000000 --- a/core/tests/test_serializers.py +++ /dev/null @@ -1,514 +0,0 @@ -from types import SimpleNamespace -from typing import Any, cast - -import pytest -from django.contrib.auth.models import AnonymousUser -from django.db.models import Model -from rest_framework import serializers as drf_serializers - -from content.models import Content -from content.serializers import ContentSerializer, UserFeedbackSerializer -from entities.models import Entity -from entities.serializers import EntitySerializer -from ingestion.serializers import IngestionRunSerializer -from pipeline.models import ReviewReason, SkillResult -from pipeline.serializers import ReviewQueueSerializer, SkillResultSerializer -from projects.model_support import SourcePluginName -from projects.models import ( - LinkedInCredentials, - MastodonCredentials, - Project, - ProjectMembership, - ProjectRole, - SourceConfig, -) -from projects.serializers import ( - LinkedInCredentialsSerializer, - MastodonCredentialsSerializer, - ProjectSerializer, - SourceConfigSerializer, -) - -pytestmark = pytest.mark.django_db - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed serializer assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _serializer_fields( - serializer: drf_serializers.BaseSerializer[Any], -) -> dict[str, Any]: - """Return serializer fields with a mapping type Pylance can index.""" - - return cast(dict[str, Any], cast(Any, serializer).fields) - - -def _serializer_data(serializer: drf_serializers.BaseSerializer[Any]) -> dict[str, Any]: - """Return serializer output data as a dictionary for typed assertions.""" - - return cast(dict[str, Any], serializer.data) - - -def _validated_data( - serializer: drf_serializers.BaseSerializer[Any], -) -> dict[str, Any]: - """Return validated serializer data as a standard dictionary.""" - - return cast(dict[str, Any], serializer.validated_data) - - -@pytest.fixture -def serializer_context(django_user_model): - user = django_user_model.objects.create_user( - username="serializer-owner", password="testpass123" - ) - other_user = django_user_model.objects.create_user( - username="serializer-other", password="testpass123" - ) - project = Project.objects.create( - name="Serializer Project", topic_description="Infra" - ) - other_project = Project.objects.create( - name="Other Serializer Project", topic_description="Data" - ) - ProjectMembership.objects.create(user=user, project=project, role=ProjectRole.ADMIN) - ProjectMembership.objects.create( - user=other_user, - project=other_project, - role=ProjectRole.ADMIN, - ) - entity = Entity.objects.create( - project=project, name="Serializer Entity", type="vendor" - ) - other_entity = Entity.objects.create( - project=other_project, name="Other Entity", type="vendor" - ) - content = Content.objects.create( - project=project, - url="https://example.com/serializer-content", - title="Serializer Content", - author="Author", - entity=entity, - source_plugin=SourcePluginName.RSS, - published_date="2026-04-28T00:00:00Z", - content_text="Serializer content body.", - ) - other_content = Content.objects.create( - project=other_project, - url="https://example.com/serializer-other-content", - title="Other Content", - author="Author", - entity=other_entity, - source_plugin=SourcePluginName.RSS, - published_date="2026-04-28T00:00:00Z", - content_text="Other serializer content body.", - ) - skill_result = SkillResult.objects.create( - project=project, - content=content, - skill_name="summarization", - status="completed", - result_data={"summary": "ok"}, - ) - other_skill_result = SkillResult.objects.create( - project=other_project, - content=other_content, - skill_name="summarization", - status="completed", - result_data={"summary": "other"}, - ) - return SimpleNamespace( - user=user, - other_user=other_user, - project=project, - other_project=other_project, - entity=entity, - other_entity=other_entity, - content=content, - other_content=other_content, - skill_result=skill_result, - other_skill_result=other_skill_result, - ) - - -def _request_for(user): - return SimpleNamespace(user=user) - - -def test_project_scoped_serializer_filters_related_querysets_with_project_context( - serializer_context, -): - serializer = SkillResultSerializer( - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - } - ) - fields = _serializer_fields(serializer) - - assert list(fields["content"].queryset) == [serializer_context.content] - assert list(fields["superseded_by"].queryset) == [serializer_context.skill_result] - assert list(fields["project"].queryset) == [serializer_context.project] - - -def test_project_scoped_serializer_filters_related_querysets_without_project_context( - serializer_context, -): - serializer = ContentSerializer( - context={"request": _request_for(serializer_context.user)} - ) - fields = _serializer_fields(serializer) - - assert list(fields["entity"].queryset) == [serializer_context.entity] - assert list(fields["project"].queryset) == [serializer_context.project] - - -def test_project_scoped_serializer_skips_filtering_for_anonymous_user(): - serializer = ProjectSerializer(context={"request": _request_for(AnonymousUser())}) - - assert "project" not in _serializer_fields(serializer) - - -def test_content_serializer_rejects_cross_project_entity(serializer_context): - serializer = ContentSerializer( - instance=serializer_context.content, - data={"entity": _require_pk(serializer_context.other_entity)}, - partial=True, - context={"project": serializer_context.project}, - ) - - assert serializer.is_valid() is False - assert serializer.errors == { - "entity": ["Entity must belong to the selected project."] - } - - -def test_content_serializer_exposes_duplicate_state_as_read_only_fields( - serializer_context, -): - duplicate = Content.objects.create( - project=serializer_context.project, - url="https://example.com/serializer-content?utm_source=reddit", - canonical_url="https://example.com/serializer-content", - title="Serializer Duplicate", - author="Author", - entity=serializer_context.entity, - source_plugin=SourcePluginName.REDDIT, - published_date="2026-04-28T01:00:00Z", - content_text="Duplicate serializer content body.", - duplicate_of=serializer_context.content, - ) - serializer_context.content.duplicate_signal_count = 1 - serializer_context.content.canonical_url = "https://example.com/serializer-content" - serializer_context.content.save( - update_fields=["duplicate_signal_count", "canonical_url"] - ) - - serializer = ContentSerializer(instance=duplicate) - data = _serializer_data(serializer) - - assert data["canonical_url"] == "https://example.com/serializer-content" - assert data["duplicate_of"] == _require_pk(serializer_context.content) - assert data["duplicate_signal_count"] == 0 - - -def test_content_serializer_ignores_duplicate_fields_on_update(serializer_context): - serializer = ContentSerializer( - instance=serializer_context.content, - data={ - "canonical_url": "https://malicious.example/canonical", - "duplicate_of": _require_pk(serializer_context.other_content), - "duplicate_signal_count": 99, - }, - partial=True, - context={"project": serializer_context.project}, - ) - - assert serializer.is_valid(), serializer.errors - updated = cast(Content, serializer.save()) - - assert updated.canonical_url == "" - assert updated.duplicate_of is None - assert updated.duplicate_signal_count == 0 - - -def test_skill_result_serializer_rejects_cross_project_content(serializer_context): - serializer = SkillResultSerializer( - data={ - "content": _require_pk(serializer_context.other_content), - "skill_name": "summarization", - "status": "completed", - }, - context={ - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid() is False - assert serializer.errors == { - "content": ["Content must belong to the selected project."] - } - - -def test_review_queue_serializer_rejects_cross_project_content(serializer_context): - serializer = ReviewQueueSerializer( - data={ - "content": _require_pk(serializer_context.other_content), - "reason": ReviewReason.BORDERLINE_RELEVANCE, - "confidence": 0.5, - }, - context={ - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid() is False - assert serializer.errors == { - "content": ["Content must belong to the selected project."] - } - - -def test_source_config_serializer_normalizes_valid_config(serializer_context): - serializer = SourceConfigSerializer( - data={ - "plugin_name": SourcePluginName.RSS, - "config": {"feed_url": "https://example.com/feed.xml"}, - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - assert _validated_data(serializer)["config"] == { - "feed_url": "https://example.com/feed.xml" - } - - -def test_source_config_serializer_surfaces_plugin_validation_errors(serializer_context): - serializer = SourceConfigSerializer( - instance=SourceConfig( - project=serializer_context.project, - plugin_name=SourcePluginName.RSS, - config={"feed_url": "https://example.com/feed.xml"}, - ), - data={"config": {"feed_url": ""}}, - partial=True, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid() is False - assert serializer.errors == {"config": ["Invalid source configuration."]} - - -def test_source_config_serializer_normalizes_bluesky_author_handle_config( - serializer_context, -): - serializer = SourceConfigSerializer( - data={ - "plugin_name": SourcePluginName.BLUESKY, - "config": {"author_handle": "@Alice.BSKY.social"}, - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - assert _validated_data(serializer)["config"] == { - "author_handle": "alice.bsky.social", - "include_replies": False, - "max_posts_per_fetch": 100, - } - - -def test_source_config_serializer_normalizes_mastodon_hashtag_config( - serializer_context, -): - serializer = SourceConfigSerializer( - data={ - "plugin_name": SourcePluginName.MASTODON, - "config": { - "instance_url": "https://hachyderm.io/", - "hashtag": "#PlatformEngineering", - }, - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - assert _validated_data(serializer)["config"] == { - "instance_url": "https://hachyderm.io", - "hashtag": "platformengineering", - "include_replies": False, - "include_reblogs": True, - "max_statuses_per_fetch": 100, - } - - -def test_source_config_serializer_normalizes_linkedin_organization_config( - serializer_context, -): - serializer = SourceConfigSerializer( - data={ - "plugin_name": SourcePluginName.LINKEDIN, - "config": { - "organization_urn": "urn:li:organization:1337", - "max_posts_per_fetch": "25", - }, - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - assert _validated_data(serializer)["config"] == { - "organization_urn": "urn:li:organization:1337", - "include_reshares": False, - "max_posts_per_fetch": 25, - } - - -def test_mastodon_credentials_serializer_encrypts_access_token(serializer_context): - serializer = MastodonCredentialsSerializer( - data={ - "instance_url": "https://hachyderm.io/", - "account_acct": "@Alice", - "access_token": "secret-token", - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - credentials = cast( - MastodonCredentials, - serializer.save(project=serializer_context.project), - ) - - assert credentials.instance_url == "https://hachyderm.io" - assert credentials.account_acct == "alice@hachyderm.io" - assert credentials.has_stored_credential() is True - assert credentials.get_access_token() == "secret-token" - - -def test_linkedin_credentials_serializer_encrypts_oauth_tokens(serializer_context): - serializer = LinkedInCredentialsSerializer( - data={ - "member_urn": "urn:li:person:abc123", - "access_token": "access-token", - "refresh_token": "refresh-token", - "expires_at": "2026-04-27T13:00:00Z", - "is_active": True, - }, - context={ - "request": _request_for(serializer_context.user), - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - credentials = cast( - LinkedInCredentials, - serializer.save(project=serializer_context.project), - ) - - assert credentials.member_urn == "urn:li:person:abc123" - assert credentials.has_stored_credential() is True - assert credentials.get_access_token() == "access-token" - assert credentials.get_refresh_token() == "refresh-token" - - -def test_entity_serializer_filters_project_queryset_to_request_user(serializer_context): - serializer = EntitySerializer( - context={"request": _request_for(serializer_context.user)} - ) - - assert list(_serializer_fields(serializer)["project"].queryset) == [ - serializer_context.project - ] - - -def test_user_feedback_serializer_rejects_cross_project_content(serializer_context): - serializer = UserFeedbackSerializer( - data={ - "content": _require_pk(serializer_context.other_content), - "feedback_type": "upvote", - }, - context={ - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid() is False - assert serializer.errors == { - "content": ["Content must belong to the selected project."] - } - - -def test_review_queue_serializer_accepts_same_project_content(serializer_context): - serializer = ReviewQueueSerializer( - data={ - "content": _require_pk(serializer_context.content), - "reason": ReviewReason.BORDERLINE_RELEVANCE, - "confidence": 0.5, - }, - context={ - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - validated_data = cast(dict[str, Content | Any], serializer.validated_data) - assert validated_data["content"] == serializer_context.content - - -def test_source_config_serializer_skips_plugin_validation_when_plugin_name_missing( - serializer_context, -): - serializer = SourceConfigSerializer( - instance=SourceConfig( - project=serializer_context.project, plugin_name="", config={} - ), - data={"config": {}}, - partial=True, - context={ - "project": serializer_context.project, - }, - ) - - assert serializer.is_valid(), serializer.errors - assert _validated_data(serializer)["config"] == {} - - -def test_ingestion_run_serializer_filters_project_queryset(serializer_context): - serializer = IngestionRunSerializer( - context={"request": _request_for(serializer_context.user)} - ) - - assert list(_serializer_fields(serializer)["project"].queryset) == [ - serializer_context.project - ] diff --git a/core/tests/test_tasks.py b/core/tests/test_tasks.py index 0e283e64..a70c5b65 100644 --- a/core/tests/test_tasks.py +++ b/core/tests/test_tasks.py @@ -1,6 +1,8 @@ from datetime import datetime, timedelta from datetime import timezone as dt_timezone from types import SimpleNamespace +from typing import cast +from unittest.mock import AsyncMock import pytest from django.db.models import Model @@ -66,7 +68,10 @@ def source_plugin_context(django_user_model): def test_run_all_authority_recomputations_enqueues_all_projects( source_plugin_context, mocker ): - delay_mock = mocker.patch("core.tasks.recompute_authority_scores.delay") + queue_mock = mocker.patch( + "core.tasks.recompute_authority_scores.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Other Project", topic_description="Security", @@ -75,17 +80,20 @@ def test_run_all_authority_recomputations_enqueues_all_projects( enqueued_count = run_all_authority_recomputations() assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 def test_run_all_authority_recomputations_executes_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True recompute_mock = mocker.patch("core.tasks.recompute_authority_scores") - delay_mock = mocker.patch("core.tasks.recompute_authority_scores.delay") + queue_mock = mocker.patch( + "core.tasks.recompute_authority_scores.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Inline Project", topic_description="Platform", @@ -97,13 +105,16 @@ def test_run_all_authority_recomputations_executes_inline_when_eager( recompute_mock.assert_any_call(source_plugin_context.project.id) recompute_mock.assert_any_call(_require_pk(other_project)) assert recompute_mock.call_count == 2 - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_run_all_source_quality_recomputations_enqueues_all_projects( source_plugin_context, mocker ): - delay_mock = mocker.patch("core.tasks.recompute_source_quality.delay") + queue_mock = mocker.patch( + "core.tasks.recompute_source_quality.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Source Quality Project", topic_description="Signals", @@ -112,17 +123,20 @@ def test_run_all_source_quality_recomputations_enqueues_all_projects( enqueued_count = run_all_source_quality_recomputations() assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 def test_run_all_source_quality_recomputations_executes_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True recompute_mock = mocker.patch("core.tasks.recompute_source_quality") - delay_mock = mocker.patch("core.tasks.recompute_source_quality.delay") + queue_mock = mocker.patch( + "core.tasks.recompute_source_quality.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Inline Source Quality Project", topic_description="Signals", @@ -134,13 +148,16 @@ def test_run_all_source_quality_recomputations_executes_inline_when_eager( recompute_mock.assert_any_call(source_plugin_context.project.id) recompute_mock.assert_any_call(_require_pk(other_project)) assert recompute_mock.call_count == 2 - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_run_all_retention_policies_enqueues_all_projects( source_plugin_context, mocker ): - delay_mock = mocker.patch("core.tasks.apply_retention_policies.delay") + queue_mock = mocker.patch( + "core.tasks.apply_retention_policies.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Retention Project", topic_description="Ops", @@ -149,9 +166,9 @@ def test_run_all_retention_policies_enqueues_all_projects( enqueued_count = run_all_retention_policies() assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 def test_apply_retention_policies_deletes_old_observability_records( @@ -317,13 +334,14 @@ def test_apply_retention_policies_deletes_old_observability_records( ReviewQueue.objects.filter(pk=old_review_item.pk).update(resolved_at=old_timestamp) result = apply_retention_policies(_require_pk(project)) - - assert result["deleted"]["topic_centroid_snapshots"] == 1 - assert result["deleted"]["topic_velocity_snapshots"] == 1 - assert result["deleted"]["source_diversity_snapshots"] == 1 - assert result["deleted"]["entity_authority_snapshots"] == 1 - assert result["deleted"]["trend_task_runs"] == 1 - assert result["deleted"]["resolved_review_items"] == 1 + deleted_counts = cast(dict[str, int], result["deleted"]) + + assert deleted_counts["topic_centroid_snapshots"] == 1 + assert deleted_counts["topic_velocity_snapshots"] == 1 + assert deleted_counts["source_diversity_snapshots"] == 1 + assert deleted_counts["entity_authority_snapshots"] == 1 + assert deleted_counts["trend_task_runs"] == 1 + assert deleted_counts["resolved_review_items"] == 1 assert TopicCentroidSnapshot.objects.filter(pk=kept_centroid.pk).exists() assert TopicVelocitySnapshot.objects.filter(pk=kept_velocity.pk).exists() assert SourceDiversitySnapshot.objects.filter(pk=kept_diversity.pk).exists() @@ -515,18 +533,21 @@ def test_queue_content_skill_enqueues_relevance_task(source_plugin_context, mock published_date="2026-04-20T12:00:00Z", content_text="Manual content body", ) - delay_mock = mocker.patch("core.tasks.run_relevance_scoring_skill.delay") + queue_mock = mocker.patch( + "core.tasks.run_relevance_scoring_skill.kiq", + new_callable=AsyncMock, + ) skill_result = queue_content_skill(content, RELEVANCE_SKILL_NAME) assert skill_result.status == SkillStatus.PENDING - delay_mock.assert_called_once_with(_require_pk(skill_result)) + queue_mock.assert_awaited_once_with(_require_pk(skill_result)) def test_queue_content_skill_executes_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True content = Content.objects.create( project=source_plugin_context.project, entity=source_plugin_context.entity, @@ -538,19 +559,22 @@ def test_queue_content_skill_executes_inline_when_eager( content_text="Manual content body", ) task_mock = mocker.patch("core.tasks.run_relevance_scoring_skill") - delay_mock = mocker.patch("core.tasks.run_relevance_scoring_skill.delay") + queue_mock = mocker.patch( + "core.tasks.run_relevance_scoring_skill.kiq", + new_callable=AsyncMock, + ) skill_result = queue_content_skill(content, RELEVANCE_SKILL_NAME) assert skill_result.status == SkillStatus.PENDING task_mock.assert_called_once_with(_require_pk(skill_result)) - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_queue_content_skill_executes_summary_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True content = Content.objects.create( project=source_plugin_context.project, entity=source_plugin_context.entity, @@ -563,13 +587,16 @@ def test_queue_content_skill_executes_summary_inline_when_eager( relevance_score=0.9, ) task_mock = mocker.patch("core.tasks.run_summarization_skill") - delay_mock = mocker.patch("core.tasks.run_summarization_skill.delay") + queue_mock = mocker.patch( + "core.tasks.run_summarization_skill.kiq", + new_callable=AsyncMock, + ) skill_result = queue_content_skill(content, SUMMARIZATION_SKILL_NAME) assert skill_result.status == SkillStatus.PENDING task_mock.assert_called_once_with(_require_pk(skill_result)) - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_run_relevance_scoring_skill_updates_pending_result( @@ -595,10 +622,13 @@ def test_run_relevance_scoring_skill_updates_pending_result( "latency_ms": 0, }, ) - delay_mock = mocker.patch("core.tasks.run_relevance_scoring_skill.delay") + queue_mock = mocker.patch( + "core.tasks.run_relevance_scoring_skill.kiq", + new_callable=AsyncMock, + ) pending_result = queue_content_skill(content, RELEVANCE_SKILL_NAME) - delay_mock.assert_called_once_with(_require_pk(pending_result)) + queue_mock.assert_awaited_once_with(_require_pk(pending_result)) result = run_relevance_scoring_skill(_require_pk(pending_result)) @@ -624,10 +654,13 @@ def test_run_summarization_skill_marks_result_failed_when_relevance_is_too_low( content_text="Manual content body", relevance_score=0.25, ) - delay_mock = mocker.patch("core.tasks.run_summarization_skill.delay") + queue_mock = mocker.patch( + "core.tasks.run_summarization_skill.kiq", + new_callable=AsyncMock, + ) pending_result = queue_content_skill(content, SUMMARIZATION_SKILL_NAME) - delay_mock.assert_called_once_with(_require_pk(pending_result)) + queue_mock.assert_awaited_once_with(_require_pk(pending_result)) result = run_summarization_skill(_require_pk(pending_result)) diff --git a/deploy/helm/digest-engine/templates/_helpers.tpl b/deploy/helm/digest-engine/templates/_helpers.tpl index 7b91b1db..41f0b64d 100644 --- a/deploy/helm/digest-engine/templates/_helpers.tpl +++ b/deploy/helm/digest-engine/templates/_helpers.tpl @@ -43,6 +43,10 @@ app.kubernetes.io/component: {{ .component }} {{- printf "%s-redis" (include "digest-engine.fullname" .) -}} {{- end -}} +{{- define "digest-engine.rabbitmqHost" -}} +{{- printf "%s-rabbitmq" (include "digest-engine.fullname" .) -}} +{{- end -}} + {{- define "digest-engine.qdrantHost" -}} {{- printf "%s-qdrant" (include "digest-engine.fullname" .) -}} {{- end -}} @@ -59,6 +63,10 @@ app.kubernetes.io/component: {{ .component }} {{- printf "redis://%s:%v/0" (include "digest-engine.redisHost" .) .Values.redis.service.port -}} {{- end -}} +{{- define "digest-engine.rabbitmqUrl" -}} +{{- printf "amqp://%s:%s@%s:%v/" .Values.rabbitmq.username .Values.rabbitmq.password (include "digest-engine.rabbitmqHost" .) .Values.rabbitmq.service.port -}} +{{- end -}} + {{- define "digest-engine.qdrantUrl" -}} {{- printf "http://%s:%v" (include "digest-engine.qdrantHost" .) .Values.qdrant.service.port -}} {{- end -}} diff --git a/deploy/helm/digest-engine/templates/configmap.yaml b/deploy/helm/digest-engine/templates/configmap.yaml index bc0ada31..586bba7d 100644 --- a/deploy/helm/digest-engine/templates/configmap.yaml +++ b/deploy/helm/digest-engine/templates/configmap.yaml @@ -11,6 +11,8 @@ data: SITE_ID: {{ .Values.env.siteId | quote }} REDIS_URL: {{ include "digest-engine.redisUrl" . | quote }} CHANNEL_LAYER_URL: {{ default (include "digest-engine.redisUrl" .) .Values.env.channelLayerUrl | quote }} + TASKIQ_ALWAYS_EAGER: {{ .Values.env.taskiqAlwaysEager | quote }} + TASKIQ_SCHEDULER_SKIP_FIRST_RUN: {{ .Values.env.taskiqSchedulerSkipFirstRun | quote }} QDRANT_URL: {{ include "digest-engine.qdrantUrl" . | quote }} MESSAGING_ENABLED: {{ .Values.env.messagingEnabled | quote }} NEWSLETTER_PUBLIC_URL: {{ .Values.env.newsletterPublicUrl | quote }} @@ -20,7 +22,6 @@ data: RESEND_FROM_EMAIL: {{ .Values.env.resendFromEmail | quote }} REDDIT_USER_AGENT: {{ .Values.env.redditUserAgent | quote }} LOG_LEVEL: {{ .Values.env.logLevel | quote }} - CELERY_TASK_ALWAYS_EAGER: {{ .Values.env.celeryTaskAlwaysEager | quote }} OPENROUTER_API_BASE: {{ .Values.env.openrouterApiBase | quote }} OPENROUTER_APP_URL: {{ .Values.env.openrouterAppUrl | quote }} OPENROUTER_APP_NAME: {{ .Values.env.openrouterAppName | quote }} diff --git a/deploy/helm/digest-engine/templates/keda-scaledobject.yaml b/deploy/helm/digest-engine/templates/keda-scaledobject.yaml index 45b213d5..35f8b4cf 100644 --- a/deploy/helm/digest-engine/templates/keda-scaledobject.yaml +++ b/deploy/helm/digest-engine/templates/keda-scaledobject.yaml @@ -2,20 +2,22 @@ apiVersion: keda.sh/v1alpha1 kind: ScaledObject metadata: - name: {{ include "digest-engine.fullname" . }}-celery-worker + name: {{ include "digest-engine.fullname" . }}-taskiq-worker labels: {{- include "digest-engine.labels" . | nindent 4 }} spec: scaleTargetRef: - name: {{ include "digest-engine.fullname" . }}-celery-worker + name: {{ include "digest-engine.fullname" . }}-taskiq-worker minReplicaCount: {{ .Values.keda.minReplicaCount }} maxReplicaCount: {{ .Values.keda.maxReplicaCount }} pollingInterval: {{ .Values.keda.pollingInterval }} cooldownPeriod: {{ .Values.keda.cooldownPeriod }} triggers: - - type: redis + - type: rabbitmq metadata: - address: {{ printf "%s:%v" (include "digest-engine.redisHost" .) .Values.redis.service.port | quote }} - listName: {{ .Values.keda.redisListName | quote }} - listLength: "5" + host: {{ include "digest-engine.rabbitmqUrl" . | quote }} + protocol: amqp + queueName: {{ .Values.keda.queueName | quote }} + mode: QueueLength + value: "5" {{- end }} diff --git a/deploy/helm/digest-engine/templates/networkpolicy.yaml b/deploy/helm/digest-engine/templates/networkpolicy.yaml index 85105a3e..6d7eeaad 100644 --- a/deploy/helm/digest-engine/templates/networkpolicy.yaml +++ b/deploy/helm/digest-engine/templates/networkpolicy.yaml @@ -82,6 +82,13 @@ spec: ports: - port: {{ .Values.redis.service.port }} protocol: TCP + - to: + - podSelector: + matchLabels: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 14 }} + ports: + - port: {{ .Values.rabbitmq.service.port }} + protocol: TCP - to: - podSelector: matchLabels: @@ -100,7 +107,7 @@ spec: apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: {{ include "digest-engine.fullname" . }}-allow-worker-and-beat + name: {{ include "digest-engine.fullname" . }}-allow-taskiq labels: {{- include "digest-engine.labels" . | nindent 4 }} spec: @@ -109,8 +116,8 @@ spec: - key: app.kubernetes.io/component operator: In values: - - celery-worker - - celery-beat + - taskiq-worker + - taskiq-scheduler - key: app.kubernetes.io/name operator: In values: @@ -136,6 +143,13 @@ spec: ports: - port: {{ .Values.redis.service.port }} protocol: TCP + - to: + - podSelector: + matchLabels: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 14 }} + ports: + - port: {{ .Values.rabbitmq.service.port }} + protocol: TCP - to: - podSelector: matchLabels: @@ -171,8 +185,8 @@ spec: operator: In values: - django - - celery-worker - - celery-beat + - taskiq-worker + - taskiq-scheduler - key: app.kubernetes.io/name operator: In values: @@ -205,8 +219,8 @@ spec: operator: In values: - django - - celery-worker - - celery-beat + - taskiq-worker + - taskiq-scheduler - key: app.kubernetes.io/name operator: In values: @@ -221,6 +235,40 @@ spec: --- apiVersion: networking.k8s.io/v1 kind: NetworkPolicy +metadata: + name: {{ include "digest-engine.fullname" . }}-allow-rabbitmq + labels: + {{- include "digest-engine.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 6 }} + policyTypes: + - Ingress + ingress: + - from: + - podSelector: + matchExpressions: + - key: app.kubernetes.io/component + operator: In + values: + - django + - taskiq-worker + - taskiq-scheduler + - key: app.kubernetes.io/name + operator: In + values: + - {{ include "digest-engine.name" . }} + - key: app.kubernetes.io/instance + operator: In + values: + - {{ .Release.Name }} + ports: + - port: {{ .Values.rabbitmq.service.port }} + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy metadata: name: {{ include "digest-engine.fullname" . }}-allow-qdrant labels: @@ -239,8 +287,8 @@ spec: operator: In values: - django - - celery-worker - - celery-beat + - taskiq-worker + - taskiq-scheduler - key: app.kubernetes.io/name operator: In values: diff --git a/deploy/helm/digest-engine/templates/rabbitmq-service.yaml b/deploy/helm/digest-engine/templates/rabbitmq-service.yaml new file mode 100644 index 00000000..ced975c8 --- /dev/null +++ b/deploy/helm/digest-engine/templates/rabbitmq-service.yaml @@ -0,0 +1,15 @@ +{{- if .Values.rabbitmq.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "digest-engine.rabbitmqHost" . }} + labels: + {{- include "digest-engine.labels" . | nindent 4 }} +spec: + ports: + - name: amqp + port: {{ .Values.rabbitmq.service.port }} + targetPort: amqp + selector: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 4 }} +{{- end }} diff --git a/deploy/helm/digest-engine/templates/rabbitmq-statefulset.yaml b/deploy/helm/digest-engine/templates/rabbitmq-statefulset.yaml new file mode 100644 index 00000000..a237537b --- /dev/null +++ b/deploy/helm/digest-engine/templates/rabbitmq-statefulset.yaml @@ -0,0 +1,51 @@ +{{- if .Values.rabbitmq.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "digest-engine.rabbitmqHost" . }} + labels: + {{- include "digest-engine.labels" . | nindent 4 }} +spec: + serviceName: {{ include "digest-engine.rabbitmqHost" . }} + replicas: 1 + selector: + matchLabels: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 6 }} + template: + metadata: + labels: + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "rabbitmq") | nindent 8 }} + spec: + containers: + - name: rabbitmq + image: {{ .Values.rabbitmq.image.repository }}:{{ .Values.rabbitmq.image.tag }} + imagePullPolicy: {{ .Values.rabbitmq.image.pullPolicy }} + env: + - name: RABBITMQ_DEFAULT_USER + valueFrom: + secretKeyRef: + name: {{ include "digest-engine.secretName" . }} + key: RABBITMQ_DEFAULT_USER + - name: RABBITMQ_DEFAULT_PASS + valueFrom: + secretKeyRef: + name: {{ include "digest-engine.secretName" . }} + key: RABBITMQ_DEFAULT_PASS + ports: + - containerPort: {{ .Values.rabbitmq.service.port }} + name: amqp + volumeMounts: + - name: rabbitmq-data + mountPath: /var/lib/rabbitmq + resources: + {{- toYaml .Values.rabbitmq.resources | nindent 12 }} + volumeClaimTemplates: + - metadata: + name: rabbitmq-data + spec: + accessModes: + {{- toYaml .Values.rabbitmq.persistence.accessModes | nindent 10 }} + resources: + requests: + storage: {{ .Values.rabbitmq.persistence.size }} +{{- end }} diff --git a/deploy/helm/digest-engine/templates/secret.yaml b/deploy/helm/digest-engine/templates/secret.yaml index 4f7b5c31..70c5b7bb 100644 --- a/deploy/helm/digest-engine/templates/secret.yaml +++ b/deploy/helm/digest-engine/templates/secret.yaml @@ -10,6 +10,9 @@ stringData: SECRET_KEY: {{ .Values.secrets.secretKey | quote }} DATABASE_URL: {{ include "digest-engine.databaseUrl" . | quote }} POSTGRES_PASSWORD: {{ .Values.postgres.password | quote }} + RABBITMQ_URL: {{ include "digest-engine.rabbitmqUrl" . | quote }} + RABBITMQ_DEFAULT_USER: {{ .Values.rabbitmq.username | quote }} + RABBITMQ_DEFAULT_PASS: {{ .Values.rabbitmq.password | quote }} BLUESKY_CREDENTIALS_ENCRYPTION_KEY: {{ .Values.secrets.blueskyCredentialsEncryptionKey | quote }} METRICS_TOKEN: {{ .Values.secrets.metricsToken | quote }} OPENROUTER_API_KEY: {{ .Values.secrets.openrouterApiKey | quote }} diff --git a/deploy/helm/digest-engine/templates/celery-beat-deployment.yaml b/deploy/helm/digest-engine/templates/taskiq-scheduler-deployment.yaml similarity index 62% rename from deploy/helm/digest-engine/templates/celery-beat-deployment.yaml rename to deploy/helm/digest-engine/templates/taskiq-scheduler-deployment.yaml index 3becfdde..e6a4d09b 100644 --- a/deploy/helm/digest-engine/templates/celery-beat-deployment.yaml +++ b/deploy/helm/digest-engine/templates/taskiq-scheduler-deployment.yaml @@ -1,25 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "digest-engine.fullname" . }}-celery-beat + name: {{ include "digest-engine.fullname" . }}-taskiq-scheduler labels: {{- include "digest-engine.labels" . | nindent 4 }} spec: - replicas: {{ .Values.celeryBeat.replicaCount }} + replicas: {{ .Values.taskiqScheduler.replicaCount }} selector: matchLabels: - {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "celery-beat") | nindent 6 }} + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "taskiq-scheduler") | nindent 6 }} template: metadata: labels: - {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "celery-beat") | nindent 8 }} + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "taskiq-scheduler") | nindent 8 }} spec: containers: - - name: celery-beat + - name: taskiq-scheduler image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} command: - {{- toYaml .Values.celeryBeat.command | nindent 12 }} + {{- toYaml .Values.taskiqScheduler.command | nindent 12 }} env: - name: BOOTSTRAP_APP value: "false" @@ -29,4 +29,4 @@ spec: - secretRef: name: {{ include "digest-engine.secretName" . }} resources: - {{- toYaml .Values.celeryBeat.resources | nindent 12 }} + {{- toYaml .Values.taskiqScheduler.resources | nindent 12 }} diff --git a/deploy/helm/digest-engine/templates/celery-worker-deployment.yaml b/deploy/helm/digest-engine/templates/taskiq-worker-deployment.yaml similarity index 68% rename from deploy/helm/digest-engine/templates/celery-worker-deployment.yaml rename to deploy/helm/digest-engine/templates/taskiq-worker-deployment.yaml index 2a599b4f..04dd0a25 100644 --- a/deploy/helm/digest-engine/templates/celery-worker-deployment.yaml +++ b/deploy/helm/digest-engine/templates/taskiq-worker-deployment.yaml @@ -1,25 +1,25 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: {{ include "digest-engine.fullname" . }}-celery-worker + name: {{ include "digest-engine.fullname" . }}-taskiq-worker labels: {{- include "digest-engine.labels" . | nindent 4 }} spec: - replicas: {{ .Values.celeryWorker.replicaCount }} + replicas: {{ .Values.taskiqWorker.replicaCount }} selector: matchLabels: - {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "celery-worker") | nindent 6 }} + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "taskiq-worker") | nindent 6 }} template: metadata: labels: - {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "celery-worker") | nindent 8 }} + {{- include "digest-engine.componentLabels" (dict "Release" .Release "Values" .Values "Chart" .Chart "component" "taskiq-worker") | nindent 8 }} spec: containers: - - name: celery-worker + - name: taskiq-worker image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ .Values.image.pullPolicy }} command: - {{- toYaml .Values.celeryWorker.command | nindent 12 }} + {{- toYaml .Values.taskiqWorker.command | nindent 12 }} env: - name: BOOTSTRAP_APP value: "false" @@ -29,4 +29,4 @@ spec: - secretRef: name: {{ include "digest-engine.secretName" . }} resources: - {{- toYaml .Values.celeryWorker.resources | nindent 12 }} + {{- toYaml .Values.taskiqWorker.resources | nindent 12 }} diff --git a/deploy/helm/digest-engine/values.yaml b/deploy/helm/digest-engine/values.yaml index 1e481803..a53daa6b 100644 --- a/deploy/helm/digest-engine/values.yaml +++ b/deploy/helm/digest-engine/values.yaml @@ -20,7 +20,8 @@ env: resendFromEmail: onboarding@resend.dev redditUserAgent: digest-engine/0.1 logLevel: INFO - celeryTaskAlwaysEager: "false" + taskiqAlwaysEager: "false" + taskiqSchedulerSkipFirstRun: "true" openrouterApiBase: https://openrouter.ai/api/v1 openrouterAppUrl: "" openrouterAppName: digest-engine @@ -66,24 +67,26 @@ django: - "2" resources: {} -celeryWorker: +taskiqWorker: replicaCount: 1 command: - - celery - - -A - - digest_engine + - taskiq - worker - - --loglevel=info + - digest_engine.taskiq:broker + - --fs-discover + - --log-level + - INFO resources: {} -celeryBeat: +taskiqScheduler: replicaCount: 1 command: - - celery - - -A - - digest_engine - - beat - - --loglevel=info + - sh + - -lc + - >- + skip_flag=""; + if [ "${TASKIQ_SCHEDULER_SKIP_FIRST_RUN:-true}" = "true" ]; then skip_flag="--skip-first-run"; fi; + exec taskiq scheduler digest_engine.taskiq:scheduler --fs-discover --log-level INFO ${skip_flag} resources: {} nginx: @@ -129,6 +132,23 @@ redis: - ReadWriteOnce resources: {} +rabbitmq: + enabled: true + image: + repository: rabbitmq + tag: 3.13-alpine + pullPolicy: IfNotPresent + username: digest-engine + password: digest-engine + service: + port: 5672 + persistence: + enabled: true + size: 4Gi + accessModes: + - ReadWriteOnce + resources: {} + qdrant: enabled: true image: @@ -159,7 +179,7 @@ keda: maxReplicaCount: 5 pollingInterval: 30 cooldownPeriod: 120 - redisListName: celery + queueName: taskiq networkPolicy: enabled: false diff --git a/digest_engine/__init__.py b/digest_engine/__init__.py index 0e9a8f6d..fbe37a1a 100644 --- a/digest_engine/__init__.py +++ b/digest_engine/__init__.py @@ -1,3 +1 @@ -from digest_engine.celery import app as celery_app - -__all__ = ("celery_app",) +"""Digest Engine Django project package.""" diff --git a/digest_engine/celery.py b/digest_engine/celery.py deleted file mode 100644 index 230dc8a7..00000000 --- a/digest_engine/celery.py +++ /dev/null @@ -1,17 +0,0 @@ -import os - -from celery import Celery - -from digest_engine.telemetry import configure_telemetry - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "digest_engine.settings") - -app = Celery("digest_engine") -app.config_from_object("django.conf:settings", namespace="CELERY") -app.autodiscover_tasks() -configure_telemetry(celery_app=app) - - -@app.task(bind=True) -def debug_task(self): - print(f"Request: {self.request!r}") diff --git a/digest_engine/ninja_api.py b/digest_engine/ninja_api.py new file mode 100644 index 00000000..ec8852e7 --- /dev/null +++ b/digest_engine/ninja_api.py @@ -0,0 +1,28 @@ +"""Project-level Django Ninja API registration.""" + +from ninja import NinjaAPI + +from messaging.ninja_api import router as messaging_router +from notifications.ninja_api import router as notifications_router +from projects.ninja_api import router as projects_router +from users.ninja_api import router as users_router + + +def _build_api(*, urls_namespace: str) -> NinjaAPI: + """Create one Ninja API instance with the shared Digest Engine routers.""" + + api = NinjaAPI( + title="Digest Engine Ninja API", + version="1.0.0", + urls_namespace=urls_namespace, + ) + api.add_router("/v1", users_router) + api.add_router("/v1", notifications_router) + api.add_router("/v1", messaging_router) + api.add_router("/v1", projects_router) + return api + + +api = _build_api(urls_namespace="ninja-api") + +__all__ = ["api"] diff --git a/digest_engine/settings/__init__.py b/digest_engine/settings/__init__.py index f1b8f590..463a0a79 100644 --- a/digest_engine/settings/__init__.py +++ b/digest_engine/settings/__init__.py @@ -42,7 +42,6 @@ DEBUG, DEFAULT_AUTO_FIELD, DEFAULT_FROM_EMAIL, - DRF_STANDARDIZED_ERRORS, EMAIL_BACKEND, FRONTEND_BASE_URL, INSTALLED_APPS, @@ -74,7 +73,6 @@ RESEND_API_KEY, RESEND_FROM_EMAIL, RESEND_INBOUND_SECRET, - REST_FRAMEWORK, ROOT_URLCONF, SECRET_KEY, SECURE_PROXY_SSL_HEADER, @@ -90,18 +88,14 @@ USE_X_FORWARDED_HOST, WSGI_APPLICATION, ) -from .celery import ( - CELERY_BEAT_SCHEDULE, - CELERY_BROKER_URL, - CELERY_RESULT_BACKEND, - CELERY_TASK_ALWAYS_EAGER, - CELERY_TASK_SOFT_TIME_LIMIT, - CELERY_TASK_TIME_LIMIT, - CELERY_WORKER_REDIRECT_STDOUTS_LEVEL, +from .logging import LOG_LEVEL, LOGGING +from .taskiq import ( + RABBITMQ_URL, REDIS_URL, + TASKIQ_ALWAYS_EAGER, + TASKIQ_RESULT_BACKEND_URL, + TASKIQ_SCHEDULER_SKIP_FIRST_RUN, ) -from .logging import LOG_LEVEL, LOGGING -from .swagger import SPECTACULAR_SETTINGS __all__ = [ "ACCOUNT_EMAIL_VERIFICATION", @@ -126,13 +120,6 @@ "ASGI_APPLICATION", "CHANNEL_LAYER_PREFIX", "CHANNEL_LAYER_URL", - "CELERY_BEAT_SCHEDULE", - "CELERY_BROKER_URL", - "CELERY_RESULT_BACKEND", - "CELERY_TASK_ALWAYS_EAGER", - "CELERY_WORKER_REDIRECT_STDOUTS_LEVEL", - "CELERY_TASK_SOFT_TIME_LIMIT", - "CELERY_TASK_TIME_LIMIT", "CHANNEL_LAYERS", "CSRF_TRUSTED_ORIGINS", "DATABASES", @@ -140,7 +127,6 @@ "DEBUG", "DEFAULT_FROM_EMAIL", "DEFAULT_AUTO_FIELD", - "DRF_STANDARDIZED_ERRORS", "EMAIL_BACKEND", "FRONTEND_BASE_URL", "ANYMAIL", @@ -178,21 +164,23 @@ "OPENROUTER_APP_NAME", "OPENROUTER_APP_URL", "QDRANT_URL", + "RABBITMQ_URL", "REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET", "REDDIT_USER_AGENT", "RESEND_API_KEY", "RESEND_INBOUND_SECRET", "RESEND_FROM_EMAIL", + "TASKIQ_ALWAYS_EAGER", + "TASKIQ_RESULT_BACKEND_URL", + "TASKIQ_SCHEDULER_SKIP_FIRST_RUN", "ANYMAIL_WEBHOOK_SECRET", "REDIS_URL", - "REST_FRAMEWORK", "ROOT_URLCONF", "SERVER_EMAIL", "SECRET_KEY", "SECURE_PROXY_SSL_HEADER", "SITE_ID", - "SPECTACULAR_SETTINGS", "STORAGES", "STATIC_ROOT", "STATIC_URL", diff --git a/digest_engine/settings/base.py b/digest_engine/settings/base.py index b06f7315..33ba0a22 100644 --- a/digest_engine/settings/base.py +++ b/digest_engine/settings/base.py @@ -122,10 +122,6 @@ def env_list(name: str, default: str = "") -> list[str]: "django.contrib.sites", # Required for allauth "channels", # 4. Third-Party Authentication & API Tools - "rest_framework", - "rest_framework.authtoken", - "dj_rest_auth", - "dj_rest_auth.registration", "allauth", "allauth.account", "allauth.socialaccount", @@ -134,8 +130,6 @@ def env_list(name: str, default: str = "") -> list[str]: "anymail", # 5. Utilities & Schema Tools "import_export", # Standard library - "drf_spectacular", - "drf_standardized_errors", # 6. Project Apps "users", "projects", @@ -255,25 +249,6 @@ def env_list(name: str, default: str = "") -> list[str]: MEDIA_URL = os.getenv("MEDIA_URL", "/media/") MEDIA_ROOT = Path(os.getenv("MEDIA_ROOT", str(BASE_DIR / "var" / "media"))) -# DRF: the API defaults to authenticated access so browser sessions and basic -# auth work locally, but anonymous requests are rejected. -REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": [ - "rest_framework.permissions.IsAuthenticated", - ], - "DEFAULT_AUTHENTICATION_CLASSES": [ - "rest_framework.authentication.SessionAuthentication", - "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.BasicAuthentication", - ], - "DEFAULT_SCHEMA_CLASS": "drf_standardized_errors.openapi.AutoSchema", - "EXCEPTION_HANDLER": "drf_standardized_errors.handler.exception_handler", -} - -DRF_STANDARDIZED_ERRORS = { - "ALLOWED_ERROR_STATUS_CODES": ["400", "403", "404"], -} - SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") USE_X_FORWARDED_HOST = True @@ -336,8 +311,6 @@ def env_list(name: str, default: str = "") -> list[str]: "MEDIA_URL", "MEDIA_ROOT", "MESSAGING_ENABLED", - "REST_FRAMEWORK", - "DRF_STANDARDIZED_ERRORS", "CHANNEL_LAYERS", "SECURE_PROXY_SSL_HEADER", "USE_X_FORWARDED_HOST", diff --git a/digest_engine/settings/celery.py b/digest_engine/settings/celery.py deleted file mode 100644 index c187634a..00000000 --- a/digest_engine/settings/celery.py +++ /dev/null @@ -1,69 +0,0 @@ -import os - -from celery.schedules import crontab - -from .base import env_bool - -REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") - -# Celery: these settings point workers at Redis and keep recurring -# ingestion and trend-analysis jobs on their beat schedules. -CELERY_BROKER_URL = os.getenv("CELERY_BROKER_URL", REDIS_URL) -CELERY_RESULT_BACKEND = os.getenv("CELERY_RESULT_BACKEND", REDIS_URL) -CELERY_TASK_ALWAYS_EAGER = env_bool("CELERY_TASK_ALWAYS_EAGER", default=False) -CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" -CELERY_TASK_TIME_LIMIT = 300 -CELERY_TASK_SOFT_TIME_LIMIT = 270 -CELERY_BEAT_SCHEDULE = { - "run-all-source-ingestions-every-6-hours": { - "task": "core.tasks.run_all_ingestions", - "schedule": 60 * 60 * 6, - }, - "refresh-linkedin-tokens-hourly": { - "task": "core.tasks.refresh_linkedin_tokens", - "schedule": 60 * 60, - }, - "run-all-source-quality-recomputations-nightly": { - "task": "core.tasks.run_all_source_quality_recomputations", - "schedule": crontab(hour=1, minute=45), - }, - "run-all-scheduled-newsletter-drafts-every-minute": { - "task": "core.tasks.run_all_scheduled_newsletter_drafts", - "schedule": 60, - }, - "run-all-authority-recomputations-nightly": { - "task": "core.tasks.run_all_authority_recomputations", - "schedule": crontab(hour=2, minute=0), - }, - "run-all-topic-centroid-recomputations-nightly": { - "task": "core.tasks.run_all_topic_centroid_recomputations", - "schedule": crontab(hour=3, minute=0), - }, - "run-all-topic-cluster-recomputations-nightly": { - "task": "core.tasks.run_all_topic_cluster_recomputations", - "schedule": crontab(hour=4, minute=0), - }, - "run-all-entity-candidate-clustering-nightly": { - "task": "entities.tasks.run_all_entity_candidate_clustering", - "schedule": crontab(hour=4, minute=30), - }, - "run-all-entity-candidate-auto-promotions-nightly": { - "task": "entities.tasks.run_all_entity_candidate_auto_promotions", - "schedule": crontab(hour=4, minute=45), - }, - "run-all-retention-policies-nightly": { - "task": "core.tasks.run_all_retention_policies", - "schedule": crontab(hour=5, minute=15), - }, -} - -__all__ = [ - "REDIS_URL", - "CELERY_BROKER_URL", - "CELERY_RESULT_BACKEND", - "CELERY_TASK_ALWAYS_EAGER", - "CELERY_WORKER_REDIRECT_STDOUTS_LEVEL", - "CELERY_TASK_TIME_LIMIT", - "CELERY_TASK_SOFT_TIME_LIMIT", - "CELERY_BEAT_SCHEDULE", -] diff --git a/digest_engine/settings/swagger.py b/digest_engine/settings/swagger.py deleted file mode 100644 index 2180eb0f..00000000 --- a/digest_engine/settings/swagger.py +++ /dev/null @@ -1,49 +0,0 @@ -SPECTACULAR_SETTINGS = { - "TITLE": "Digest Engine API", - "DESCRIPTION": "API documentation for the digest engine app", - "VERSION": "1.0.0", - "SERVE_INCLUDE_SCHEMA": False, - "POSTPROCESSING_HOOKS": [ - "drf_standardized_errors.openapi_hooks.postprocess_schema_enums" - ], - "ENUM_NAME_OVERRIDES": { - "ValidationErrorEnum": "drf_standardized_errors.openapi_serializers.ValidationErrorEnum.choices", - "ClientErrorEnum": "drf_standardized_errors.openapi_serializers.ClientErrorEnum.choices", - "ServerErrorEnum": "drf_standardized_errors.openapi_serializers.ServerErrorEnum.choices", - "ParseErrorCodeEnum": "drf_standardized_errors.openapi_serializers.ParseErrorCodeEnum.choices", - "ErrorCode403Enum": "drf_standardized_errors.openapi_serializers.ErrorCode403Enum.choices", - "ErrorCode404Enum": "drf_standardized_errors.openapi_serializers.ErrorCode404Enum.choices", - }, - "TAGS": [ - { - "name": "Project Management", - "description": "Create projects and manage project-specific configuration for newsletter workspaces.", - }, - { - "name": "Entity Catalog", - "description": "Manage tracked people, companies, and organizations associated with a project.", - }, - { - "name": "Content Library", - "description": "Browse and maintain ingested content items that feed newsletter generation and ranking.", - }, - { - "name": "AI Processing", - "description": "Inspect AI skill execution results, model outputs, and confidence metadata for project content.", - }, - { - "name": "Feedback", - "description": "Capture editorial feedback signals that influence ranking and future recommendation quality.", - }, - { - "name": "Ingestion", - "description": "Configure source plugins and review ingestion runs for each project.", - }, - { - "name": "Review Queue", - "description": "Review borderline or low-confidence content items that need human resolution.", - }, - ], -} - -__all__ = ["SPECTACULAR_SETTINGS"] diff --git a/digest_engine/settings/taskiq.py b/digest_engine/settings/taskiq.py new file mode 100644 index 00000000..e5db973b --- /dev/null +++ b/digest_engine/settings/taskiq.py @@ -0,0 +1,23 @@ +"""Taskiq settings shared by the broker bootstrap and future task migrations.""" + +from __future__ import annotations + +import os + +from .base import env_bool + +REDIS_URL = os.getenv("REDIS_URL", "redis://redis:6379/0") +RABBITMQ_URL = os.getenv("RABBITMQ_URL", "amqp://guest:guest@rabbitmq:5672/") +TASKIQ_RESULT_BACKEND_URL = os.getenv("TASKIQ_RESULT_BACKEND_URL", REDIS_URL) +TASKIQ_ALWAYS_EAGER = env_bool("TASKIQ_ALWAYS_EAGER", default=False) +TASKIQ_SCHEDULER_SKIP_FIRST_RUN = env_bool( + "TASKIQ_SCHEDULER_SKIP_FIRST_RUN", default=True +) + +__all__ = [ + "REDIS_URL", + "RABBITMQ_URL", + "TASKIQ_ALWAYS_EAGER", + "TASKIQ_RESULT_BACKEND_URL", + "TASKIQ_SCHEDULER_SKIP_FIRST_RUN", +] diff --git a/digest_engine/taskiq.py b/digest_engine/taskiq.py new file mode 100644 index 00000000..11e87732 --- /dev/null +++ b/digest_engine/taskiq.py @@ -0,0 +1,64 @@ +"""Taskiq broker and scheduler bootstrap for the Digest Engine project.""" + +from __future__ import annotations + +import os +from inspect import iscoroutinefunction +from typing import Any, cast + +from asgiref.sync import async_to_sync +from django.conf import settings +from taskiq import TaskiqScheduler +from taskiq.schedule_sources import LabelScheduleSource +from taskiq_aio_pika import AioPikaBroker +from taskiq_redis import RedisAsyncResultBackend + +from digest_engine.settings import ( + RABBITMQ_URL, + TASKIQ_RESULT_BACKEND_URL, +) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "digest_engine.settings") + +result_backend = RedisAsyncResultBackend( + redis_url=TASKIQ_RESULT_BACKEND_URL, + result_ex_time=60 * 60, +) + +broker = AioPikaBroker(RABBITMQ_URL).with_result_backend(result_backend) + +scheduler = TaskiqScheduler( + broker=broker, + sources=[LabelScheduleSource(broker)], +) + + +def enqueue_task(task: object, *args: object, **kwargs: object) -> object: + """Dispatch one Taskiq task from synchronous Django code.""" + + return async_to_sync(cast(Any, task).kiq)(*args, **kwargs) + + +def run_task_inline(task: object, *args: object, **kwargs: object) -> object: + """Run one Taskiq task inline for eager and test-only flows.""" + + original_func = cast(Any, task).original_func + if iscoroutinefunction(original_func): + return async_to_sync(original_func)(*args, **kwargs) + return original_func(*args, **kwargs) + + +def task_always_eager() -> bool: + """Return whether Taskiq-backed tasks should execute inline.""" + + return bool(getattr(settings, "TASKIQ_ALWAYS_EAGER", False)) + + +__all__ = [ + "broker", + "enqueue_task", + "result_backend", + "run_task_inline", + "scheduler", + "task_always_eager", +] diff --git a/digest_engine/telemetry.py b/digest_engine/telemetry.py index 5f6ca386..55bca39f 100644 --- a/digest_engine/telemetry.py +++ b/digest_engine/telemetry.py @@ -15,7 +15,6 @@ _PROVIDER_CONFIGURED = False _DJANGO_INSTRUMENTED = False -_CELERY_INSTRUMENTED = False _DEPENDENCY_WARNING_EMITTED = False @@ -58,7 +57,6 @@ def _warn_missing_dependencies() -> None: def configure_telemetry( *, instrument_django: bool = False, - celery_app: object | None = None, ) -> bool: """Configure OTLP tracing and instrument supported runtimes when enabled.""" @@ -70,7 +68,6 @@ def configure_telemetry( from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter, ) - from opentelemetry.instrumentation.celery import CeleryInstrumentor from opentelemetry.instrumentation.django import DjangoInstrumentor from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider @@ -79,7 +76,7 @@ def configure_telemetry( _warn_missing_dependencies() return False - global _PROVIDER_CONFIGURED, _DJANGO_INSTRUMENTED, _CELERY_INSTRUMENTED + global _PROVIDER_CONFIGURED, _DJANGO_INSTRUMENTED if not _PROVIDER_CONFIGURED: resource = Resource.create( @@ -107,13 +104,6 @@ def configure_telemetry( DjangoInstrumentor().instrument(tracer_provider=tracer_provider) _DJANGO_INSTRUMENTED = True - if celery_app is not None and not _CELERY_INSTRUMENTED: - CeleryInstrumentor().instrument( - tracer_provider=tracer_provider, - skip_dep_check=True, - ) - _CELERY_INSTRUMENTED = True - return True diff --git a/digest_engine/urls.py b/digest_engine/urls.py index a20fd5b8..5bc44863 100644 --- a/digest_engine/urls.py +++ b/digest_engine/urls.py @@ -7,10 +7,20 @@ from django.shortcuts import redirect from django.urls import include, path from django.views.generic.base import RedirectView -from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from core.auth_views import GitHubLoginView, GoogleLoginView +from digest_engine.ninja_api import api as ninja_api +from projects.linkedin_oauth import linkedin_oauth_callback_view from trends.metrics import trend_task_run_metrics_view +from users.auth_views import ( + login_view, + logout_view, + password_change_view, + password_reset_confirm_view, + password_reset_view, + register_view, + user_view, +) def root_redirect_view(request): @@ -19,6 +29,18 @@ def root_redirect_view(request): return redirect("/admin/") +legacy_v1_callback_patterns = ( + [ + path( + "linkedin/oauth/callback/", + linkedin_oauth_callback_view, + name="linkedin-oauth-callback", + ) + ], + "api", +) + + urlpatterns = [ path("metrics", trend_task_run_metrics_view, name="metrics"), path("metrics/", trend_task_run_metrics_view), @@ -29,16 +51,31 @@ def root_redirect_view(request): path("anymail/", include("anymail.urls")), path( "api/docs/", - SpectacularSwaggerView.as_view(url_name="schema"), - name="swagger-ui", + RedirectView.as_view(url="/api/docs", permanent=False), + name="api-docs", + ), + path( + "api/schema/", + RedirectView.as_view(url="/api/openapi.json", permanent=False), + name="openapi-schema", + ), + path("api/auth/login/", login_view, name="auth_login"), + path("api/auth/logout/", logout_view, name="auth_logout"), + path( + "api/auth/password/change/", password_change_view, name="auth_password_change" + ), + path("api/auth/password/reset/", password_reset_view, name="auth_password_reset"), + path( + "api/auth/password/reset/confirm/", + password_reset_confirm_view, + name="auth_password_reset_confirm", ), - path("api/schema/", SpectacularAPIView.as_view(), name="schema"), - path("api/auth/", include("dj_rest_auth.urls")), - path("api/auth/registration/", include("dj_rest_auth.registration.urls")), + path("api/auth/registration/", register_view, name="auth_register"), + path("api/auth/user/", user_view.as_view(), name="auth_user"), path("api/auth/github/", GitHubLoginView.as_view(), name="github_login"), path("api/auth/google/", GoogleLoginView.as_view(), name="google_login"), - path("api/v1/", include("users.api_urls")), - path("api/v1/", include(("core.api_urls", "api"), namespace="v1")), + path("api/v1/", include(legacy_v1_callback_patterns, namespace="v1")), + path("api/", ninja_api.urls), path( "favicon.ico", RedirectView.as_view(url="/static/core/favicon.ico", permanent=True), diff --git a/docker-compose.yml b/docker-compose.yml index 3f745cfd..9514ff54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ x-app-common: &app-common x-shared-env: &shared-env DATABASE_URL: postgresql://newsletter:newsletter@postgres:5432/digest_engine + RABBITMQ_URL: amqp://digest-engine:digest-engine@rabbitmq:5672/ REDIS_URL: redis://redis:6379/0 QDRANT_URL: http://qdrant:6333 @@ -38,12 +39,12 @@ services: qdrant: condition: service_started healthcheck: - test: [ "CMD", "python", "manage.py", "check" ] + test: ["CMD", "python", "manage.py", "check"] interval: 30s timeout: 10s retries: 5 - celery-worker: + taskiq-worker: <<: *app-common environment: BOOTSTRAP_APP: "false" @@ -56,19 +57,24 @@ services: "--pattern=*.py", "--recursive", "--", - "celery", - "-A", - "digest_engine", + "taskiq", "worker", - "--loglevel=info", + "digest_engine.taskiq:broker", + "--fs-discover", + "--log-level", + "INFO", ] depends_on: postgres: condition: service_healthy + rabbitmq: + condition: service_healthy redis: condition: service_healthy + qdrant: + condition: service_started - celery-beat: + taskiq-scheduler: <<: *app-common environment: BOOTSTRAP_APP: "false" @@ -77,15 +83,17 @@ services: [ "sh", "-lc", - "mkdir -p .cache && exec watchmedo auto-restart --directory=. - --pattern=*.py --recursive -- celery -A digest_engine beat - --loglevel=info --schedule=.cache/celerybeat-schedule", + 'skip_flag=''''; if [ "$${TASKIQ_SCHEDULER_SKIP_FIRST_RUN:-true}" = "true" ]; then skip_flag=''--skip-first-run''; fi; exec watchmedo auto-restart --directory=. --pattern=*.py --recursive -- taskiq scheduler digest_engine.taskiq:scheduler --fs-discover --log-level INFO $${skip_flag}', ] depends_on: postgres: condition: service_healthy + rabbitmq: + condition: service_healthy redis: condition: service_healthy + qdrant: + condition: service_started postgres: image: postgres:16 @@ -96,18 +104,31 @@ services: volumes: - postgres_data:/var/lib/postgresql/data healthcheck: - test: [ "CMD-SHELL", "pg_isready -U newsletter -d digest_engine" ] + test: ["CMD-SHELL", "pg_isready -U newsletter -d digest_engine"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine - command: [ "redis-server", "--save", "", "--appendonly", "no" ] + command: ["redis-server", "--save", "", "--appendonly", "no"] volumes: - redis_data:/data healthcheck: - test: [ "CMD", "redis-cli", "ping" ] + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + rabbitmq: + image: rabbitmq:3.13-alpine + environment: + RABBITMQ_DEFAULT_USER: digest-engine + RABBITMQ_DEFAULT_PASS: digest-engine + volumes: + - rabbitmq_data:/var/lib/rabbitmq + healthcheck: + test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"] interval: 10s timeout: 5s retries: 5 @@ -153,5 +174,6 @@ services: volumes: postgres_data: redis_data: + rabbitmq_data: qdrant_data: frontend_node_modules: diff --git a/docs/README.md b/docs/README.md index 84c22eda..681828b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,7 +7,7 @@ These documents are organized by audience. * **I am an Editor or Curator using the product day-to-day**: Head to the [User Guide](user-guide/getting-started-saas.md) to learn how to ingest content, manage authority, and synthesize drafts. * **I am an Administrator installing or managing the platform**: Head to the [Admin Guide](admin-guide/overview.md) to understand Docker deployments, API keys, and queue troubleshooting. * **I am a Developer contributing code to this repository**: Head to the [Developer Guide](developer-guide/overview.md) to understand local workflows, architecture, and coding conventions. -* **I need to understand the underlying Math and Logic**: Head to the [Reference Section](reference/data-model.md) to see how LangGraph, LangChain, Celery, Qdrant, and the Cosine similarity algorithms are wired together. +* **I need to understand the underlying Math and Logic**: Head to the [Reference Section](reference/data-model.md) to see how LangGraph, LangChain, Taskiq, Qdrant, and the Cosine similarity algorithms are wired together. ## Terminology Note In this repository, a distinct newsletter workspace is called a **Project** (not a Tenant, not a Workspace). An article or extracted text is called **Content**. diff --git a/entities/api.py b/entities/api.py deleted file mode 100644 index 31b00aab..00000000 --- a/entities/api.py +++ /dev/null @@ -1,265 +0,0 @@ -"""Entity-domain API viewsets kept under the existing nested project routes.""" - -from django.db.models import Count, Prefetch -from drf_spectacular.utils import OpenApiParameter, extend_schema -from rest_framework import serializers, viewsets -from rest_framework.decorators import action -from rest_framework.exceptions import NotFound -from rest_framework.filters import OrderingFilter -from rest_framework.permissions import BasePermission -from rest_framework.response import Response - -from core.api import ( - AUTHENTICATION_REQUIRED_RESPONSE, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import ( - IsProjectAdmin, - IsProjectContributor, - IsProjectMember, - IsProjectMemberWritable, -) -from entities.extraction import ( - accept_entity_candidate, - merge_entity_candidate, - reject_entity_candidate, -) -from entities.models import Entity, EntityCandidate, EntityMention -from entities.serializers import ( - EntityAuthoritySnapshotSerializer, - EntityCandidateMergeSerializer, - EntityCandidateSerializer, - EntityMentionSummarySerializer, - EntitySerializer, -) - - -@document_project_owned_viewset( - resource_plural="entities", - resource_singular="entity", - create_description="Create a new tracked entity for the selected project, such as a company, person, or organization.", - tag="Entity Catalog", - action_overrides=build_crud_action_overrides( - EntitySerializer, - resource_plural="entities for the selected project", - resource_singular="entity", - ), -) -class EntityViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage tracked entities associated with a project.""" - - serializer_class = EntitySerializer - filter_backends = [OrderingFilter] - ordering_fields = ["authority_score", "created_at", "name"] - ordering = ["name"] - queryset = ( - Entity.objects.select_related("project") - .annotate(mention_count=Count("mentions", distinct=True)) - .prefetch_related("identity_claims") - .prefetch_related( - Prefetch( - "mentions", - queryset=EntityMention.objects.select_related("content").order_by( - "-created_at" - ), - to_attr="prefetched_mentions", - ) - ) - ) - - def get_permissions(self): - """Apply read, contributor-write, and admin-delete permissions for entities.""" - - permission_classes: list[type[BasePermission]] - if self.action == "destroy": - permission_classes = [IsProjectAdmin] - elif self.action in {"create", "update", "partial_update"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - @extend_schema( - summary="List entity mentions", - description="Return the extracted mention history for one tracked entity inside the selected project.", - request=None, - responses={ - 200: EntityMentionSummarySerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["get"], url_path="mentions") - def mentions(self, request, *args, **kwargs): - """Return the extracted mentions for the selected entity.""" - - entity = self.get_object() - mentions = entity.mentions.select_related("content").order_by("-created_at") - serializer = EntityMentionSummarySerializer(mentions, many=True) - return Response(serializer.data) - - @extend_schema( - summary="Get current authority components", - description=( - "Return the latest persisted authority-score component breakdown for one " - "tracked entity inside the selected project." - ), - request=None, - responses={ - 200: EntityAuthoritySnapshotSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["get"], url_path="authority_components") - def authority_components(self, request, *args, **kwargs): - """Return the latest authority snapshot for the selected entity.""" - - entity = self.get_object() - snapshot = entity.authority_snapshots.order_by("-computed_at").first() - if snapshot is None: - raise NotFound("No authority snapshots exist for this entity yet.") - serializer = EntityAuthoritySnapshotSerializer(snapshot) - return Response(serializer.data) - - @extend_schema( - summary="List authority history", - description=( - "Return persisted authority-score snapshots for one tracked entity. " - "Use the optional limit query parameter to cap the number of snapshots returned." - ), - parameters=[ - OpenApiParameter( - name="limit", - type=int, - location=OpenApiParameter.QUERY, - description="Maximum number of authority snapshots to return.", - required=False, - ) - ], - request=None, - responses={ - 200: EntityAuthoritySnapshotSerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["get"], url_path="authority_history") - def authority_history(self, request, *args, **kwargs): - """Return recent authority snapshots for the selected entity.""" - - entity = self.get_object() - snapshots = entity.authority_snapshots.order_by("-computed_at") - limit_param = request.query_params.get("limit") - if limit_param: - try: - limit = max(1, min(int(limit_param), 100)) - except ValueError as exc: - raise serializers.ValidationError( - {"limit": "Limit must be an integer between 1 and 100."} - ) from exc - snapshots = snapshots[:limit] - serializer = EntityAuthoritySnapshotSerializer(snapshots, many=True) - return Response(serializer.data) - - -@document_project_owned_viewset( - resource_plural="entity candidates", - resource_singular="entity candidate", - create_description="Entity candidates are created by the pipeline and can be reviewed through dedicated actions.", - tag="Entity Catalog", - action_overrides=build_crud_action_overrides( - EntityCandidateSerializer, - resource_plural="entity candidates for the selected project", - resource_singular="entity candidate", - ), -) -class EntityCandidateViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect and resolve entity candidates surfaced by entity extraction.""" - - serializer_class = EntityCandidateSerializer - queryset = EntityCandidate.objects.select_related( - "project", "first_seen_in", "merged_into" - ).prefetch_related("evidence") - - def get_permissions(self): - """Allow all members to read candidates and contributors to resolve them.""" - - permission_classes: list[type[BasePermission]] - if self.action in {"accept", "reject", "merge"}: - permission_classes = [IsProjectContributor] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - @extend_schema( - summary="Accept entity candidate", - description="Promote a pending entity candidate into a tracked entity and backfill recent mentions.", - request=None, - responses={ - 200: EntityCandidateSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["post"], url_path="accept") - def accept(self, request, *args, **kwargs): - """Accept an entity candidate and return its updated representation.""" - - candidate = self.get_object() - accept_entity_candidate(candidate, schedule_enrichment=True) - candidate.refresh_from_db() - serializer = self.get_serializer(candidate) - return Response(serializer.data) - - @extend_schema( - summary="Reject entity candidate", - description="Mark a pending entity candidate as rejected without creating a tracked entity.", - request=None, - responses={ - 200: EntityCandidateSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["post"], url_path="reject") - def reject(self, request, *args, **kwargs): - """Reject an entity candidate and return its updated representation.""" - - candidate = self.get_object() - reject_entity_candidate(candidate) - candidate.refresh_from_db() - serializer = self.get_serializer(candidate) - return Response(serializer.data) - - @extend_schema( - summary="Merge entity candidate", - description="Merge a pending entity candidate into an existing tracked entity from the same project.", - request=EntityCandidateMergeSerializer, - responses={ - 200: EntityCandidateSerializer, - 400: EntityCandidateMergeSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Entity Catalog"], - ) - @action(detail=True, methods=["post"], url_path="merge") - def merge(self, request, *args, **kwargs): - """Merge an entity candidate into an existing tracked entity.""" - - candidate = self.get_object() - serializer = EntityCandidateMergeSerializer( - data=request.data, - context=self.get_serializer_context(), - ) - serializer.is_valid(raise_exception=True) - merge_entity_candidate( - candidate, - serializer.validated_data["merged_into"], - schedule_enrichment=True, - ) - candidate.refresh_from_db() - response_serializer = self.get_serializer(candidate) - return Response(response_serializer.data) diff --git a/entities/api_urls.py b/entities/api_urls.py deleted file mode 100644 index 49e6a244..00000000 --- a/entities/api_urls.py +++ /dev/null @@ -1,16 +0,0 @@ -"""API route registration for entity resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from entities.api import EntityCandidateViewSet, EntityViewSet - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested entity endpoints.""" - - project_router.register(r"entities", EntityViewSet, basename="project-entity") - project_router.register( - r"entity-candidates", - EntityCandidateViewSet, - basename="project-entity-candidate", - ) diff --git a/entities/extraction.py b/entities/extraction.py index 78f66b57..2a039b34 100644 --- a/entities/extraction.py +++ b/entities/extraction.py @@ -15,6 +15,7 @@ from content.models import Content from core.embeddings import search_similar_entities_for_content from core.llm import build_skill_user_prompt, get_skill_definition, openrouter_chat_json +from digest_engine.taskiq import enqueue_task, task_always_eager from entities.models import ( Entity, EntityCandidate, @@ -355,10 +356,10 @@ def queue_entity_identity_enrichment(entity_id: int) -> None: from entities.tasks import enrich_entity_identity - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): enrich_entity_identity(entity_id) return - enrich_entity_identity.delay(entity_id) + enqueue_task(enrich_entity_identity, entity_id) def backfill_entity_mentions( diff --git a/entities/ninja_api.py b/entities/ninja_api.py new file mode 100644 index 00000000..af9df65f --- /dev/null +++ b/entities/ninja_api.py @@ -0,0 +1,686 @@ +"""Django Ninja endpoints for project-scoped entity resources.""" + +from __future__ import annotations + +import datetime +from typing import Any + +from django.core.exceptions import ValidationError +from django.db.models import Count, Prefetch, QuerySet +from ninja import Path, Query, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from entities.extraction import ( + accept_entity_candidate, + merge_entity_candidate, + reject_entity_candidate, +) +from entities.models import ( + Entity, + EntityAuthoritySnapshot, + EntityCandidate, + EntityMention, +) +from projects.ninja_helpers import ( + _get_project_or_404, + _require_project_admin, + _require_project_writable, +) + +entity_router = Router(tags=["Entity Catalog"]) +entity_candidate_router = Router(tags=["Entity Catalog"]) + +ENTITY_ORDERING_FIELDS = {"authority_score", "created_at", "name"} + + +class EntityIdentityClaimSchema(Schema): + """Serialized external identity claim payload.""" + + id: int + surface: str + claim_url: str + verified: bool + verified_at: datetime.datetime | None = None + verification_method: str + + +class EntityMentionSummarySchema(Schema): + """Serialized compact entity mention payload.""" + + id: int + content_id: int + content_title: str + role: str + sentiment: str + span: str + confidence: float + created_at: datetime.datetime + + +class EntitySchema(Schema): + """Serialized tracked entity payload.""" + + id: int + project: int + name: str + type: str + description: str + authority_score: float + website_url: str + github_url: str + linkedin_url: str + bluesky_handle: str + mastodon_handle: str + twitter_handle: str + identity_claims: list[EntityIdentityClaimSchema] + mention_count: int + latest_mentions: list[EntityMentionSummarySchema] + created_at: datetime.datetime + + +class EntityCreateInput(Schema): + """Writable entity fields for create requests.""" + + name: str + type: str + description: str = "" + authority_score: float | None = None + website_url: str = "" + github_url: str = "" + linkedin_url: str = "" + bluesky_handle: str = "" + mastodon_handle: str = "" + twitter_handle: str = "" + project: int | None = None + + +class EntityUpdateInput(Schema): + """Writable entity fields for update requests.""" + + name: str | None = None + type: str | None = None + description: str | None = None + authority_score: float | None = None + website_url: str | None = None + github_url: str | None = None + linkedin_url: str | None = None + bluesky_handle: str | None = None + mastodon_handle: str | None = None + twitter_handle: str | None = None + project: int | None = None + + +class EntityAuthoritySnapshotSchema(Schema): + """Serialized entity authority snapshot payload.""" + + id: int + entity: int + project: int + computed_at: datetime.datetime + mention_component: float + engagement_component: float + recency_component: float + source_quality_component: float + cross_newsletter_component: float + feedback_component: float + duplicate_component: float + decayed_prior: float + weights_at_compute: dict[str, Any] + final_score: float + + +class EntityCandidateSchema(Schema): + """Serialized entity candidate payload.""" + + id: int + project: int + name: str + suggested_type: str + first_seen_in: int | None = None + first_seen_title: str | None = None + occurrence_count: int + cluster_key: str + auto_promotion_blocked_reason: str + evidence_count: int + source_plugin_count: int + source_plugins: list[str] + identity_surfaces: list[str] + status: str + merged_into: int | None = None + merged_into_name: str | None = None + created_at: datetime.datetime + updated_at: datetime.datetime + + +class EntityCandidateMergeInput(Schema): + """Writable merge payload for one entity candidate.""" + + merged_into: int + + +def _entity_queryset() -> QuerySet[Entity]: + """Return the canonical queryset used by entity endpoints.""" + + return ( + Entity.objects.select_related("project") + .annotate(mention_count=Count("mentions", distinct=True)) + .prefetch_related("identity_claims") + .prefetch_related( + Prefetch( + "mentions", + queryset=EntityMention.objects.select_related("content").order_by( + "-created_at" + ), + to_attr="prefetched_mentions", + ) + ) + ) + + +def _entity_candidate_queryset() -> QuerySet[EntityCandidate]: + """Return the canonical queryset used by entity-candidate endpoints.""" + + return EntityCandidate.objects.select_related( + "project", "first_seen_in", "merged_into" + ).prefetch_related("evidence") + + +def _serialize_entity(entity: Entity) -> dict[str, Any]: + """Return one entity response body.""" + + prefetched_mentions = getattr(entity, "prefetched_mentions", None) + if prefetched_mentions is None: + prefetched_mentions = entity.mentions.select_related("content").order_by( + "-created_at" + ) + return { + "id": int(entity.pk), + "project": entity.project_id, + "name": entity.name, + "type": entity.type, + "description": entity.description, + "authority_score": entity.authority_score, + "website_url": entity.website_url, + "github_url": entity.github_url, + "linkedin_url": entity.linkedin_url, + "bluesky_handle": entity.bluesky_handle, + "mastodon_handle": entity.mastodon_handle, + "twitter_handle": entity.twitter_handle, + "identity_claims": [ + { + "id": int(claim.pk), + "surface": claim.surface, + "claim_url": claim.claim_url, + "verified": claim.verified, + "verified_at": claim.verified_at, + "verification_method": claim.verification_method, + } + for claim in entity.identity_claims.all() + ], + "mention_count": getattr(entity, "mention_count", entity.mentions.count()), + "latest_mentions": [ + _serialize_entity_mention(mention) for mention in prefetched_mentions[:3] + ], + "created_at": entity.created_at, + } + + +def _serialize_entity_mention(mention: EntityMention) -> dict[str, Any]: + """Return one entity mention response body.""" + + return { + "id": int(mention.pk), + "content_id": mention.content_id, + "content_title": mention.content.title, + "role": mention.role, + "sentiment": mention.sentiment, + "span": mention.span, + "confidence": mention.confidence, + "created_at": mention.created_at, + } + + +def _serialize_authority_snapshot(snapshot: EntityAuthoritySnapshot) -> dict[str, Any]: + """Return one entity authority snapshot response body.""" + + return { + "id": int(snapshot.pk), + "entity": snapshot.entity_id, + "project": snapshot.project_id, + "computed_at": snapshot.computed_at, + "mention_component": snapshot.mention_component, + "engagement_component": snapshot.engagement_component, + "recency_component": snapshot.recency_component, + "source_quality_component": snapshot.source_quality_component, + "cross_newsletter_component": snapshot.cross_newsletter_component, + "feedback_component": snapshot.feedback_component, + "duplicate_component": snapshot.duplicate_component, + "decayed_prior": snapshot.decayed_prior, + "weights_at_compute": snapshot.weights_at_compute, + "final_score": snapshot.final_score, + } + + +def _serialize_entity_candidate(candidate: EntityCandidate) -> dict[str, Any]: + """Return one entity candidate response body.""" + + evidence = getattr(candidate, "prefetched_evidence", None) + if evidence is None: + evidence = candidate.evidence.all() + source_plugins = sorted( + { + evidence_row.source_plugin + for evidence_row in evidence + if evidence_row.source_plugin + } + ) + identity_surfaces = sorted( + { + evidence_row.identity_surface + for evidence_row in evidence + if evidence_row.identity_surface + } + ) + return { + "id": int(candidate.pk), + "project": candidate.project_id, + "name": candidate.name, + "suggested_type": candidate.suggested_type, + "first_seen_in": candidate.first_seen_in_id, + "first_seen_title": ( + candidate.first_seen_in.title if candidate.first_seen_in else None + ), + "occurrence_count": candidate.occurrence_count, + "cluster_key": candidate.cluster_key, + "auto_promotion_blocked_reason": candidate.auto_promotion_blocked_reason, + "evidence_count": len(evidence), + "source_plugin_count": len(source_plugins), + "source_plugins": source_plugins, + "identity_surfaces": identity_surfaces, + "status": candidate.status, + "merged_into": candidate.merged_into_id, + "merged_into_name": ( + candidate.merged_into.name if candidate.merged_into else None + ), + "created_at": candidate.created_at, + "updated_at": candidate.updated_at, + } + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _error_payload(field: str, message: str) -> dict[str, list[str]]: + """Return the native Ninja error payload shape.""" + + return {field: [message]} + + +def _validated_entity_payload( + payload: dict[str, Any], + *, + project, + instance: Entity | None = None, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate one entity payload.""" + + validated_payload = dict(payload) + validated_payload.pop("project", None) + + entity = instance or Entity(project=project) + for field_name, value in validated_payload.items(): + setattr(entity, field_name, value) + entity.project = project + try: + entity.full_clean() + except ValidationError as exc: + return validated_payload, _validation_error_payload(exc) + return validated_payload, None + + +def _validated_history_limit( + limit: str | None, +) -> tuple[int | None, dict[str, list[str]] | None]: + """Validate the optional entity authority-history limit.""" + + if limit is None: + return None, None + try: + parsed_limit = int(limit) + except ValueError: + return None, _error_payload( + "limit", "Limit must be an integer between 1 and 100." + ) + if parsed_limit < 1 or parsed_limit > 100: + return None, _error_payload( + "limit", "Limit must be an integer between 1 and 100." + ) + return parsed_limit, None + + +def _validated_merge_target( + merged_into_id: int, + *, + project_id: int, +) -> tuple[Entity | None, dict[str, list[str]] | None]: + """Resolve and validate one candidate-merge target inside the same project.""" + + merged_into = Entity.objects.filter( + pk=merged_into_id, project_id=project_id + ).first() + if merged_into is None: + return None, _error_payload("merged_into", "Select a valid choice.") + return merged_into, None + + +def _get_entity_or_404(project_id: int, entity_id: int) -> Entity: + """Load one entity for the selected project.""" + + entity = _entity_queryset().filter(project_id=project_id, pk=entity_id).first() + if not entity: + raise HttpError(404, "Not found.") + return entity + + +def _get_entity_candidate_or_404(project_id: int, candidate_id: int) -> EntityCandidate: + """Load one entity candidate for the selected project.""" + + candidate = ( + _entity_candidate_queryset() + .filter( + project_id=project_id, + pk=candidate_id, + ) + .first() + ) + if not candidate: + raise HttpError(404, "Not found.") + return candidate + + +def _apply_entity_ordering( + queryset: QuerySet[Entity], ordering: str | None +) -> QuerySet[Entity]: + """Apply supported ordering fields to the entity queryset.""" + + if not ordering: + return queryset.order_by("name") + field_name = ordering.lstrip("-") + if field_name not in ENTITY_ORDERING_FIELDS: + return queryset.order_by("name") + return queryset.order_by(ordering) + + +@entity_router.get("/", response=list[EntitySchema], auth=api_authenticate) +def list_entities( + request: Any, + project_id: int = Path(...), + ordering: str | None = Query(None), +): + """List tracked entities visible to the current project member.""" + + _get_project_or_404(request, project_id) + entities = _apply_entity_ordering( + _entity_queryset().filter(project_id=project_id), + ordering, + ) + return [_serialize_entity(entity) for entity in entities] + + +@entity_router.post( + "/", + response={201: EntitySchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_entity( + request: Any, + payload: EntityCreateInput, + project_id: int = Path(...), +): + """Create one tracked entity under the selected project.""" + + project = _require_project_writable(request, project_id) + validated_payload, errors = _validated_entity_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=project, + ) + if errors is not None: + return Status(400, errors) + entity = Entity.objects.create(project=project, **validated_payload) + return Status( + 201, _serialize_entity(_get_entity_or_404(project_id, int(entity.pk))) + ) + + +@entity_router.get("/{entity_id}/", response=EntitySchema, auth=api_authenticate) +def get_entity( + request: Any, + project_id: int = Path(...), + entity_id: int = Path(...), +): + """Return one tracked entity.""" + + _get_project_or_404(request, project_id) + return _serialize_entity(_get_entity_or_404(project_id, entity_id)) + + +@entity_router.patch( + "/{entity_id}/", + response={200: EntitySchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_entity( + request: Any, + payload: EntityUpdateInput, + project_id: int = Path(...), + entity_id: int = Path(...), +): + """Update one tracked entity.""" + + _require_project_writable(request, project_id) + entity = _get_entity_or_404(project_id, entity_id) + validated_payload, errors = _validated_entity_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=entity.project, + instance=entity, + ) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(entity, field_name, value) + entity.save() + return _serialize_entity(_get_entity_or_404(project_id, entity_id)) + + +@entity_router.delete("/{entity_id}/", response={204: None}, auth=api_authenticate) +def delete_entity( + request: Any, + project_id: int = Path(...), + entity_id: int = Path(...), +): + """Delete one tracked entity.""" + + _require_project_admin(request, project_id) + entity = _get_entity_or_404(project_id, entity_id) + entity.delete() + return Status(204, None) + + +@entity_router.get( + "/{entity_id}/mentions/", + response=list[EntityMentionSummarySchema], + auth=api_authenticate, +) +def entity_mentions( + request: Any, + project_id: int = Path(...), + entity_id: int = Path(...), +): + """Return extracted mention history for one tracked entity.""" + + _get_project_or_404(request, project_id) + entity = _get_entity_or_404(project_id, entity_id) + mentions = entity.mentions.select_related("content").order_by("-created_at") + return [_serialize_entity_mention(mention) for mention in mentions] + + +@entity_router.get( + "/{entity_id}/authority_components/", + response=EntityAuthoritySnapshotSchema, + auth=api_authenticate, +) +def entity_authority_components( + request: Any, + project_id: int = Path(...), + entity_id: int = Path(...), +): + """Return the latest authority snapshot for one tracked entity.""" + + _get_project_or_404(request, project_id) + entity = _get_entity_or_404(project_id, entity_id) + snapshot = entity.authority_snapshots.order_by("-computed_at").first() + if snapshot is None: + raise HttpError(404, "No authority snapshots exist for this entity yet.") + return _serialize_authority_snapshot(snapshot) + + +@entity_router.get( + "/{entity_id}/authority_history/", + response={200: list[EntityAuthoritySnapshotSchema], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def entity_authority_history( + request: Any, + project_id: int = Path(...), + entity_id: int = Path(...), + limit: str | None = Query(None), +): + """Return recent authority snapshots for one tracked entity.""" + + _get_project_or_404(request, project_id) + entity = _get_entity_or_404(project_id, entity_id) + snapshots = entity.authority_snapshots.order_by("-computed_at") + parsed_limit, limit_errors = _validated_history_limit(limit) + if limit_errors is not None: + return Status(400, limit_errors) + if parsed_limit is not None: + snapshots = snapshots[:parsed_limit] + return [_serialize_authority_snapshot(snapshot) for snapshot in snapshots] + + +@entity_candidate_router.get( + "/", + response=list[EntityCandidateSchema], + auth=api_authenticate, +) +def list_entity_candidates(request: Any, project_id: int = Path(...)): + """List entity candidates visible to the current project member.""" + + _get_project_or_404(request, project_id) + candidates = _entity_candidate_queryset().filter(project_id=project_id) + return [_serialize_entity_candidate(candidate) for candidate in candidates] + + +@entity_candidate_router.get( + "/{candidate_id}/", + response=EntityCandidateSchema, + auth=api_authenticate, +) +def get_entity_candidate( + request: Any, + project_id: int = Path(...), + candidate_id: int = Path(...), +): + """Return one entity candidate.""" + + _get_project_or_404(request, project_id) + return _serialize_entity_candidate( + _get_entity_candidate_or_404(project_id, candidate_id) + ) + + +@entity_candidate_router.post( + "/{candidate_id}/accept/", + response=EntityCandidateSchema, + auth=api_authenticate, +) +def accept_entity_candidate_route( + request: Any, + project_id: int = Path(...), + candidate_id: int = Path(...), +): + """Accept one entity candidate and return its updated representation.""" + + _require_project_writable(request, project_id) + candidate = _get_entity_candidate_or_404(project_id, candidate_id) + accept_entity_candidate(candidate, schedule_enrichment=True) + candidate.refresh_from_db() + return _serialize_entity_candidate( + _get_entity_candidate_or_404(project_id, candidate_id) + ) + + +@entity_candidate_router.post( + "/{candidate_id}/reject/", + response=EntityCandidateSchema, + auth=api_authenticate, +) +def reject_entity_candidate_route( + request: Any, + project_id: int = Path(...), + candidate_id: int = Path(...), +): + """Reject one entity candidate and return its updated representation.""" + + _require_project_writable(request, project_id) + candidate = _get_entity_candidate_or_404(project_id, candidate_id) + reject_entity_candidate(candidate) + candidate.refresh_from_db() + return _serialize_entity_candidate( + _get_entity_candidate_or_404(project_id, candidate_id) + ) + + +@entity_candidate_router.post( + "/{candidate_id}/merge/", + response={200: EntityCandidateSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def merge_entity_candidate_route( + request: Any, + payload: EntityCandidateMergeInput, + project_id: int = Path(...), + candidate_id: int = Path(...), +): + """Merge one entity candidate into an existing tracked entity.""" + + _require_project_writable(request, project_id) + candidate = _get_entity_candidate_or_404(project_id, candidate_id) + merged_into, errors = _validated_merge_target( + payload.merged_into, + project_id=candidate.project_id, + ) + if errors is not None: + return Status(400, errors) + merge_entity_candidate( + candidate, + merged_into, + schedule_enrichment=True, + ) + candidate.refresh_from_db() + return _serialize_entity_candidate( + _get_entity_candidate_or_404(project_id, candidate_id) + ) + + +__all__ = ["entity_router", "entity_candidate_router"] diff --git a/entities/serializers.py b/entities/serializers.py deleted file mode 100644 index 96a7226a..00000000 --- a/entities/serializers.py +++ /dev/null @@ -1,198 +0,0 @@ -"""DRF serializers for entity-domain models.""" - -from rest_framework import serializers - -from core.serializer_mixins import ProjectScopedSerializerMixin -from entities.models import ( - Entity, - EntityAuthoritySnapshot, - EntityCandidate, - EntityIdentityClaim, - EntityMention, -) - - -class EntityIdentityClaimSerializer(serializers.ModelSerializer): - """Serialize one verified external identity claim for a tracked entity.""" - - class Meta: - model = EntityIdentityClaim - fields = [ - "id", - "surface", - "claim_url", - "verified", - "verified_at", - "verification_method", - ] - read_only_fields = fields - - -class EntitySerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize tracked entities for a project.""" - - identity_claims = EntityIdentityClaimSerializer(many=True, read_only=True) - mention_count = serializers.IntegerField(read_only=True) - latest_mentions = serializers.SerializerMethodField() - - class Meta: - model = Entity - fields = [ - "id", - "project", - "name", - "type", - "description", - "authority_score", - "website_url", - "github_url", - "linkedin_url", - "bluesky_handle", - "mastodon_handle", - "twitter_handle", - "identity_claims", - "mention_count", - "latest_mentions", - "created_at", - ] - read_only_fields = ["id", "project", "created_at"] - - def get_latest_mentions(self, obj): - """Return a compact summary of the most recent mentions for an entity.""" - - mentions = getattr(obj, "prefetched_mentions", None) - if mentions is None: - mentions = obj.mentions.select_related("content").order_by("-created_at") - return EntityMentionSummarySerializer(mentions[:3], many=True).data - - -class EntityAuthoritySnapshotSerializer(serializers.ModelSerializer): - """Serialize one persisted authority recomputation for an entity.""" - - class Meta: - model = EntityAuthoritySnapshot - fields = [ - "id", - "entity", - "project", - "computed_at", - "mention_component", - "engagement_component", - "recency_component", - "source_quality_component", - "cross_newsletter_component", - "feedback_component", - "duplicate_component", - "decayed_prior", - "weights_at_compute", - "final_score", - ] - read_only_fields = fields - - -class EntityMentionSummarySerializer(serializers.ModelSerializer): - """Serialize a compact entity-mention summary for frontend display.""" - - content_id = serializers.IntegerField(read_only=True) - content_title = serializers.CharField(source="content.title", read_only=True) - - class Meta: - model = EntityMention - fields = [ - "id", - "content_id", - "content_title", - "role", - "sentiment", - "span", - "confidence", - "created_at", - ] - read_only_fields = fields - - -class EntityCandidateSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize extracted entity candidates awaiting editorial review.""" - - first_seen_title = serializers.CharField( - source="first_seen_in.title", read_only=True - ) - merged_into_name = serializers.CharField(source="merged_into.name", read_only=True) - evidence_count = serializers.SerializerMethodField() - source_plugin_count = serializers.SerializerMethodField() - source_plugins = serializers.SerializerMethodField() - identity_surfaces = serializers.SerializerMethodField() - - class Meta: - model = EntityCandidate - fields = [ - "id", - "project", - "name", - "suggested_type", - "first_seen_in", - "first_seen_title", - "occurrence_count", - "cluster_key", - "auto_promotion_blocked_reason", - "evidence_count", - "source_plugin_count", - "source_plugins", - "identity_surfaces", - "status", - "merged_into", - "merged_into_name", - "created_at", - "updated_at", - ] - read_only_fields = fields - - def _candidate_evidence(self, obj): - """Return prefetched evidence rows when available.""" - - evidence = getattr(obj, "prefetched_evidence", None) - if evidence is None: - evidence = obj.evidence.all() - return evidence - - def get_evidence_count(self, obj) -> int: - """Return the number of evidence rows attached to the candidate.""" - - return len(self._candidate_evidence(obj)) - - def get_source_plugin_count(self, obj) -> int: - """Return the number of unique source plugins backing the candidate.""" - - return len(self.get_source_plugins(obj)) - - def get_source_plugins(self, obj) -> list[str]: - """Return the unique source plugins seen in candidate evidence.""" - - return sorted( - { - evidence.source_plugin - for evidence in self._candidate_evidence(obj) - if evidence.source_plugin - } - ) - - def get_identity_surfaces(self, obj) -> list[str]: - """Return the unique identity surfaces hinted by candidate evidence.""" - - return sorted( - { - evidence.identity_surface - for evidence in self._candidate_evidence(obj) - if evidence.identity_surface - } - ) - - -class EntityCandidateMergeSerializer( - ProjectScopedSerializerMixin, serializers.Serializer -): - """Validate merge requests for entity candidates.""" - - merged_into = serializers.PrimaryKeyRelatedField(queryset=Entity.objects.none()) diff --git a/entities/tasks.py b/entities/tasks.py index d66d0ba2..3e53eba2 100644 --- a/entities/tasks.py +++ b/entities/tasks.py @@ -1,4 +1,4 @@ -"""Celery tasks and helpers for automated entity discovery.""" +"""Background tasks and helpers for automated entity discovery.""" from __future__ import annotations @@ -11,13 +11,12 @@ from hashlib import sha1 from itertools import combinations from math import sqrt -from typing import Any, Protocol, cast +from typing import Any from urllib.parse import urlsplit from uuid import NAMESPACE_URL, uuid5 import httpx from atproto import Client -from celery import shared_task from django.conf import settings from django.db.models import Model from django.utils import timezone @@ -27,6 +26,7 @@ from core.embeddings import build_entity_embedding_text, embed_text from core.llm import openrouter_chat_json +from digest_engine.taskiq import broker, enqueue_task, task_always_eager from entities.extraction import ( _normalize_name, accept_entity_candidate, @@ -79,22 +79,6 @@ class IdentityProbeResult: field_updates: dict[str, str] -class DelayedTask(Protocol): - """Protocol for Celery tasks that can run eagerly or via ``delay``.""" - - def __call__(self, *args: object, **kwargs: object) -> object: - pass - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - -def _enqueue_task(task: object, *args: object) -> None: - """Dispatch a Celery task through a typed ``delay`` seam.""" - - cast(DelayedTask, task).delay(*args) - - def _require_pk(instance: Model) -> int: """Return a saved model primary key as an ``int``.""" @@ -104,33 +88,33 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -@shared_task(name="entities.tasks.run_all_entity_candidate_clustering") +@broker.task(task_name="entities.tasks.run_all_entity_candidate_clustering") def run_all_entity_candidate_clustering() -> int: """Queue candidate clustering for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): cluster_entity_candidates(project_id) else: - _enqueue_task(cluster_entity_candidates, project_id) + enqueue_task(cluster_entity_candidates, project_id) return len(project_ids) -@shared_task(name="entities.tasks.run_all_entity_candidate_auto_promotions") +@broker.task(task_name="entities.tasks.run_all_entity_candidate_auto_promotions") def run_all_entity_candidate_auto_promotions() -> int: """Queue automated candidate promotion checks for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): auto_promote_entity_candidates(project_id) else: - _enqueue_task(auto_promote_entity_candidates, project_id) + enqueue_task(auto_promote_entity_candidates, project_id) return len(project_ids) -@shared_task(name="entities.tasks.cluster_entity_candidates") +@broker.task(task_name="entities.tasks.cluster_entity_candidates") def cluster_entity_candidates(project_id: int) -> dict[str, int]: """Assign stable cluster keys to pending candidates inside one project.""" @@ -183,7 +167,7 @@ def cluster_entity_candidates(project_id: int) -> dict[str, int]: } -@shared_task(name="entities.tasks.auto_promote_entity_candidates") +@broker.task(task_name="entities.tasks.auto_promote_entity_candidates") def auto_promote_entity_candidates(project_id: int) -> dict[str, int]: """Promote or merge pending candidates that meet the WP2 thresholds.""" @@ -245,7 +229,7 @@ def auto_promote_entity_candidates(project_id: int) -> dict[str, int]: } -@shared_task(name="entities.tasks.enrich_entity_identity") +@broker.task(task_name="entities.tasks.enrich_entity_identity") def enrich_entity_identity(entity_id: int) -> dict[str, Any]: """Project verified identity claims back onto the tracked entity fields.""" diff --git a/entities/tests/test_extraction.py b/entities/tests/test_extraction.py index 250a1d25..4a22dd99 100644 --- a/entities/tests/test_extraction.py +++ b/entities/tests/test_extraction.py @@ -1,10 +1,11 @@ from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from django.utils import timezone from content.models import Content -from entities.extraction import run_entity_extraction +from entities.extraction import queue_entity_identity_enrichment, run_entity_extraction from entities.models import EntityCandidate, EntityCandidateEvidence from projects.model_support import SourcePluginName from projects.models import Project @@ -60,3 +61,37 @@ def test_run_entity_extraction_persists_linkedin_candidate_identity_evidence( assert evidence.identity_surface == "linkedin" assert evidence.claim_url == "https://www.linkedin.com/company/river-labs" assert "River Labs" in evidence.context_excerpt + + +def test_queue_entity_identity_enrichment_enqueues_task_when_not_eager( + extraction_context, + mocker, + settings, +): + settings.TASKIQ_ALWAYS_EAGER = False + queue_mock = mocker.patch( + "entities.tasks.enrich_entity_identity.kiq", + new_callable=AsyncMock, + ) + + queue_entity_identity_enrichment(123) + + queue_mock.assert_awaited_once_with(123) + + +def test_queue_entity_identity_enrichment_executes_inline_when_eager( + extraction_context, + mocker, + settings, +): + settings.TASKIQ_ALWAYS_EAGER = True + enrich_mock = mocker.patch("entities.tasks.enrich_entity_identity") + queue_mock = mocker.patch( + "entities.tasks.enrich_entity_identity.kiq", + new_callable=AsyncMock, + ) + + queue_entity_identity_enrichment(123) + + enrich_mock.assert_called_once_with(123) + queue_mock.assert_not_awaited() diff --git a/entities/tests/test_api.py b/entities/tests/test_ninja_api.py similarity index 55% rename from entities/tests/test_api.py rename to entities/tests/test_ninja_api.py index e18ecbe9..ec9ee946 100644 --- a/entities/tests/test_api.py +++ b/entities/tests/test_ninja_api.py @@ -1,11 +1,13 @@ +from __future__ import annotations + +from http import HTTPStatus from typing import Any, cast from unittest.mock import patch from django.contrib.auth import get_user_model from django.db.models import Model +from django.test import TestCase from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase from content.models import Content from entities.models import ( @@ -29,26 +31,28 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - def _create_user(user_model: type[Any], **kwargs: object): """Create a user through the custom manager with a typed escape hatch.""" return cast(Any, user_model.objects).create_user(**kwargs) -class EntityApiTests(APITestCase): - """Exercise entity-owned project API endpoints.""" +class EntityNinjaApiTests(TestCase): + """Exercise entity-owned Ninja API endpoints.""" def setUp(self): user_model = get_user_model() self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member = _create_user( + user_model, username="member", password="testpass123" + ) + self.reader = _create_user( + user_model, username="reader", password="testpass123" + ) self.other_user = _create_user( - user_model, username="other", password="testpass123" + user_model, + username="other", + password="testpass123", ) self.owner_project = Project.objects.create( name="Owner Project", @@ -63,6 +67,16 @@ def setUp(self): project=self.owner_project, role=ProjectRole.ADMIN, ) + ProjectMembership.objects.create( + user=self.member, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) + ProjectMembership.objects.create( + user=self.reader, + project=self.owner_project, + role=ProjectRole.READER, + ) ProjectMembership.objects.create( user=self.other_user, project=self.other_project, @@ -98,30 +112,21 @@ def setUp(self): published_date="2026-04-21T00:00:00Z", content_text="Other content text", ) - _typed_client(self.client).force_authenticate(self.owner) - - def assert_standardized_validation_error( - self, payload: dict[str, object], attr: str - ): - """Assert the repo-standardized validation payload shape.""" - - self.assertEqual(payload["type"], "validation_error") - errors = cast(list[dict[str, object]], payload["errors"]) - self.assertTrue(any(error["attr"] == attr for error in errors)) + self.client.force_login(self.owner) def test_entity_list_is_scoped_to_request_user_project(self): response = self.client.get( reverse( - "v1:project-entity-list", + "ninja-api:list_entities", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(len(response.json()), 1) self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_entity)) - def test_entity_list_includes_recent_mentions(self): + def test_entity_list_includes_recent_mentions_and_identity_claims(self): mention = EntityMention.objects.create( project=self.owner_project, content=self.owner_content, @@ -131,25 +136,6 @@ def test_entity_list_includes_recent_mentions(self): span="Owner Entity", confidence=0.88, ) - - response = self.client.get( - reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()[0]["mention_count"], 1) - self.assertEqual( - response.json()[0]["latest_mentions"][0]["id"], _require_pk(mention) - ) - self.assertEqual( - response.json()[0]["latest_mentions"][0]["content_title"], - self.owner_content.title, - ) - - def test_entity_list_includes_identity_claims(self): EntityIdentityClaim.objects.create( entity=self.owner_entity, surface="linkedin", @@ -160,14 +146,20 @@ def test_entity_list_includes_identity_claims(self): response = self.client.get( reverse( - "v1:project-entity-list", + "ninja-api:list_entities", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(response.json()[0]["mention_count"], 1) + self.assertEqual( + response.json()[0]["latest_mentions"][0]["id"], + _require_pk(mention), + ) self.assertEqual( - response.json()[0]["identity_claims"][0]["surface"], "linkedin" + response.json()[0]["identity_claims"][0]["surface"], + "linkedin", ) def test_entity_mentions_action_returns_full_mention_history(self): @@ -202,19 +194,18 @@ def test_entity_mentions_action_returns_full_mention_history(self): response = self.client.get( reverse( - "v1:project-entity-mentions", + "ninja-api:entity_mentions", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), + "entity_id": _require_pk(self.owner_entity), }, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(len(response.json()), 2) self.assertEqual(response.json()[0]["id"], _require_pk(second_mention)) self.assertEqual(response.json()[1]["id"], _require_pk(first_mention)) - self.assertEqual(response.json()[0]["content_title"], second_content.title) def test_entity_list_supports_authority_score_ordering(self): second_entity = Entity.objects.create( @@ -228,18 +219,17 @@ def test_entity_list_supports_authority_score_ordering(self): response = self.client.get( reverse( - "v1:project-entity-list", + "ninja-api:list_entities", kwargs={"project_id": _require_pk(self.owner_project)}, ), {"ordering": "-authority_score"}, ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.json()[0]["id"], _require_pk(second_entity)) - self.assertEqual(response.json()[1]["id"], _require_pk(self.owner_entity)) - def test_entity_authority_history_action_returns_recent_snapshots(self): - first_snapshot = EntityAuthoritySnapshot.objects.create( + def test_entity_authority_actions_return_snapshots(self): + EntityAuthoritySnapshot.objects.create( entity=self.owner_entity, project=self.owner_project, mention_component=0.6, @@ -264,68 +254,90 @@ def test_entity_authority_history_action_returns_recent_snapshots(self): feedback_component=0.7, duplicate_component=0.6, decayed_prior=0.53, - weights_at_compute={"mention": 0.2}, + weights_at_compute={"engagement": 0.15}, final_score=0.66, ) - response = self.client.get( + components_response = self.client.get( reverse( - "v1:project-entity-authority-history", + "ninja-api:entity_authority_components", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), + "entity_id": _require_pk(self.owner_entity), + }, + ) + ) + history_response = self.client.get( + reverse( + "ninja-api:entity_authority_history", + kwargs={ + "project_id": _require_pk(self.owner_project), + "entity_id": _require_pk(self.owner_entity), }, ), {"limit": 1}, ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(second_snapshot)) - self.assertNotEqual(response.json()[0]["id"], _require_pk(first_snapshot)) - self.assertEqual(response.json()[0]["engagement_component"], 0.65) - - def test_entity_authority_components_action_returns_latest_snapshot(self): - EntityAuthoritySnapshot.objects.create( - entity=self.owner_entity, - project=self.owner_project, - mention_component=0.7, - engagement_component=0.5, - recency_component=0.6, - source_quality_component=0.55, - cross_newsletter_component=0.4, - feedback_component=0.65, - duplicate_component=0.3, - decayed_prior=0.45, - weights_at_compute={"engagement": 0.15}, - final_score=0.62, + self.assertEqual(components_response.status_code, HTTPStatus.OK) + self.assertEqual( + components_response.json()["weights_at_compute"]["engagement"], + 0.15, ) + self.assertEqual(history_response.status_code, HTTPStatus.OK) + self.assertEqual(len(history_response.json()), 1) + self.assertEqual(history_response.json()[0]["id"], _require_pk(second_snapshot)) + def test_entity_authority_history_rejects_invalid_limit(self): response = self.client.get( reverse( - "v1:project-entity-authority-components", + "ninja-api:entity_authority_history", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(self.owner_entity), + "entity_id": _require_pk(self.owner_entity), }, - ) + ), + {"limit": "abc"}, ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["source_quality_component"], 0.55) - self.assertEqual(response.json()["weights_at_compute"]["engagement"], 0.15) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["limit"][0], + "Limit must be an integer between 1 and 100.", + ) - def test_nested_entity_list_rejects_other_users_project(self): - response = self.client.get( + def test_entity_create_uses_project_from_url(self): + response = self.client.post( reverse( - "v1:project-entity-list", - kwargs={"project_id": _require_pk(self.other_project)}, + "ninja-api:create_entity", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "name": "Created Entity", + "type": "organization", + "project": _require_pk(self.other_project), + }, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.CREATED) + created_entity = Entity.objects.get(name="Created Entity") + self.assertEqual(created_entity.project, self.owner_project) + + def test_reader_cannot_delete_entity(self): + self.client.force_login(self.reader) + response = self.client.delete( + reverse( + "ninja-api:delete_entity", + kwargs={ + "project_id": _require_pk(self.owner_project), + "entity_id": _require_pk(self.owner_entity), + }, ) ) - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) - def test_entity_candidate_list_is_scoped_to_request_user_project(self): + def test_entity_candidate_list_and_actions(self): owner_candidate = EntityCandidate.objects.create( project=self.owner_project, name="Owner Candidate", @@ -343,129 +355,66 @@ def test_entity_candidate_list_is_scoped_to_request_user_project(self): identity_surface="linkedin", claim_url="https://www.linkedin.com/company/owner-candidate", ) - EntityCandidate.objects.create( - project=self.other_project, - name="Other Candidate", - suggested_type="organization", - first_seen_in=self.other_content, - ) - response = self.client.get( + list_response = self.client.get( reverse( - "v1:project-entity-candidate-list", + "ninja-api:list_entity_candidates", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(owner_candidate)) - self.assertEqual(response.json()[0]["cluster_key"], owner_candidate.cluster_key) - self.assertEqual( - response.json()[0]["auto_promotion_blocked_reason"], - owner_candidate.auto_promotion_blocked_reason, - ) - self.assertEqual(response.json()[0]["evidence_count"], 1) - self.assertEqual(response.json()[0]["source_plugin_count"], 1) - self.assertEqual(response.json()[0]["source_plugins"], ["linkedin"]) - self.assertEqual(response.json()[0]["identity_surfaces"], ["linkedin"]) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(list_response.json()[0]["id"], _require_pk(owner_candidate)) + self.assertEqual(list_response.json()[0]["source_plugins"], ["linkedin"]) - def test_entity_candidate_accept_action_returns_updated_candidate(self): with patch("entities.extraction.queue_entity_identity_enrichment"): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="River Labs", - suggested_type="vendor", - first_seen_in=self.owner_content, + accept_response = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/entity-candidates/{_require_pk(owner_candidate)}/accept/", ) - response = self.client.post( - reverse( - "v1:project-entity-candidate-accept", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - format="json", - ) - - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.ACCEPTED) - self.assertIsNotNone(candidate.merged_into) - self.assertEqual(response.json()["status"], EntityCandidateStatus.ACCEPTED) + owner_candidate.refresh_from_db() + self.assertEqual(accept_response.status_code, HTTPStatus.OK) + self.assertEqual(owner_candidate.status, EntityCandidateStatus.ACCEPTED) + self.assertEqual( + accept_response.json()["status"], EntityCandidateStatus.ACCEPTED + ) - def test_entity_candidate_reject_action_returns_updated_candidate(self): - candidate = EntityCandidate.objects.create( + def test_entity_candidate_reject_and_merge(self): + reject_candidate = EntityCandidate.objects.create( project=self.owner_project, name="Rejected Candidate", suggested_type="organization", first_seen_in=self.owner_content, ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-reject", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), - format="json", + reject_response = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/entity-candidates/{_require_pk(reject_candidate)}/reject/", ) + reject_candidate.refresh_from_db() + self.assertEqual(reject_response.status_code, HTTPStatus.OK) + self.assertEqual(reject_candidate.status, EntityCandidateStatus.REJECTED) - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.REJECTED) - self.assertEqual(response.json()["status"], EntityCandidateStatus.REJECTED) - - def test_entity_candidate_merge_rejects_cross_project_entity(self): - candidate = EntityCandidate.objects.create( + merge_candidate = EntityCandidate.objects.create( project=self.owner_project, name="Merge Candidate", suggested_type="vendor", first_seen_in=self.owner_content, ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-merge", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), + invalid_merge = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/entity-candidates/{_require_pk(merge_candidate)}/merge/", {"merged_into": _require_pk(self.other_entity)}, - format="json", + content_type="application/json", ) + self.assertEqual(invalid_merge.status_code, HTTPStatus.BAD_REQUEST) + self.assertIn("merged_into", invalid_merge.json()) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "merged_into") - - def test_entity_candidate_merge_action_returns_updated_candidate(self): with patch("entities.extraction.queue_entity_identity_enrichment"): - candidate = EntityCandidate.objects.create( - project=self.owner_project, - name="Owner Entity Alias", - suggested_type="vendor", - first_seen_in=self.owner_content, - ) - - response = self.client.post( - reverse( - "v1:project-entity-candidate-merge", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(candidate), - }, - ), + valid_merge = self.client.post( + f"/api/v1/projects/{_require_pk(self.owner_project)}/entity-candidates/{_require_pk(merge_candidate)}/merge/", {"merged_into": _require_pk(self.owner_entity)}, - format="json", + content_type="application/json", ) - candidate.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(candidate.status, EntityCandidateStatus.MERGED) - self.assertEqual(candidate.merged_into, self.owner_entity) - self.assertEqual(response.json()["merged_into"], _require_pk(self.owner_entity)) + merge_candidate.refresh_from_db() + self.assertEqual(valid_merge.status_code, HTTPStatus.OK) + self.assertEqual(merge_candidate.status, EntityCandidateStatus.MERGED) + self.assertEqual(merge_candidate.merged_into, self.owner_entity) diff --git a/entities/tests/test_tasks.py b/entities/tests/test_tasks.py index ed202cc3..aeb62109 100644 --- a/entities/tests/test_tasks.py +++ b/entities/tests/test_tasks.py @@ -1,4 +1,5 @@ from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from django.utils import timezone @@ -16,6 +17,8 @@ auto_promote_entity_candidates, cluster_entity_candidates, enrich_entity_identity, + run_all_entity_candidate_auto_promotions, + run_all_entity_candidate_clustering, ) from projects.model_support import SourcePluginName from projects.models import Project @@ -43,6 +46,13 @@ def _content(project: Project, *, suffix: str, source_plugin: str) -> Content: ) +def _require_pk(instance) -> int: + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + def test_cluster_entity_candidates_groups_similar_candidates( entity_discovery_context, mocker, @@ -86,12 +96,60 @@ def test_cluster_entity_candidates_groups_similar_candidates( assert first_candidate.contextual_embedding_id is not None +def test_run_all_entity_candidate_clustering_enqueues_all_projects( + entity_discovery_context, + settings, + mocker, +): + settings.TASKIQ_ALWAYS_EAGER = False + queue_mock = mocker.patch( + "entities.tasks.cluster_entity_candidates.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Other Discovery Project", + topic_description="Security", + ) + + enqueued_count = run_all_entity_candidate_clustering() + + assert enqueued_count == 2 + queue_mock.assert_any_await(entity_discovery_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 + + +def test_run_all_entity_candidate_clustering_executes_inline_when_eager( + entity_discovery_context, + settings, + mocker, +): + settings.TASKIQ_ALWAYS_EAGER = True + cluster_mock = mocker.patch("entities.tasks.cluster_entity_candidates") + queue_mock = mocker.patch( + "entities.tasks.cluster_entity_candidates.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Inline Discovery Project", + topic_description="Platform", + ) + + enqueued_count = run_all_entity_candidate_clustering() + + assert enqueued_count == 2 + cluster_mock.assert_any_call(entity_discovery_context.project.id) + cluster_mock.assert_any_call(_require_pk(other_project)) + assert cluster_mock.call_count == 2 + queue_mock.assert_not_awaited() + + def test_auto_promote_entity_candidates_promotes_verified_multi_source_candidate( entity_discovery_context, settings, mocker, ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True candidate = EntityCandidate.objects.create( project=entity_discovery_context.project, name="River Labs", @@ -160,6 +218,54 @@ def test_auto_promote_entity_candidates_promotes_verified_multi_source_candidate assert claim.verified is True +def test_run_all_entity_candidate_auto_promotions_enqueues_all_projects( + entity_discovery_context, + settings, + mocker, +): + settings.TASKIQ_ALWAYS_EAGER = False + queue_mock = mocker.patch( + "entities.tasks.auto_promote_entity_candidates.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Other Promotion Project", + topic_description="Signals", + ) + + enqueued_count = run_all_entity_candidate_auto_promotions() + + assert enqueued_count == 2 + queue_mock.assert_any_await(entity_discovery_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 + + +def test_run_all_entity_candidate_auto_promotions_executes_inline_when_eager( + entity_discovery_context, + settings, + mocker, +): + settings.TASKIQ_ALWAYS_EAGER = True + promote_mock = mocker.patch("entities.tasks.auto_promote_entity_candidates") + queue_mock = mocker.patch( + "entities.tasks.auto_promote_entity_candidates.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Inline Promotion Project", + topic_description="Signals", + ) + + enqueued_count = run_all_entity_candidate_auto_promotions() + + assert enqueued_count == 2 + promote_mock.assert_any_call(entity_discovery_context.project.id) + promote_mock.assert_any_call(_require_pk(other_project)) + assert promote_mock.call_count == 2 + queue_mock.assert_not_awaited() + + def test_enrich_entity_identity_uses_github_profile_probe( entity_discovery_context, mocker, diff --git a/fix_script.py b/fix_script.py new file mode 100644 index 00000000..954063d8 --- /dev/null +++ b/fix_script.py @@ -0,0 +1,22 @@ +with open("projects/ninja_api.py", "r") as f: + text = f.read() + +writable_fn = """def _require_project_writable(request: Any, project_id: int) -> Project: + \"\"\"Load one project, requiring admin or member (write) access.\"\"\" + project = _get_project_or_404(request, project_id) + membership = ProjectMembership.objects.filter( + project=project, user=request.user + ).first() + if not membership or membership.role not in {ProjectRole.ADMIN, ProjectRole.MEMBER}: + raise HttpError(403, "You do not have permission to perform this action.") + return project + +""" +text = text.replace( + "def _require_project_admin(request: Any, project_id: int) -> Project:", + writable_fn + + "def _require_project_admin(request: Any, project_id: int) -> Project:", +) + +with open("projects/ninja_api.py", "w") as f: + f.write(text) diff --git a/frontend/src/lib/__tests__/api.test.ts b/frontend/src/lib/__tests__/api.test.ts index 7c693d7b..8ac86bf8 100644 --- a/frontend/src/lib/__tests__/api.test.ts +++ b/frontend/src/lib/__tests__/api.test.ts @@ -59,7 +59,7 @@ describe("api helpers", () => { vi.unstubAllGlobals() }) - it("prefers the backend token key for authenticated API requests", async () => { + it("falls back to basic auth when only a legacy token key is present", async () => { getServerSessionMock.mockResolvedValue({ backendAuth: { key: "abc123" } }) const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: true })) vi.stubGlobal("fetch", fetchMock) @@ -70,7 +70,27 @@ describe("api helpers", () => { expect(fetchMock).toHaveBeenCalledWith( "https://api.example.com/api/v1/projects/", expect.objectContaining({ - headers: expect.objectContaining({ Authorization: "Token abc123" }), + headers: expect.objectContaining({ + Authorization: getExpectedBasicAuthHeader(), + }), + }), + ) + }) + + it("prefers a bearer token when the session exposes both access and a legacy key", async () => { + getServerSessionMock.mockResolvedValue({ + backendAuth: { access: "jwt-token", key: "abc123" }, + }) + const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ ok: true })) + vi.stubGlobal("fetch", fetchMock) + + const { apiFetch } = await import("@/lib/api") + await apiFetch("/api/v1/projects/") + + expect(fetchMock).toHaveBeenCalledWith( + "https://api.example.com/api/v1/projects/", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer jwt-token" }), }), ) }) @@ -109,8 +129,8 @@ describe("api helpers", () => { ) }) - it("retries once with basic auth when a session token has gone stale", async () => { - getServerSessionMock.mockResolvedValue({ backendAuth: { key: "stale-token" } }) + it("retries once with basic auth when a bearer token has gone stale", async () => { + getServerSessionMock.mockResolvedValue({ backendAuth: { access: "stale-jwt" } }) const fetchMock = vi .fn() .mockResolvedValueOnce( @@ -139,7 +159,7 @@ describe("api helpers", () => { 1, "https://api.example.com/api/v1/projects/", expect.objectContaining({ - headers: expect.objectContaining({ Authorization: "Token stale-token" }), + headers: expect.objectContaining({ Authorization: "Bearer stale-jwt" }), }), ) expect(fetchMock).toHaveBeenNthCalledWith( @@ -154,7 +174,7 @@ describe("api helpers", () => { }) it("does not retry with basic auth for non-authentication 403 responses", async () => { - getServerSessionMock.mockResolvedValue({ backendAuth: { key: "active-token" } }) + getServerSessionMock.mockResolvedValue({ backendAuth: { access: "active-jwt" } }) const fetchMock = vi.fn().mockResolvedValue( jsonResponse( { diff --git a/frontend/src/lib/__tests__/auth.test.ts b/frontend/src/lib/__tests__/auth.test.ts index d2f1175d..0bbc4777 100644 --- a/frontend/src/lib/__tests__/auth.test.ts +++ b/frontend/src/lib/__tests__/auth.test.ts @@ -85,7 +85,7 @@ describe("authOptions", () => { it("authorizes credentials against the backend auth endpoint", async () => { const fetchMock = vi.fn().mockResolvedValue( - jsonResponse({ key: "abc123", access: "jwt" }), + jsonResponse({ access: "jwt", refresh: "refresh-jwt" }), ) vi.stubGlobal("fetch", fetchMock) @@ -113,7 +113,7 @@ describe("authOptions", () => { expect(user).toEqual({ id: "alice@example.com", name: "alice@example.com", - backendAuth: { key: "abc123", access: "jwt" }, + backendAuth: { access: "jwt", refresh: "refresh-jwt" }, }) }) @@ -179,7 +179,9 @@ describe("authOptions", () => { }) it("prefers the server-side backend base URL for credential auth", async () => { - const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ key: "abc123" })) + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ access: "jwt", refresh: "refresh-jwt" })) vi.stubGlobal("fetch", fetchMock) const { authOptions } = await loadAuthModule() @@ -195,7 +197,9 @@ describe("authOptions", () => { it("falls back to the local backend default when the server-side backend URL is absent", async () => { vi.unstubAllEnvs() - const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ key: "abc123" })) + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ access: "jwt", refresh: "refresh-jwt" })) vi.stubGlobal("fetch", fetchMock) const { authOptions } = await loadAuthModule() @@ -221,7 +225,9 @@ describe("authOptions", () => { }) it("enriches social sign-in users with backend auth when access tokens are present", async () => { - const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ key: "social-key" })) + const fetchMock = vi + .fn() + .mockResolvedValue(jsonResponse({ access: "social-access", refresh: "social-refresh" })) vi.stubGlobal("fetch", fetchMock) const { authOptions } = await loadAuthModule() @@ -236,7 +242,7 @@ describe("authOptions", () => { }) expect(result).toBe(true) - expect(user.backendAuth).toEqual({ key: "social-key" }) + expect(user.backendAuth).toEqual({ access: "social-access", refresh: "social-refresh" }) }) it("returns false when social sign-in has no account", async () => { @@ -319,7 +325,7 @@ describe("authOptions", () => { const token = await authOptions.callbacks?.jwt?.({ token: {}, - user: { backendAuth: { key: "persisted-key" } } as never, + user: { backendAuth: { access: "persisted-jwt", refresh: "persisted-refresh" } } as never, account: null, profile: undefined, trigger: "signIn", @@ -335,8 +341,8 @@ describe("authOptions", () => { trigger: "update", }) - expect(token).toEqual({ backendAuth: { key: "persisted-key" } }) - expect(session).toMatchObject({ backendAuth: { key: "persisted-key" } }) + expect(token).toEqual({ backendAuth: { access: "persisted-jwt", refresh: "persisted-refresh" } }) + expect(session).toMatchObject({ backendAuth: { access: "persisted-jwt", refresh: "persisted-refresh" } }) }) it("leaves jwt and session unchanged when no backend auth is present", async () => { diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5d44b87a..eb6f5a02 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -61,12 +61,11 @@ const API_BASE_URL = process.env.NEWSLETTER_API_BASE_URL ?? "http://127.0.0.1:8080" -type AuthorizationSource = "token" | "bearer" | "basic" +type AuthorizationSource = "bearer" | "basic" type SessionWithBackendAuth = { backendAuth?: { access?: string - key?: string } } @@ -109,9 +108,9 @@ function buildUrl(path: string) { /** * Choose the best available backend authorization mechanism for the request. * - * The preference order is token auth, then bearer auth, then fallback Basic auth. - * This keeps editor sessions aligned with backend auth while still supporting - * local development outside the full auth flow. + * The preference order is bearer auth, then fallback Basic auth. + * This keeps editor sessions aligned with the JWT-backed auth path while still + * supporting local development when no backend auth session is available. * * @returns An Authorization header value accepted by the Django API. * @example @@ -122,13 +121,6 @@ function buildUrl(path: string) { async function getAuthorizationHeader() { const session = (await getServerSession(authOptions)) as SessionWithBackendAuth | null - if (session?.backendAuth?.key) { - return { - authorization: `Token ${session.backendAuth.key}`, - source: "token" as const, - } - } - if (session?.backendAuth?.access) { return { authorization: `Bearer ${session.backendAuth.access}`, diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 8b1a0702..7312c5df 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -15,7 +15,6 @@ import GoogleProvider from "next-auth/providers/google" type BackendAuthPayload = { access?: string detail?: string - key?: string non_field_errors?: string[] refresh?: string user?: Record diff --git a/frontend/src/styles/_theme.css b/frontend/src/styles/_theme.css index af61caf8..18f8a175 100644 --- a/frontend/src/styles/_theme.css +++ b/frontend/src/styles/_theme.css @@ -18,5 +18,3 @@ * utilities in app code instead. Shared type scale overrides now live in * `typography.css`. */ - - diff --git a/frontend/src/styles/focus.css b/frontend/src/styles/focus.css index a0895bb4..d0bc172d 100644 --- a/frontend/src/styles/focus.css +++ b/frontend/src/styles/focus.css @@ -29,4 +29,4 @@ [tabindex='-1']:focus:not(:focus-visible) { outline: none; } -} \ No newline at end of file +} diff --git a/frontend/src/styles/fonts.css b/frontend/src/styles/fonts.css index 43fc8f84..7767dca7 100644 --- a/frontend/src/styles/fonts.css +++ b/frontend/src/styles/fonts.css @@ -469,4 +469,4 @@ src: local("JetBrains Mono Italic"), url("/fonts/JetBrainsMono-Italic-VariableFont_wght.ttf") format("truetype"); -} \ No newline at end of file +} diff --git a/frontend/src/styles/reset.css b/frontend/src/styles/reset.css index 106b0e8c..7fccf25e 100644 --- a/frontend/src/styles/reset.css +++ b/frontend/src/styles/reset.css @@ -89,4 +89,4 @@ th { padding: 0; } -} \ No newline at end of file +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 18f073ad..2c44fb42 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.iterator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.float16.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/global.d.ts","../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/css.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/macro.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/style.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/global.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/get-page-files.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/compatibility/iterators.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/globals.typedarray.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/buffer.buffer.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/globals.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/abortcontroller.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/blob.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/console.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/crypto.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/domexception.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/encoding.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/events.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/utility.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/header.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/readable.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/fetch.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/formdata.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/connector.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/client-stats.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/errors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/dispatcher.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/global-dispatcher.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/global-origin.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/pool-stats.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/handlers.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/balanced-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/round-robin-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/h2c-client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-interceptor.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-call-history.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/snapshot-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-errors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/socks5-proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/env-http-proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/retry-handler.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/retry-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/api.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cache-interceptor.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/interceptors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/util.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cookies.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/patch.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/websocket.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/eventsource.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/diagnostics-channel.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/content-type.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cache.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/index.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/fetch.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/importmeta.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/messaging.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/navigator.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/performance.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/storage.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/streams.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/timers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/url.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/assert.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/assert/strict.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/async_hooks.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/buffer.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/child_process.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/cluster.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/console.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/constants.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/crypto.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dgram.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/diagnostics_channel.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dns.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dns/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/domain.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/events.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/fs.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/fs/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/http.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/http2.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/https.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector.generated.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/module.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/net.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/os.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path/posix.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path/win32.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/perf_hooks.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/process.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/punycode.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/querystring.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/quic.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/readline.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/readline/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/repl.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/sea.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/sqlite.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/consumers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/web.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/string_decoder.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/test.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/test/reporters.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/timers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/timers/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/tls.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/trace_events.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/tty.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/url.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/util.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/util/types.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/v8.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/vm.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/wasi.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/worker_threads.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/zlib.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/canary.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/experimental.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/index.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/canary.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/experimental.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/fallback.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/webpack/webpack.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/entry-constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/bundler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/load-custom-routes.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/body-streams.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/search-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/vary-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-kind.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/route-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/app-router-headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cache-control.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/app-router-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/use-cache-wrapper.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render-result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/static-paths/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/adapter/setup-node-env.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/instrumentation/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/page-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/require-hook.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-baseline.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/page-extensions-type.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/i18n-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/next-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/request.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/mitt.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/with-router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/route-loader.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/page-loader.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/readonly-url-search-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/flight-data-helpers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/scheduler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache-map.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/vary-path.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/pages.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/builtin-request-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/load-default-error-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-life.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/lazy-result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/create-error-handler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/work-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/http.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/hooks-server-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-status-code.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/cache-signal.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-samples.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/implicit-tags.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/staged-rendering.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-route.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/get-supported-browsers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/rendering-mode.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cpu-profile.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/routes/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/coalesced-function.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/trace.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/load-jsconfig.d.ts","../node_modules/.pnpm/@next+env@16.2.6/node_modules/@next/env/dist/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/telemetry/storage.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/build-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/generated-native.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/define-env.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/parse-version-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/shared/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/parse-stack.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/server/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/debug-channel.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/node.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/async-callback-set.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../node_modules/.pnpm/sharp@0.34.5/node_modules/sharp/lib/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/image-optimizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/lru-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/next-dev-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/render-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/route-module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/load-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/adapter.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/app-dir-module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/app-render.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/error-boundary.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/layout-router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/render-from-template-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-page.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-segment.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/icons.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/metadata.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/framework/boundary-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-validation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/entry-base.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-page.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-dev-runtime.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/compiler-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/client.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/static.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/fallback-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/url-pattern.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/connection.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/exports/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request-meta.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/cli/next-test.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/size-limit.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config-shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/api-utils/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/adapter/build-complete.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_app.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/app.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_document.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/document.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/dynamic.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dynamic.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/catch-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/api/error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/head.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/draft-mode.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/get-img-props.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/image-component.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/link.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/link.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unrecognized-action-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/not-found.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/forbidden.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unauthorized.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.react-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/script.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/script.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/global.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image-types/global.d.ts","./.next/types/routes.d.ts","./next-env.d.ts","./storybook.shims.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/hmrPayload.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/customEvent.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/logging-C6h4g8dA.d.mts","../node_modules/.pnpm/@oxc-project+types@0.130.0/node_modules/@oxc-project/types/types.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/binding-zH1vcmbM.d.mts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/composable-filters.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/filter-vite-plugins.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/simple-filters.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/index.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/index.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/define-config-CeKzMIcs.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/index.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/parse-ast-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/rollupTypeCompat.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/constructors-DAnSz-_I.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/plugins-index.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/transform-DgZ3paSD.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/utils-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/hot.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/module-runner.d.ts","../node_modules/.pnpm/esbuild@0.27.7/node_modules/esbuild/lib/main.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/esbuildOptions.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/metadata.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/terserOptions.d.ts","../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/previous-map.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/input.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/css-syntax-error.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/declaration.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/root.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/warning.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/lazy-result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/no-work-result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/processor.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/document.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/rule.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/node.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/comment.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/container.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/at-rule.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/list.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/postcss.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/postcss.d.mts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/ast.d.ts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/targets.d.ts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/index.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/lightningcssOptions.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/filter-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/importGlob.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/index.d.ts","../node_modules/.pnpm/@vitest+pretty-format@4.1.6/node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/display.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/types.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/helpers.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/timers.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/diff.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/tasks.d-DEYaIMIu.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/traces.d.D2T_R8rx.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/environment.d-DOJxxZV9.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/rawSnapshot.d-D_X3-62x.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/config.d.A1h_Y6Jt.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/environment.d.CrsxCzP1.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/rpc.d.B_8sPU0w.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/worker.d.ZpHpO4yb.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/browser.d.BcoexmFG.d.ts","../node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/optional-types.d.ts","../node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/dist/index.d.ts","../node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.d.ts","../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/index.d.ts","../node_modules/.pnpm/assertion-error@2.0.1/node_modules/assertion-error/index.d.ts","../node_modules/.pnpm/@types+chai@5.2.3/node_modules/@types/chai/index.d.ts","../node_modules/.pnpm/@vitest+expect@4.1.6/node_modules/@vitest/expect/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/optional-types.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/utils.d.ts","../node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/benchmark.d.DAaHLpsq.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/types.d-BjI5eAwu.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/index.d-B41z0AuW.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/source-map.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/global.d.DVsSRdQ5.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/optional-runtime-types.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/suite.d.udJtyAgw.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/evaluatedModules.d.BxJ5omdx.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/runners.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/coverage.d.BZtK59WP.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/serialize.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/error.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/browser.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/aria-role.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/jest-dom.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/matchers.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/context.d.ts","../node_modules/.pnpm/@vitest+browser-playwright@4.1.6_playwright@1.60.0_vite@8.0.13_@types+node@25.8.0_esbui_d3b4c98b933477f7e7f49b0aad7a0b49/node_modules/@vitest/browser-playwright/context.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/browser/context.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/manager.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/reporters.d.CtLUhkkA.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/plugin.d.C0upjs_f.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/coverage.d.ts","../node_modules/.pnpm/obug@2.1.1/node_modules/obug/dist/core.d.ts","../node_modules/.pnpm/obug@2.1.1/node_modules/obug/dist/node.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/node.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/dist/index.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/protocol.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/structs.d.ts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/standard-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ar.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/az.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/be.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/bg.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ca.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/cs.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/da.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/de.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/el.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/en.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/eo.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/es.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fa.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fi.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fr-CA.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/he.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hu.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hy.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/id.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/is.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/it.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ja.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ka.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/kh.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/km.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ko.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/lt.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/mk.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ms.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/nl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/no.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ota.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ps.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/pl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/pt.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ro.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ru.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/sl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/sv.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ta.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/th.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/tr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ua.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/uk.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ur.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/uz.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/vi.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/zh-CN.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/zh-TW.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/yo.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-generator.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/checks.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/compat.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/from-json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/coerce.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/typeAliases.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/util.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/ZodError.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/locales/en.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/parseUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/enumUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/errorUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/partialUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/standard-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/types.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/external.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/index.d.cts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/types.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/index.d.ts","../node_modules/.pnpm/playwright@1.60.0/node_modules/playwright/index.d.ts","../node_modules/.pnpm/@vitest+browser-playwright@4.1.6_playwright@1.60.0_vite@8.0.13_@types+node@25.8.0_esbui_d3b4c98b933477f7e7f49b0aad7a0b49/node_modules/@vitest/browser-playwright/dist/index.d.ts","./vitest.shims.d.ts","./next.config.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/config.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/config.d.ts","../node_modules/.pnpm/@storybook+addon-vitest@10.4.0_@vitest+browser-playwright@4.1.6_@vitest+browser@4.1.6_@_92cdec18fed2ca7a5a8c0a4dff19a221/node_modules/@storybook/addon-vitest/dist/vitest-plugin/index.d.ts","../node_modules/.pnpm/@vitejs+plugin-react@6.0.2_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitejs/plugin-react/types/optionalTypes.d.ts","../node_modules/.pnpm/@vitejs+plugin-react@6.0.2_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitejs/plugin-react/dist/index.d.ts","./vitest.config.ts","../node_modules/.pnpm/@types+aria-query@5.0.4/node_modules/@types/aria-query/index.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/matchers.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/jest.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/index.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/matches.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/wait-for.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/query-helpers.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/queries.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/get-queries-for-element.d.ts","../node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/build/types.d.ts","../node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/build/index.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/screen.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/wait-for-element-to-be-removed.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/get-node-text.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/events.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/pretty-dom.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/role-helpers.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/config.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/suggestions.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/index.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/test-utils/index.d.ts","../node_modules/.pnpm/@testing-library+react@16.3.2_@testing-library+dom@10.4.1_@types+react-dom@19.2.3_@type_01f2c7bab43719bd0469ec1c0554baf5/node_modules/@testing-library/react/types/index.d.ts","./vitest.setup.ts","./src/lib/types.ts","./src/lib/storybook-fixtures.ts","./src/lib/view-helpers.ts","./src/lib/dashboard-view.ts","./src/app/(home)/_components/shared.ts","./src/app/(home)/_components/shared.test.ts","./src/app/admin/health/_components/helpers.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/adapters.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/types.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/general/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/general/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/compact/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/flattened/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/general/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/produce.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/compact/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/flattened/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/general/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwk/thumbprint.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwk/embedded.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwks/local.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwks/remote.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/unsecured.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/export.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/import.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/decode_protected_header.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/decode_jwt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/errors.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/generate_key_pair.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/generate_secret.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/base64url.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/runtime.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/index.d.ts","../node_modules/.pnpm/openid-client@5.7.1/node_modules/openid-client/types/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/oauth-types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/oauth.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/email.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/lib/cookie.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/credentials.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/jwt/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/jwt/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/utils/logger.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/next/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/github.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/google.d.ts","./src/lib/auth.ts","./src/lib/api.ts","./src/app/admin/sources/_components/helpers.ts","./src/app/api/auth/[...nextauth]/route.ts","./src/app/api/auth/[...nextauth]/route.test.ts","./src/app/api/content-skills/route.ts","./src/app/api/content-skills/route.test.ts","./src/app/api/entities/route.ts","./src/app/api/entities/route.test.ts","./src/app/api/entities/[id]/route.ts","./src/app/api/entities/[id]/route.test.ts","./src/app/api/entity-candidates/[id]/route.ts","./src/app/api/entity-candidates/[id]/route.test.ts","./src/app/api/feedback/route.ts","./src/app/api/feedback/route.test.ts","./src/app/api/invitations/[token]/accept/route.ts","./src/app/api/invitations/[token]/accept/route.test.ts","./src/app/api/messages/threads/route.ts","./src/app/api/messages/threads/route.test.ts","./src/app/api/messages/threads/[threadId]/messages/route.ts","./src/app/api/messages/threads/[threadId]/messages/route.test.ts","./src/app/api/messages/threads/[threadId]/read/route.ts","./src/app/api/messages/threads/[threadId]/read/route.test.ts","./src/app/api/notifications/route.ts","./src/app/api/notifications/route.test.ts","./src/app/api/profile/route.ts","./src/app/api/profile/route.test.ts","./src/app/api/profile/avatar/route.ts","./src/app/api/profile/avatar/route.test.ts","./src/app/api/projects/route.ts","./src/app/api/projects/route.test.ts","./src/app/api/projects/[id]/draft-action-helpers.ts","./src/app/api/projects/[id]/trend-action-helpers.ts","./src/app/api/projects/[id]/authority-settings/route.ts","./src/app/api/projects/[id]/authority-settings/route.test.ts","./src/app/api/projects/[id]/bluesky-credentials/route.ts","./src/app/api/projects/[id]/bluesky-credentials/route.test.ts","./src/app/api/projects/[id]/draft-items/[itemId]/route.ts","./src/app/api/projects/[id]/draft-items/[itemId]/route.test.ts","./src/app/api/projects/[id]/draft-original-pieces/[pieceId]/route.ts","./src/app/api/projects/[id]/draft-original-pieces/[pieceId]/route.test.ts","./src/app/api/projects/[id]/draft-sections/[sectionId]/route.ts","./src/app/api/projects/[id]/draft-sections/[sectionId]/route.test.ts","./src/app/api/projects/[id]/drafts/[draftId]/route.ts","./src/app/api/projects/[id]/drafts/[draftId]/route.test.ts","./src/app/api/projects/[id]/drafts/[draftId]/regenerate-section/route.ts","./src/app/api/projects/[id]/drafts/[draftId]/regenerate-section/route.test.ts","./src/app/api/projects/[id]/drafts/generate/route.ts","./src/app/api/projects/[id]/drafts/generate/route.test.ts","./src/app/api/projects/[id]/entity-candidate-bulk/route.ts","./src/app/api/projects/[id]/entity-candidate-bulk/route.test.ts","./src/app/api/projects/[id]/ideas/generate/route.ts","./src/app/api/projects/[id]/ideas/route.test.ts","./src/app/api/projects/[id]/ideas/[ideaId]/accept/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/dismiss/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/mark-written/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/route.test.ts","./src/app/api/projects/[id]/intake/route.ts","./src/app/api/projects/[id]/intake/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/route.ts","./src/app/api/projects/[id]/intake-allowlist/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.test.ts","./src/app/api/projects/[id]/invitations/route.ts","./src/app/api/projects/[id]/invitations/route.test.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.test.ts","./src/app/api/projects/[id]/linkedin-oauth/start/route.ts","./src/app/api/projects/[id]/linkedin-oauth/start/route.test.ts","./src/app/api/projects/[id]/linkedin-source-configs/route.ts","./src/app/api/projects/[id]/linkedin-source-configs/route.test.ts","./src/app/api/projects/[id]/mastodon-credentials/route.ts","./src/app/api/projects/[id]/mastodon-credentials/route.test.ts","./src/app/api/projects/[id]/members/[membershipId]/route.ts","./src/app/api/projects/[id]/members/[membershipId]/route.test.ts","./src/app/api/projects/[id]/rotate-intake-token/route.ts","./src/app/api/projects/[id]/rotate-intake-token/route.test.ts","./src/app/api/projects/[id]/themes/[themeId]/accept/route.ts","./src/app/api/projects/[id]/themes/[themeId]/dismiss/route.ts","./src/app/api/projects/[id]/themes/[themeId]/route.test.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.test.ts","./src/app/api/projects/[id]/verify-linkedin-credentials/route.ts","./src/app/api/projects/[id]/verify-linkedin-credentials/route.test.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.test.ts","./src/app/api/review/[id]/route.ts","./src/app/api/review/[id]/route.test.ts","./src/app/api/skills/[skillName]/route.ts","./src/app/api/skills/[skillName]/route.test.ts","./src/app/api/source-configs/route.ts","./src/app/api/source-configs/route.test.ts","./src/app/api/source-configs/[id]/route.ts","./src/app/api/source-configs/[id]/route.test.ts","./src/app/content/[id]/_components/helpers.ts","./src/app/entities/candidates/_components/shared.ts","./src/app/ideas/_components/shared.ts","./src/app/themes/_components/shared.ts","./src/app/trends/_components/shared.ts","./src/app/trends/_components/shared.test.ts","./src/lib/messages.ts","./src/lib/notifications.ts","./src/lib/profile.ts","../node_modules/.pnpm/@tanstack+query-core@5.100.10/node_modules/@tanstack/query-core/build/modern/_tsup-dts-rollup.d.ts","../node_modules/.pnpm/@tanstack+query-core@5.100.10/node_modules/@tanstack/query-core/build/modern/index.d.ts","../node_modules/.pnpm/@tanstack+react-query@5.100.10_react@19.2.6/node_modules/@tanstack/react-query/build/modern/_tsup-dts-rollup.d.ts","../node_modules/.pnpm/@tanstack+react-query@5.100.10_react@19.2.6/node_modules/@tanstack/react-query/build/modern/index.d.ts","./src/lib/useRole.ts","../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts","../node_modules/.pnpm/tailwind-merge@3.6.0/node_modules/tailwind-merge/dist/types.d.ts","./src/lib/utils.ts","./src/lib/__tests__/api.test.ts","./src/lib/__tests__/auth.test.ts","./src/lib/__tests__/dashboard-view.test.ts","./src/lib/__tests__/view-helpers.test.ts","./test-support/server-only.ts","./src/providers/QueryProvider/index.tsx","../node_modules/.pnpm/next-themes@0.4.6_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next-themes/dist/index.d.ts","./src/providers/ThemeProvider/index.tsx","./src/app/layout.tsx","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/router/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/theming/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/channels/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/preview-api/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/core-events/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/oxc-parser/index.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/namedTypes.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/kinds.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/builders.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/types.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/path.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/scope.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/node-path.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/path-visitor.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/visitor.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/main.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/options.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/parser.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/printer.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/main.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/babel/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/csf-tools/index.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/environment.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/url.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/common/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/telemetry/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/core-server/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/node-logger/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/types/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/csf/index.d.ts","../node_modules/.pnpm/@storybook+react@10.4.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react@19.2_9637a9e41fe50712e404e9d6bfecb53c/node_modules/@storybook/react/dist/index.d.ts","../node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","../node_modules/.pnpm/@rollup+pluginutils@5.3.0/node_modules/@rollup/pluginutils/types/index.d.ts","../node_modules/.pnpm/vite-plugin-storybook-nextjs@3.2.4_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_reac_a72e796d7340f857c3aad09f1cfbfdcd/node_modules/vite-plugin-storybook-nextjs/dist/index.d.ts","../node_modules/.pnpm/@storybook+builder-vite@10.4.0_esbuild@0.27.7_storybook@10.4.0_@emnapi+core@1.10.0_@emn_cdc6719e2a41f2fcaaeb8fc79968633c/node_modules/@storybook/builder-vite/dist/index.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts","../node_modules/.pnpm/react-docgen-typescript@2.4.0_typescript@5.9.3/node_modules/react-docgen-typescript/lib/parser.d.ts","../node_modules/.pnpm/react-docgen-typescript@2.4.0_typescript@5.9.3/node_modules/react-docgen-typescript/lib/index.d.ts","../node_modules/.pnpm/@joshwooding+vite-plugin-react-docgen-typescript@0.7.0_typescript@5.9.3_vite@8.0.13_@ty_12cc9b528b9fd9198b84fe3502042fbc/node_modules/@joshwooding/vite-plugin-react-docgen-typescript/dist/index.d.ts","../node_modules/.pnpm/@storybook+react-vite@10.4.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react_8acbcb421469172b896e2c2ce69779dc/node_modules/@storybook/react-vite/dist/index.d.ts","../node_modules/.pnpm/@storybook+nextjs-vite@10.4.0_@babel+core@7.29.0_@types+react-dom@19.2.3_@types+react@1_0959b0caa1b7fc3f2aed0f76ce58a98c/node_modules/@storybook/nextjs-vite/dist/index.d.ts","../node_modules/.pnpm/lucide-react@1.16.0_react@19.2.6/node_modules/lucide-react/dist/lucide-react.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/reason-parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/reasons.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/createBaseUIEventDetails.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/types/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/types.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/button/Button.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/button/index.d.ts","../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types.d.ts","../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.d.ts","./src/components/ui/button.tsx","./src/components/elements/ThemeToggle/index.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/merge-props/mergeProps.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/merge-props/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/getStateAttributesProps.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/use-render/useRender.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/use-render/index.d.ts","./src/components/ui/badge.tsx","../node_modules/.pnpm/@floating-ui+utils@0.2.11/node_modules/@floating-ui/utils/dist/floating-ui.utils.d.mts","../node_modules/.pnpm/@floating-ui+core@1.7.5/node_modules/@floating-ui/core/dist/floating-ui.core.d.mts","../node_modules/.pnpm/@floating-ui+utils@0.2.11/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.d.mts","../node_modules/.pnpm/@floating-ui+dom@1.7.6/node_modules/@floating-ui/dom/dist/floating-ui.dom.d.mts","../node_modules/.pnpm/@floating-ui+react-dom@2.1.8_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.d.mts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/utils/constants.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useInteractions.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingTreeStore.d.ts","../node_modules/.pnpm/reselect@5.1.1/node_modules/reselect/dist/reselect.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/createSelector.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/createSelectorMemoized.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/fastHooks.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/Store.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/useStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/ReactStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/StoreInspector.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/useTransitionStatus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/popupTriggerMap.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/store.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/popupStoreUtils.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingRootStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/useEnhancedClickHandler.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingFocusManager.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/useRenderElement.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useClientPoint.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useDismiss.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFocus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverShared.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHover.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverFloatingInteraction.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverReferenceInteraction.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useListNavigation.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useRole.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useTypeahead.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFloatingRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/safePolygon.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingTree.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/types.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingDelayGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useClick.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFloating.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useSyncedFloatingRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/useAnchorPositioning.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/arrow/MenuArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/backdrop/MenuBackdrop.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/store/MenuStore.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/root/MenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menubar/MenubarContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/context-menu/root/ContextMenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/context-menu/root/ContextMenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/store/MenuHandle.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/root/MenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/checkbox-item/MenuCheckboxItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/group/MenuGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/group-label/MenuGroupLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/item/MenuItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/link-item/MenuLinkItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/popup/MenuPopup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/portal/MenuPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/positioner/MenuPositioner.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-group/MenuRadioGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-item/MenuRadioItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-item-indicator/MenuRadioItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-root/MenuSubmenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-root/MenuSubmenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/trigger/MenuTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/viewport/MenuViewport.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/separator/Separator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-trigger/MenuSubmenuTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/index.d.ts","./src/components/ui/dropdown-menu.tsx","./src/components/layout/NotificationMenu/index.tsx","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/client/_utils.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/react/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/react/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/root/AvatarRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/image/useImageLoadingStatus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/image/AvatarImage.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/fallback/AvatarFallback.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/index.d.ts","./src/components/ui/avatar.tsx","./src/components/elements/UserAvatar/index.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.tsx","./src/components/layout/UserMenu/index.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/separator/index.d.ts","./src/components/ui/separator.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.tsx","./src/components/ui/card.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.tsx","./src/components/layout/AppShell/index.tsx","./src/components/ui/alert.tsx","./src/components/elements/StatusBadge/index.tsx","./src/app/(home)/_components/ContentFeed/index.tsx","./src/components/ui/label.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/resolveValueLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/root/SelectRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/form-context/FormContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/form/Form.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/form/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/root/FieldRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/label/SelectLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/trigger/SelectTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/value/SelectValue.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/icon/SelectIcon.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/portal/SelectPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/backdrop/SelectBackdrop.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/positioner/SelectPositioner.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/popup/SelectPopup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/list/SelectList.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item/SelectItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item-indicator/SelectItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item-text/SelectItemText.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/arrow/SelectArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/scroll-down-arrow/SelectScrollDownArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/scroll-up-arrow/SelectScrollUpArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/group/SelectGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/group-label/SelectGroupLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/index.d.ts","./src/components/ui/select.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.tsx","./src/app/(home)/_components/DashboardOverview/index.tsx","./src/app/(home)/_components/DashboardSidebar/index.tsx","./src/components/ui/table.tsx","./src/app/(home)/_components/ReviewQueueTable/index.tsx","./src/app/(home)/_components/HomePageContent/index.tsx","./src/lib/storybook-docs.tsx","./src/app/(home)/page.stories.tsx","./src/app/(home)/page.tsx","./src/app/(home)/page.test.tsx","./src/app/(home)/_components/ContentFeed/index.stories.tsx","./src/app/(home)/_components/ContentFeed/index.test.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.stories.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.test.tsx","./src/app/(home)/_components/DashboardOverview/index.stories.tsx","./src/app/(home)/_components/DashboardOverview/index.test.tsx","./src/app/(home)/_components/DashboardSidebar/index.stories.tsx","./src/app/(home)/_components/DashboardSidebar/index.test.tsx","./src/app/(home)/_components/HomePageContent/index.stories.tsx","./src/app/(home)/_components/HomePageContent/index.test.tsx","./src/app/(home)/_components/ReviewQueueTable/index.stories.tsx","./src/app/(home)/_components/ReviewQueueTable/index.test.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.tsx","./src/app/admin/health/page.stories.tsx","./src/app/admin/health/page.tsx","./src/app/admin/health/page.test.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.stories.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.test.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.stories.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.test.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.stories.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.test.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.stories.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/label/FieldLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/error/FieldError.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/description/FieldDescription.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/control/FieldControl.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/validity/FieldValidity.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/item/FieldItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/input/Input.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/input/index.d.ts","./src/components/ui/input.tsx","./src/components/ui/textarea.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.tsx","./src/app/admin/projects/new/page.stories.tsx","./src/app/admin/projects/new/page.tsx","./src/app/admin/projects/new/page.test.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.stories.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.test.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.stories.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.test.tsx","./src/components/elements/CopyButton/index.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.tsx","./src/app/admin/sources/_components/SourceConfigList/index.tsx","./src/app/admin/sources/page.stories.tsx","./src/app/admin/sources/page.tsx","./src/app/admin/sources/page.test.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.stories.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.test.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.stories.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.test.tsx","./src/app/admin/sources/_components/SourceConfigList/index.stories.tsx","./src/app/admin/sources/_components/SourceConfigList/index.test.tsx","./src/app/content/[id]/_components/SkillActionBar/index.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.tsx","./src/app/content/[id]/page.stories.tsx","./src/app/content/[id]/page.tsx","./src/app/content/[id]/page.test.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.stories.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.test.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.stories.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.test.tsx","./src/app/content/[id]/_components/SkillActionBar/index.stories.tsx","./src/app/content/[id]/_components/SkillActionBar/index.test.tsx","./src/app/drafts/_components/DraftsList/index.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.tsx","./src/app/drafts/_components/DraftsToolbar/index.tsx","./src/app/drafts/page.stories.tsx","./src/app/drafts/page.tsx","./src/app/drafts/page.test.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.tsx","./src/app/drafts/[draftId]/page.stories.tsx","./src/app/drafts/[draftId]/page.tsx","./src/app/drafts/[draftId]/page.test.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.test.tsx","./src/app/drafts/_components/DraftsList/index.stories.tsx","./src/app/drafts/_components/DraftsList/index.test.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.stories.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.test.tsx","./src/app/drafts/_components/DraftsToolbar/index.stories.tsx","./src/app/drafts/_components/DraftsToolbar/index.test.tsx","./src/app/entities/_components/shared.tsx","./src/app/entities/_components/CreateEntityCard/index.tsx","./src/app/entities/_components/EntityCandidatesCard/index.tsx","./src/app/entities/_components/EntityCard/index.tsx","./src/app/entities/_components/EntitiesPageContent/index.tsx","./src/app/entities/page.stories.tsx","./src/app/entities/page.tsx","./src/app/entities/page.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/root/SliderRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/label/SliderLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/value/SliderValue.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/control/SliderControl.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/track/SliderTrack.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/labelable-provider/LabelableContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/thumb/SliderThumb.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/indicator/SliderIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/index.d.ts","./src/components/ui/slider.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.tsx","./src/app/entities/[id]/_components/EntityDetailPageContent/index.tsx","./src/app/entities/[id]/page.stories.tsx","./src/app/entities/[id]/page.tsx","./src/app/entities/[id]/page.test.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.stories.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.test.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.stories.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.test.tsx","./src/app/entities/[id]/_components/EntityDetailPageContent/index.test.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.stories.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.test.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.stories.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.test.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.stories.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.test.tsx","./src/app/entities/_components/CreateEntityCard/index.stories.tsx","./src/app/entities/_components/CreateEntityCard/index.test.tsx","./src/app/entities/_components/EntitiesPageContent/index.test.tsx","./src/app/entities/_components/EntityCandidatesCard/index.stories.tsx","./src/app/entities/_components/EntityCandidatesCard/index.test.tsx","./src/app/entities/_components/EntityCard/index.stories.tsx","./src/app/entities/_components/EntityCard/index.test.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.tsx","./src/app/entities/candidates/page.stories.tsx","./src/app/entities/candidates/page.tsx","./src/app/entities/candidates/page.test.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.stories.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.test.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.stories.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.test.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.stories.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.test.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.tsx","./src/app/ideas/page.stories.tsx","./src/app/ideas/page.tsx","./src/app/ideas/page.test.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.stories.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.test.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.stories.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.test.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.stories.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.test.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.tsx","./src/app/invite/[token]/page.stories.tsx","./src/app/invite/[token]/page.tsx","./src/app/invite/[token]/page.test.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.stories.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.test.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.stories.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/root/CheckboxRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/indicator/CheckboxIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/index.d.ts","./src/components/ui/checkbox.tsx","./src/app/login/_components/LoginForm/index.tsx","./src/app/login/_components/SocialAuthButtons/index.tsx","./src/app/login/_components/LoginPageContent/index.tsx","./src/app/login/page.stories.tsx","./src/app/login/page.tsx","./src/app/login/page.test.tsx","./src/app/login/_components/LoginForm/index.stories.tsx","./src/app/login/_components/LoginForm/index.test.tsx","./src/app/login/_components/LoginPageContent/index.stories.tsx","./src/app/login/_components/LoginPageContent/index.test.tsx","./src/app/login/_components/SocialAuthButtons/index.stories.tsx","./src/app/login/_components/SocialAuthButtons/index.test.tsx","./src/app/messages/_components/MessagesWorkspace/index.tsx","./src/app/messages/_components/MessagesPageContent/index.tsx","./src/app/messages/page.tsx","./src/app/messages/page.test.tsx","./src/app/messages/_components/MessagesPageContent/index.test.tsx","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/eventMap.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/types.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/dispatchEvent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/focus.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/input.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/click/isClickableInput.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/Blob.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/DataTransfer.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/FileList.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/Clipboard.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/timeValue.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/isContentEditable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/isEditable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/maxLength.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/setFiles.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/cursor.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/getActiveElement.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/getTabDestination.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/isFocusable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/selection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/selector.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/keyDef/readNextDescriptor.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/cloneEvent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/findClosest.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getDocumentFromNode.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getTreeDiff.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getWindow.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isDescendantOrSelf.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isElementType.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isVisible.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isDisabled.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/level.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/wait.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/pointer/cssPointerEvents.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/UI.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/getValueOrTextContent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/copySelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/trackValue.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/getInputRange.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/modifySelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/moveSelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelectionPerMouse.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/modifySelectionPerMouse.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/selectAll.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelectionRange.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/updateSelectionOnFocus.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/buttons.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/shared.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/keyboard.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/options.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/click.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/hover.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/tab.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/keyboard/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/copy.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/cut.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/paste.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/pointer/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/clear.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/selectOptions.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/type.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/upload.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/api.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/directApi.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/setup.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/index.d.ts","./src/app/messages/_components/MessagesWorkspace/index.test.tsx","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/file.d.ts","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/file-selector.d.ts","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/index.d.ts","../node_modules/.pnpm/react-dropzone@15.0.0_react@19.2.6/node_modules/react-dropzone/typings/react-dropzone.d.ts","./src/app/profile/_components/AvatarDropzone/index.tsx","./src/app/profile/_components/AvatarPreview/index.tsx","./src/app/profile/_components/ProfileForm/index.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.tsx","./src/app/profile/page.stories.tsx","./src/app/profile/_components/ProfileSettingsPanel/index.tsx","./src/app/profile/page.tsx","./src/app/profile/page.test.tsx","./src/app/profile/_components/AvatarDropzone/index.stories.tsx","./src/app/profile/_components/AvatarDropzone/index.test.tsx","./src/app/profile/_components/AvatarPreview/index.stories.tsx","./src/app/profile/_components/AvatarPreview/index.test.tsx","./src/app/profile/_components/ProfileForm/index.stories.tsx","./src/app/profile/_components/ProfileForm/index.test.tsx","./src/app/profile/_components/ProfileSettingsPanel/index.test.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.stories.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.test.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.tsx","./src/app/projects/[id]/members/_components/shared.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.tsx","./src/app/projects/[id]/members/page.stories.tsx","./src/app/projects/[id]/members/page.tsx","./src/app/projects/[id]/members/page.test.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.stories.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.test.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.stories.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.test.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.stories.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.test.tsx","./src/app/projects/[id]/members/invite/_components/shared.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.tsx","./src/app/projects/[id]/members/invite/page.stories.tsx","./src/app/projects/[id]/members/invite/page.tsx","./src/app/projects/[id]/members/invite/page.test.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.stories.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.test.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.stories.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.test.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.tsx","./src/app/themes/_components/ThemesQueueOverview/index.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.tsx","./src/app/themes/_components/ThemesPageContent/index.tsx","./src/app/themes/page.stories.tsx","./src/app/themes/page.tsx","./src/app/themes/page.test.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.stories.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.test.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.stories.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.test.tsx","./src/app/themes/_components/ThemesPageContent/index.stories.tsx","./src/app/themes/_components/ThemesPageContent/index.test.tsx","./src/app/themes/_components/ThemesQueueOverview/index.stories.tsx","./src/app/themes/_components/ThemesQueueOverview/index.test.tsx","./src/app/trends/_components/TopicClusterCard/index.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.tsx","./src/app/trends/_components/TrendsQueueOverview/index.tsx","./src/app/trends/_components/TrendsPageContent/index.tsx","./src/app/trends/page.stories.tsx","./src/app/trends/page.tsx","./src/app/trends/page.test.tsx","./src/app/trends/_components/TopicClusterCard/index.stories.tsx","./src/app/trends/_components/TopicClusterCard/index.test.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.stories.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.test.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.stories.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.test.tsx","./src/app/trends/_components/TrendsPageContent/index.stories.tsx","./src/app/trends/_components/TrendsPageContent/index.test.tsx","./src/app/trends/_components/TrendsQueueOverview/index.stories.tsx","./src/app/trends/_components/TrendsQueueOverview/index.test.tsx","./src/components/elements/CopyButton/index.stories.tsx","./src/components/elements/CopyButton/index.test.tsx","./src/components/elements/StatusBadge/index.stories.tsx","./src/components/elements/StatusBadge/index.test.tsx","./src/components/elements/ThemeToggle/index.stories.tsx","./src/components/elements/ThemeToggle/index.test.tsx","./src/components/elements/UserAvatar/index.stories.tsx","./src/components/elements/UserAvatar/index.test.tsx","./src/components/layout/AppShell/index.stories.tsx","./src/components/layout/AppShell/index.test.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.stories.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.test.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.stories.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.test.tsx","./src/components/layout/NotificationMenu/index.test.tsx","../node_modules/.pnpm/@vitest+spy@3.2.4/node_modules/@vitest/spy/dist/index.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/matchers-standalone.d.ts","../node_modules/.pnpm/@vitest+pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/helpers.d.ts","../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/index-8b61d5bc.d.ts","../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/diff.d.ts","../node_modules/.pnpm/@vitest+expect@3.2.4/node_modules/@vitest/expect/dist/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/test/index.d.ts","./src/components/layout/UserMenu/index.stories.tsx","./src/components/layout/UserMenu/index.test.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.stories.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.test.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.stories.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.test.tsx","./src/lib/__tests__/useRole.test.tsx","./src/providers/QueryProvider/index.test.tsx","./src/providers/ThemeProvider/index.test.tsx","./.next/types/cache-life.d.ts","./.next/types/validator.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/globals.d.ts"],"fileIdsList":[[94,158,166,170,173,175,176,177,189,506,507,508,509,548,555],[94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,249,547,548,550,553,555,863,865,867,869,871,873,875,877,879,881,883,885,887,889,893,895,897,899,901,903,905,907,909,911,913,914,915,917,919,921,923,925,927,929,931,933,935,937,938,940,942,944,946,948,950,952,979,1176,1195,1220,1231,1243,1255,1262,1284,1304,1328,1340,1351,1366,1376,1467,1483,1495,1506,1522],[94,158,166,170,173,175,176,177,189,548,551,552,553,555],[94,158,166,170,173,175,176,177,189,249,548,551,555],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1140,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1140],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,807,970,1032,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1168,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1168],[94,158,166,170,173,175,176,177,189,249,530,548,555,806,807,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1169,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1169],[94,158,166,170,173,175,176,177,189,249,548,555,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1170,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1170],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1173,1174],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,1173],[94,158,166,170,173,175,176,177,189,249,548,555,803,806,807,1137,1138,1140,1168,1169,1170,1172],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1172,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,1172],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1032,1138,1171],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,804,807],[94,158,166,170,173,175,176,177,189,249,548,555,803,806],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1176],[94,158,166,170,173,175,176,177,189,249,548,555,805,806,807,861,1137,1138,1173],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1190],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1190],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1191],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1191],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139,1171],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1192],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1192],[85,94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1135,1139,1171],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1193],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1193],[94,158,166,170,173,175,176,177,189,249,548,555,803],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1190,1191,1192,1193],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,809,1195],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,809,861,1137,1190,1191,1192,1193],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1217],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1217],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1135,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1218],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1218],[94,158,166,170,173,175,176,177,189,249,548,555,1138],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1217,1218],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1220],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1217,1218],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1227],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1227],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,862,970,1032,1135,1139,1141,1167,1215,1226],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1228],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1228],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,862,1032,1135,1139,1141,1167,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1229],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1229],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1032,1135,1139,1141,1167,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,803,861],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1227,1228,1229],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,862,1231],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,862,1137,1138,1227,1228,1229],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,863],[94,158,166,170,173,175,176,177,189,249,548,555,860],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,865],[94,158,166,170,173,175,176,177,189,249,547,548,555,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,869],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,867],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,871],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,873],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,875],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,879],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,881],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,877],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,883],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,887],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,885],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,893],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,895],[94,158,166,170,173,175,176,177,189,249,548,555],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,897],[94,158,166,170,173,175,176,177,189,249,547,548,555,861,891],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,899],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,901],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,905],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,903],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,907],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,909],[94,158,166,170,173,175,176,177,189,249,547,548,555,861,892],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,913,914,915],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,911],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,921],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,919],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,917],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,925],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,923],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,927],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,929],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,931],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,933],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,935],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,937,938],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,940],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,942],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,944],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,889],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,946],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,948],[94,158,166,170,173,175,176,177,189,249,547,548,555,803,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,952],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,950],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1174,1240],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1240],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1139,1239],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1174,1241],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1241],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,976,1021,1174,1239],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,976,1239],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,966,1032,1138],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1240,1241],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,954,1243],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,954,1137,1138,1240,1241],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1257],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1257],[85,94,158,166,170,173,175,176,177,189,249,530,540,548,555,803,970,1032,1135,1138,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1258],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1258],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1260],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1260],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135,1259],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1259],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1259],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1257,1258,1259,1260],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1262],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1257,1258,1259,1260],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1251],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1251],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1252],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1252],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1253],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1253],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135,1167],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1251,1252,1253],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1255],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1251,1252,1253],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1298],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1298],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1039,1135,1297],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1297],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1297],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,1032,1135,1138,1141,1215,1296],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1302],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1298,1299,1300,1301],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1299],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1299],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1300],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1300],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1301],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1301],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,970,1032,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1302],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1304],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1302],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1279],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1279],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1135,1141,1215,1216,1278],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1282],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1279,1280,1281],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1280],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1280],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1138,1139,1141,1167,1278],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1281],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1281],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1032,1135,1138,1139,1141,1215,1216,1278],[94,158,166,170,173,175,176,177,189,249,548,555,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,955,1021,1174,1324],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,955,1324],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,955,1032,1039,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1325],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1325],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1326],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1326],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1039,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,955,1021,1137,1138,1174,1324,1325,1326],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1328],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,955,1137,1138,1324,1325,1326],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1282],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1284],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1282],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1336],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1336],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1337],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1337],[94,158,166,170,173,175,176,177,189,249,530,548,555,956,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1338],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1338],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,956,970,1032,1039,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1138,1174,1336,1337,1338],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1340],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,956,1137,1138,1336,1337,1338],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1348],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1348],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1349],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1349],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135,1138,1348],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1351],[94,158,166,170,173,175,176,177,189,249,548,555,805,857,860,861,1349],[85,94,158,166,170,173,175,176,177,189,249,548,551,555,970,976,978],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1362],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1362],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,1032,1120,1138,1141,1215,1361],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1364],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1364],[94,158,166,170,173,175,176,177,189,249,530,548,555,1133,1135,1362,1363],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1363],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1363],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1120],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1366],[94,158,166,170,173,175,176,177,189,249,548,555,1364],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1375],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1374],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,960,976,1374,1455],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,960,966,1032,1135,1138,1216],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1376],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1375],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1461],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1461],[85,94,158,166,170,173,175,176,177,189,249,548,555,1135,1138,1460],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1462],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1462],[94,158,166,170,173,175,176,177,189,249,548,555,803,1032,1128,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1463],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1463],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,1032,1135,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,966,1466],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,962,966,1138,1464],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1464],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1464],[94,158,166,170,173,175,176,177,189,249,548,555,803,1138,1461,1462,1463],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1464],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1467],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1466],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1478],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1478],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,970,1032,1039,1135,1138,1226],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1481],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1481],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,970,1032,1135,1137,1138,1478,1480],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1480],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1480],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1032,1135,1141,1167,1479],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1492],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1492],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135,1141,1167,1215,1491],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1493],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1493],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1492],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1495],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1493],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1483],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1481],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1503],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1503],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,957,970,1032,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1501],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1501],[94,158,166,170,173,175,176,177,189,249,530,548,555,957,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1504],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1504],[94,158,166,170,173,175,176,177,189,249,548,555,803,957,1137,1138,1501,1502,1503],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1502],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1502],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1506],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1504],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1516],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1516],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1517],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1517],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,958,970,1032,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1518],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1518],[94,158,166,170,173,175,176,177,189,249,530,548,555,958,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1520],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1520],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1516,1517,1518,1519],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1519],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1519],[94,158,166,170,173,175,176,177,189,249,548,555,805,1135],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,804,958],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1522],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1520],[85,94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1226],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1226],[85,94,158,166,170,173,175,176,177,189,249,548,555,1022,1032],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1139,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1139],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1031,1039],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1033,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1033],[85,94,158,166,170,173,175,176,177,189,249,548,555,977,1022,1032],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1128,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1128],[94,158,166,170,173,175,176,177,189,249,548,555,970,1127],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1134,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1134],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1022,1032,1033,1117,1131,1133],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1136,1174],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,960,966,976,1136],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,960,966,970,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1135,1137,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1137],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,861,1134,1136],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,961,976,1117,1455],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,961,966,1022,1039,1116],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1116,1129,1174,1560],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1116,1129],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1116,1120,1128],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1116,1130,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1116,1130],[94,158,166,170,173,175,176,177,189,249,548,555,1116,1128],[94,158,166,170,173,175,176,177,189,249,548,555,803,962,966,1021,1131,1174,1560],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,976,1131],[94,158,166,170,173,175,176,177,189,249,548,555,962,966,1116,1129,1130],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1031],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1126],[94,158,166,170,173,175,176,177,189,249,548,555,970,1031,1035,1038],[94,158,166,170,173,175,176,177,189,249,548,555,970,1029,1031],[85,94,158,166,170,173,175,176,177,189,249,548,555,970],[94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1360],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1115],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1214],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1166],[94,158,166,170,173,175,176,177,189,249,548,555,970,1132],[94,158,166,170,173,175,176,177,189,249,548,555,970,1295],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,860],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,806],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,966,967],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,805],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,857,860],[94,158,166,170,173,175,176,177,189,249,548,555,850,857,858,859],[94,158,166,170,173,175,176,177,189,249,548,555,803,805],[94,158,166,170,173,175,176,177,189,249,548,555,803,966],[94,158,166,170,173,175,176,177,189,249,548,555,968,969],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,966,976],[85,94,158,166,170,173,175,176,177,189,249,548,555,966],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,978],[85,94,158,166,170,173,175,176,177,189,249,548,555,977],[94,158,166,170,173,175,176,177,189,548],[94,158,166,170,173,175,176,177,181,189,206,249,548,555,771,775,776,778],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801],[94,158,166,170,173,175,176,177,189,548,555,771],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1121],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1121,1122],[85,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,548,555,1121,1123,1124,1125],[94,158,166,170,173,175,176,177,189,548,555,1121,1123,1124],[85,94,158,166,170,173,175,176,177,189,548,555,1027],[94,158,166,170,173,175,176,177,189,548,555,1028],[94,158,166,170,173,175,176,177,189,548,555,1357,1358,1359],[94,158,166,170,173,175,176,177,189,548,555,1357,1358],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1357],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1027,1147],[94,158,166,170,173,175,176,177,189,249,548,555,1026,1095,1115],[85,94,158,166,170,173,175,176,177,189,548,555,1092],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1147],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1147],[94,158,166,170,173,175,176,177,189,548,555,1147,1205,1206,1207,1208,1209,1210,1211],[94,158,166,170,173,175,176,177,189,548,555,1147,1205,1206,1207,1208,1209,1210],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1146],[85,94,158,166,170,173,175,176,177,189,548,555,1057,1147],[85,94,158,166,170,173,175,176,177,189,548,555,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1047,1063,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1065],[85,94,158,166,170,173,175,176,177,189,548,555,1025,1056,1057,1061,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1047,1080],[94,158,166,170,173,175,176,177,189,548,555,1080],[94,158,166,170,173,175,176,177,189,548,555,1024,1080],[94,158,166,170,173,175,176,177,189,548,555,1047,1080],[94,158,166,170,173,175,176,177,189,548,555,1025,1062,1080],[94,158,166,170,173,175,176,177,189,548,555,1070,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1047,1070,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1045,1080],[94,158,166,170,173,175,176,177,189,548,555,1026,1056,1061,1062],[94,158,166,170,173,175,176,177,189,548,555,1044,1046,1047,1064,1066,1067,1068,1069,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084],[94,158,166,170,173,175,176,177,189,548,555,1070],[85,94,158,166,170,173,175,176,177,189,548,555,1025,1044,1046,1047,1062,1064,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1081,1085],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1027,1144],[94,158,166,170,173,175,176,177,189,548,555,1145],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1212],[94,158,166,170,173,175,176,177,189,548,555,1213],[94,158,166,170,173,175,176,177,189,548,555,1024],[85,94,158,166,170,173,175,176,177,189,548,555,1145,1147],[94,158,166,170,173,175,176,177,189,548,555,1023],[85,94,158,166,170,173,175,176,177,189,548,555,1026],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1036],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1086],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1095],[94,158,166,170,173,175,176,177,189,548,555,1087,1088,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1109,1110,1111,1113,1114],[94,158,166,170,173,175,176,177,189,548,555,1087,1088,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1109,1110,1111,1112,1113],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1063,1086],[85,94,158,166,170,173,175,176,177,189,548,555,1085],[85,94,158,166,170,173,175,176,177,189,249,548,555,1024,1025,1061,1089,1090,1091,1093,1094],[85,94,158,166,170,173,175,176,177,189,548,555,1089,1095],[94,158,166,170,173,175,176,177,189,548,555,1089],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1047,1056,1057,1061,1062,1063,1085,1095,1115],[94,158,166,170,173,175,176,177,189,249,548,555,1095,1108],[85,94,158,166,170,173,175,176,177,189,548,555,1089],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1094],[85,94,158,166,170,173,175,176,177,189,548,555,1095],[94,158,166,170,173,175,176,177,189,548,555,1034],[94,158,166,170,173,175,176,177,189,548,555,1143,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165],[94,158,166,170,173,175,176,177,189,548,555,1112,1143,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1142],[94,158,166,170,173,175,176,177,189,548,555,1112],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1286],[94,158,166,170,173,175,176,177,189,548,555,1286,1287,1288,1289,1290,1292,1293,1294],[94,158,166,170,173,175,176,177,189,548,555,1286,1287,1288,1289,1290,1292,1293],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1286,1291],[85,94,158,166,170,173,175,176,177,189,548,555,1025],[94,158,166,170,173,175,176,177,189,548,555,1027,1037],[94,158,166,170,173,175,176,177,189,548,555,1058,1059,1060],[85,94,158,166,170,173,175,176,177,189,548,555,1056,1057,1059],[94,158,166,170,173,175,176,177,189,548,555,1027,1057,1058,1062,1085],[85,94,158,166,170,173,175,176,177,189,548,555,1040,1085],[94,158,166,170,173,175,176,177,189,548,555,1052],[85,94,158,166,170,173,175,176,177,189,249,548,555,1052],[94,158,166,170,173,175,176,177,189,548,555,1048],[94,158,166,170,173,175,176,177,189,548,555,1049],[94,158,166,170,173,175,176,177,189,548,555,1049,1050,1052,1053,1054,1055],[94,158,166,170,173,175,176,177,189,548,555,1051,1052],[94,158,166,170,173,175,176,177,189,548,555,1040],[94,158,166,170,173,175,176,177,189,548,555,1041,1042],[85,94,158,166,170,173,175,176,177,189,548,555,1043],[94,158,166,170,173,175,176,177,189,548,555,608,774,1016,1018],[94,158,166,170,173,175,176,177,189,548,555,562,563,564],[94,158,166,170,173,175,176,177,189,548,555,565],[94,158,166,170,173,175,176,177,189,548,555,1012],[94,158,166,170,173,175,176,177,189,548,555,775],[94,158,166,170,173,175,176,177,189,548,555,608,774,1009],[94,158,166,170,173,175,176,177,189,541,548,555,1009,1010,1011,1014,1015,1020,1560],[94,158,166,170,173,175,176,177,189,548,555,1009,1011,1015,1019],[85,94,158,166,170,173,175,176,177,189,219,482,548,555,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,963],[85,94,158,166,170,173,175,176,177,189,249,548,555,964],[94,158,166,170,173,175,176,177,189,548,555,965],[94,158,166,170,173,175,176,177,189,548,555,787],[94,158,166,170,173,175,176,177,189,548,555,784,785,786,787,788,791,792,793,794,795,796,797,798],[94,158,166,170,173,175,176,177,189,548,555,780],[94,158,166,170,173,175,176,177,189,548,555,790],[94,158,166,170,173,175,176,177,189,548,555,784,785,786],[94,158,166,170,173,175,176,177,189,548,555,784,785],[94,158,166,170,173,175,176,177,189,548,555,787,788,790],[94,158,166,170,173,175,176,177,189,548,555,785],[94,158,166,170,173,175,176,177,189,548,555,782],[94,158,166,170,173,175,176,177,189,548,555,781],[85,94,158,166,170,173,175,176,177,189,219,482,548,555,799,800],[94,158,166,170,173,175,176,177,189,548,555,1454],[94,158,166,170,173,175,176,177,189,548,555,1441,1442,1443],[94,158,166,170,173,175,176,177,189,548,555,1436,1437,1438],[94,158,166,170,173,175,176,177,189,548,555,1414,1415,1416,1417],[94,158,166,170,173,175,176,177,189,548,555,1380,1454],[94,158,166,170,173,175,176,177,189,548,555,1380],[94,158,166,170,173,175,176,177,189,548,555,1380,1381,1382,1383,1428],[94,158,166,170,173,175,176,177,189,548,555,1418],[94,158,166,170,173,175,176,177,189,548,555,1413,1419,1420,1421,1422,1423,1424,1425,1426,1427],[94,158,166,170,173,175,176,177,189,548,555,1428],[94,158,166,170,173,175,176,177,189,548,555,1379],[94,158,166,170,173,175,176,177,189,548,555,1432,1434,1435,1453,1454],[94,158,166,170,173,175,176,177,189,548,555,1432,1434],[94,158,166,170,173,175,176,177,189,548,555,1429,1432,1454],[94,158,166,170,173,175,176,177,189,548,555,1439,1440,1444,1445,1450],[94,158,166,170,173,175,176,177,189,548,555,1433,1435,1445,1453],[94,158,166,170,173,175,176,177,189,548,555,1452,1453],[94,158,166,170,173,175,176,177,189,548,555,1429,1433,1435,1451,1452],[94,158,166,170,173,175,176,177,189,548,555,1433,1454],[94,158,166,170,173,175,176,177,189,548,555,1431],[94,158,166,170,173,175,176,177,189,548,555,1431,1433,1454],[94,158,166,170,173,175,176,177,189,548,555,1429,1430],[94,158,166,170,173,175,176,177,189,548,555,1446,1447,1448,1449],[94,158,166,170,173,175,176,177,189,548,555,1435,1454],[94,158,166,170,173,175,176,177,189,548,555,1390],[94,158,166,170,173,175,176,177,189,548,555,1384,1391],[94,158,166,170,173,175,176,177,189,548,555,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412],[94,158,166,170,173,175,176,177,189,548,555,1410,1454],[94,158,166,170,173,175,176,177,189,548,555,632,633],[94,155,156,158,166,170,173,175,176,177,189,548,555],[94,157,158,166,170,173,175,176,177,189,548,555],[158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,197,548,555],[94,158,159,164,166,169,170,173,175,176,177,179,189,194,206,548,555],[94,158,159,160,166,169,170,173,175,176,177,189,548,555],[94,158,161,166,170,173,175,176,177,189,207,548,555],[94,158,162,163,166,170,173,175,176,177,180,189,548,555],[94,158,163,166,170,173,175,176,177,189,194,203,548,555],[94,158,164,166,169,170,173,175,176,177,179,189,548,555],[94,157,158,165,166,170,173,175,176,177,189,548,555],[94,158,166,167,170,173,175,176,177,189,548,555],[94,158,166,168,169,170,173,175,176,177,189,548,555],[94,157,158,166,169,170,173,175,176,177,189,548,555],[94,158,166,169,170,171,173,175,176,177,189,194,206,548,555],[94,158,166,169,170,171,173,175,176,177,189,194,197,548,555],[94,145,158,166,169,170,172,173,175,176,177,179,189,194,206,548,555],[94,158,166,169,170,172,173,175,176,177,179,189,194,203,206,548,555],[94,158,166,170,172,173,174,175,176,177,189,194,203,206,548,555],[92,93,94,95,96,97,98,99,100,101,102,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,548,555],[94,158,166,169,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,177,189,548,555],[94,158,166,170,173,175,176,177,178,189,206,548,555],[94,158,166,169,170,173,175,176,177,179,189,194,548,555],[94,158,166,170,173,175,176,177,180,189,548,555],[94,158,166,170,173,175,176,177,181,189,548,555],[94,158,166,169,170,173,175,176,177,184,189,548,555],[94,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,548,555],[94,158,166,170,173,175,176,177,186,189,548,555],[94,158,166,170,173,175,176,177,187,189,548,555],[94,158,163,166,170,173,175,176,177,179,189,197,548,555],[94,158,166,169,170,173,175,176,177,189,190,548,555],[94,158,166,170,173,175,176,177,189,191,207,210,548,555],[94,158,166,169,170,173,175,176,177,189,194,196,197,548,555],[94,158,166,170,173,175,176,177,189,195,197,548,555],[94,158,166,170,173,175,176,177,189,197,207,548,555],[94,158,166,170,173,175,176,177,189,198,548,555],[94,155,158,166,170,173,175,176,177,189,194,200,206,548,555],[94,158,166,170,173,175,176,177,189,194,199,548,555],[94,158,166,169,170,173,175,176,177,189,201,202,548,555],[94,158,166,170,173,175,176,177,189,201,202,548,555],[94,158,163,166,170,173,175,176,177,179,189,194,203,548,555],[94,158,166,170,173,175,176,177,189,204,548,555],[94,158,166,170,173,175,176,177,179,189,205,548,555],[94,158,166,170,172,173,175,176,177,187,189,206,548,555],[94,158,166,170,173,175,176,177,189,207,208,548,555],[94,158,163,166,170,173,175,176,177,189,208,548,555],[94,158,166,170,173,175,176,177,189,194,209,548,555],[94,158,166,170,173,175,176,177,178,189,210,548,555],[94,158,166,170,173,175,176,177,189,211,548,555],[94,158,161,166,170,173,175,176,177,189,548,555],[94,158,163,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,207,548,555],[94,145,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,189,212,548,555],[94,158,166,170,173,175,176,177,184,189,548,555],[94,158,166,170,173,175,176,177,189,202,548,555],[94,145,158,166,169,170,171,173,175,176,177,184,189,194,197,206,209,210,212,548,555],[94,158,166,170,173,175,176,177,189,194,213,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,217,219,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,217,218,482,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,218,219,501,546,548,555],[85,94,158,166,170,173,175,176,177,189,219,482,483,548,555],[85,94,158,166,170,173,175,176,177,189,219,482,548,555],[85,89,94,158,166,170,173,175,176,177,189,216,217,218,219,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,217,218,219,501,546,548,555],[83,84,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,548,555,608,774,777],[94,158,166,170,173,175,176,177,189,548,555,662],[94,158,166,170,173,175,176,177,189,548,555,664,671,672,770,771],[94,158,166,170,173,175,176,177,189,548,555,654,658,659,661],[94,158,166,170,173,175,176,177,189,548,555,662,671,672,771],[94,158,166,170,173,175,176,177,189,548,555,659,662],[94,158,166,170,173,175,176,177,189,548,555,654,660,661,662],[94,158,166,170,173,175,176,177,189,548,555,1549,1555,1556,1558],[94,158,166,170,173,175,176,177,189,548,555,610,616,629,630,631,634],[94,158,166,170,173,175,176,177,189,548,555,640],[94,158,166,170,173,175,176,177,189,548,555,640,641],[94,158,166,170,173,175,176,177,189,548,555,614,616,617],[94,158,166,170,173,175,176,177,189,548,555,614,616],[94,158,166,170,173,175,176,177,189,548,555,614],[94,158,166,170,173,175,176,177,189,548,555,609,614,620,621],[94,158,166,170,173,175,176,177,189,548,555,609,614,620],[94,158,166,170,173,175,176,177,189,548,555,628],[94,158,166,170,173,175,176,177,189,548,555,1551,1557],[94,158,166,170,173,175,176,177,189,548,555,1552],[94,158,166,170,173,175,176,177,189,548,555,1551,1552,1553,1555],[94,158,166,170,173,175,176,177,189,548,555,1551],[94,158,166,170,173,175,176,177,189,548,555,609,615],[94,158,166,170,173,175,176,177,189,548,555,609],[94,158,166,170,173,175,176,177,189,548,555,609,611,615,656],[94,158,166,170,173,175,176,177,189,548,555,611],[94,158,166,170,173,175,176,177,189,548,555,609,610,611,612,613],[94,158,166,170,173,175,176,177,189,548,555,1002,1003],[94,158,166,170,173,175,176,177,189,548,555,986,987],[94,158,166,170,173,175,176,177,189,548,555,986],[94,158,166,170,173,175,176,177,189,548,555,987,989],[94,158,166,170,173,175,176,177,189,548,555,986,992,993],[94,158,166,170,173,175,176,177,189,548,555,986,988,989,990,992,993,994],[94,158,166,170,173,175,176,177,189,548,555,989,990,991],[94,158,166,170,173,175,176,177,189,548,555,989,992,994],[94,158,166,170,173,175,176,177,189,548,555,989],[94,158,166,170,173,175,176,177,189,548,555,989,992],[94,158,166,170,173,175,176,177,189,548,555,986,988],[94,158,166,170,173,175,176,177,189,548,555,968,1030],[94,158,166,170,173,175,176,177,189,548,555,968],[94,158,166,170,173,175,176,177,189,548,555,649,650],[94,158,166,170,173,175,176,177,189,548,555,649,650,651,652],[94,158,166,170,173,175,176,177,189,548,555,649,651],[94,158,166,170,173,175,176,177,189,548,555,649],[94,158,166,170,173,175,176,177,189,548,555,1457],[94,158,166,170,173,175,176,177,189,548,555,1457,1458],[94,158,166,170,173,175,176,177,189,548,555,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842],[94,158,166,170,173,175,176,177,189,548,555,811],[94,158,166,170,173,175,176,177,189,548,555,811,821],[94,158,166,170,173,175,176,177,189,548,555,601,602],[94,158,166,170,173,175,176,177,189,548,555,857],[94,158,166,170,172,173,175,176,177,189,214,548,555,857],[94,158,166,170,173,175,176,177,189,548,555,848,855],[94,158,166,170,173,175,176,177,189,547,548,551,555,855,857],[94,158,166,170,173,175,176,177,189,548,555,810,844,851,853,854],[94,158,166,170,173,175,176,177,189,548,555,849,855,856],[94,158,166,170,173,175,176,177,189,547,548,551,555,852,857],[94,158,166,170,173,175,176,177,189,214,548,555,857],[94,158,166,170,173,175,176,177,189,548,555,849,851,857],[94,158,166,170,173,175,176,177,189,548,555,851,855,857],[94,158,166,170,173,175,176,177,189,548,555,851],[94,158,166,170,173,175,176,177,189,548,555,846,847,850],[94,158,166,170,173,175,176,177,189,548,555,843,844,845,851,857],[85,94,158,166,170,173,175,176,177,189,548,555,851,857,1118,1119],[85,94,158,166,170,173,175,176,177,189,548,555,851,857],[94,158,166,170,173,175,176,177,189,504,548,555],[94,158,166,170,173,175,176,177,189,452,515,516,548,555],[94,158,166,170,173,175,176,177,189,224,225,227,239,263,378,389,497,548,555],[94,158,166,170,173,175,176,177,189,227,258,259,260,262,497,548,555],[94,158,166,170,173,175,176,177,189,227,395,397,399,400,402,497,499,548,555],[94,158,166,170,173,175,176,177,189,227,261,298,497,548,555],[94,158,166,170,173,175,176,177,189,225,227,238,239,245,251,256,377,378,379,388,497,499,548,555],[94,158,166,170,173,175,176,177,189,497,548,555],[94,158,166,170,173,175,176,177,189,234,240,259,279,374,548,555],[94,158,166,170,173,175,176,177,189,227,548,555],[94,158,166,170,173,175,176,177,189,220,234,240,548,555],[94,158,166,170,173,175,176,177,189,406,548,555],[94,158,166,170,173,175,176,177,189,403,404,406,548,555],[94,158,166,170,173,175,176,177,189,403,405,497,548,555],[94,158,166,170,172,173,175,176,177,189,279,476,494,548,555],[94,158,166,170,172,173,175,176,177,189,350,353,369,374,494,548,555],[94,158,166,170,172,173,175,176,177,189,322,494,548,555],[94,158,166,170,173,175,176,177,189,382,548,555],[94,158,166,170,173,175,176,177,189,381,382,383,548,555],[94,158,166,170,173,175,176,177,189,381,548,555],[91,94,158,166,170,172,173,175,176,177,189,220,227,239,245,251,257,259,263,264,277,278,345,375,376,389,497,501,548,555],[94,158,166,170,173,175,176,177,189,224,227,261,298,395,396,401,497,548,549,555],[94,158,166,170,173,175,176,177,189,261,548,549,555],[94,158,166,170,173,175,176,177,189,224,278,447,497,548,549,555],[94,158,166,170,173,175,176,177,189,548,549,555],[94,158,166,170,173,175,176,177,189,227,261,262,548,549,555],[94,158,166,170,173,175,176,177,189,398,548,549,555],[94,158,166,170,173,175,176,177,189,264,377,380,387,548,555],[85,94,158,166,170,173,175,176,177,189,452,548,555],[94,158,166,170,173,175,176,177,187,189,234,249,548,555],[94,158,166,170,173,175,176,177,189,234,249,548,555],[85,94,158,166,170,173,175,176,177,189,319,548,555],[85,94,158,166,170,173,175,176,177,189,249,548,555],[85,94,158,166,170,173,175,176,177,189,240,249,452,548,555],[94,158,166,170,173,175,176,177,189,234,305,319,320,531,538,548,555],[94,158,166,170,173,175,176,177,189,304,532,533,534,535,537,548,555],[94,158,166,170,173,175,176,177,189,355,548,555],[94,158,166,170,173,175,176,177,189,355,356,548,555],[94,158,166,170,173,175,176,177,189,238,240,307,308,548,555],[94,158,166,170,173,175,176,177,189,240,314,315,548,555],[94,158,166,170,173,175,176,177,189,240,309,317,548,555],[94,158,166,170,173,175,176,177,189,314,548,555],[94,158,166,170,173,175,176,177,189,232,240,307,308,309,310,311,312,313,314,317,548,555],[94,158,166,170,173,175,176,177,189,240,307,314,315,316,318,548,555],[94,158,166,170,173,175,176,177,189,240,308,310,311,548,555],[94,158,166,170,173,175,176,177,189,308,310,313,315,548,555],[94,158,166,170,173,175,176,177,189,536,548,555],[94,158,166,170,173,175,176,177,189,240,548,555],[85,94,158,166,170,173,175,176,177,189,228,525,548,555],[85,94,158,166,170,173,175,176,177,189,206,548,555],[85,94,158,166,170,173,175,176,177,189,261,296,548,555],[85,94,158,166,170,173,175,176,177,189,261,389,548,555],[94,158,166,170,173,175,176,177,189,294,299,548,555],[85,94,158,166,170,173,175,176,177,189,295,503,548,555],[85,89,94,158,166,170,172,173,175,176,177,189,215,216,217,218,219,501,545,548,555],[94,158,166,170,172,173,175,176,177,189,240,548,555],[94,158,166,170,172,173,175,176,177,189,239,244,325,342,384,385,389,444,446,497,498,548,555],[94,158,166,170,173,175,176,177,189,277,386,548,555],[94,158,166,170,173,175,176,177,189,501,548,555],[94,158,166,170,173,175,176,177,189,226,548,555],[85,94,158,166,170,173,175,176,177,189,231,234,449,465,467,548,555],[94,158,166,170,173,175,176,177,187,189,234,449,464,465,466,548,555],[94,158,166,170,173,175,176,177,189,458,459,460,461,462,463,548,555],[94,158,166,170,173,175,176,177,189,460,548,555],[94,158,166,170,173,175,176,177,189,464,548,555],[94,158,166,170,173,175,176,177,189,249,413,414,416,548,555],[85,94,158,166,170,173,175,176,177,189,240,407,408,409,410,415,548,555],[94,158,166,170,173,175,176,177,189,413,415,548,555],[94,158,166,170,173,175,176,177,189,411,548,555],[94,158,166,170,173,175,176,177,189,412,548,555],[85,94,158,166,170,173,175,176,177,189,249,295,503,548,555],[85,94,158,166,170,173,175,176,177,189,249,502,503,548,555],[85,94,158,166,170,173,175,176,177,189,249,503,548,555],[94,158,166,170,173,175,176,177,189,342,343,548,555],[94,158,166,170,173,175,176,177,189,343,548,555],[94,158,166,170,172,173,175,176,177,189,498,503,548,555],[94,158,166,170,173,175,176,177,189,372,548,555],[94,157,158,166,170,173,175,176,177,189,371,548,555],[94,158,166,170,173,175,176,177,189,234,240,246,248,350,363,367,369,446,449,486,487,494,498,548,555],[94,158,166,170,173,175,176,177,189,240,289,311,548,555],[94,158,166,170,173,175,176,177,189,350,361,364,369,548,555],[85,94,158,166,170,173,175,176,177,189,231,234,350,353,369,372,406,453,454,455,456,457,468,469,470,471,472,473,474,475,548,549,555],[94,158,166,170,173,175,176,177,189,231,234,259,350,357,358,359,362,363,548,555],[94,158,166,170,173,175,176,177,189,194,240,259,361,368,449,450,494,548,555],[94,158,166,170,173,175,176,177,189,365,548,555],[94,158,166,170,172,173,175,176,177,187,189,228,240,244,254,286,287,290,342,345,410,444,445,486,497,498,499,501,548,549,555],[94,158,166,170,173,175,176,177,189,231,232,234,548,555],[94,158,166,170,173,175,176,177,189,350,548,555],[94,157,158,166,170,173,175,176,177,189,259,286,287,344,345,346,347,348,349,498,548,555],[94,158,166,170,173,175,176,177,189,369,548,555],[94,157,158,166,170,173,175,176,177,189,233,234,244,248,284,350,357,358,359,360,361,364,365,366,367,368,487,548,555],[94,158,166,170,172,173,175,176,177,189,284,285,357,498,499,548,555],[94,158,166,170,173,175,176,177,189,259,287,342,345,350,446,498,548,555],[94,158,166,170,172,173,175,176,177,189,497,499,548,555],[94,158,166,170,172,173,175,176,177,189,194,494,498,499,548,555],[94,158,166,170,172,173,175,176,177,187,189,220,234,239,246,248,251,254,261,281,286,287,288,289,290,325,326,328,331,333,336,337,338,339,341,389,444,446,494,497,498,499,548,555],[94,158,166,170,172,173,175,176,177,189,194,548,555],[94,158,166,170,173,175,176,177,189,227,228,229,257,494,495,496,501,503,548,549,555],[94,158,166,170,173,175,176,177,189,224,225,497,548,555],[94,158,166,170,173,175,176,177,189,418,548,555],[94,158,166,170,172,173,175,176,177,189,194,206,236,402,406,407,408,409,410,416,417,548,549,555],[94,158,166,170,173,175,176,177,187,189,206,220,234,236,248,251,287,326,331,341,342,395,422,423,424,430,433,434,444,446,494,497,548,555],[94,158,166,170,173,175,176,177,189,251,257,264,277,287,345,497,548,555],[94,158,166,170,172,173,175,176,177,189,206,228,239,248,287,428,494,497,548,555],[94,158,166,170,173,175,176,177,189,448,548,555],[94,158,166,170,172,173,175,176,177,189,418,431,432,441,548,555],[94,158,166,170,173,175,176,177,189,494,497,548,555],[94,158,166,170,173,175,176,177,189,347,487,548,555],[94,158,166,170,173,175,176,177,189,248,286,389,503,548,555],[94,158,166,170,172,173,175,176,177,187,189,226,331,391,395,424,430,433,436,494,548,555],[94,158,166,170,172,173,175,176,177,189,264,277,395,437,548,555],[94,158,166,170,173,175,176,177,189,227,288,389,439,497,499,548,555],[94,158,166,170,172,173,175,176,177,189,206,410,497,548,555],[94,158,166,170,172,173,175,176,177,189,261,288,389,390,391,400,418,438,440,497,548,555],[91,94,158,166,170,172,173,175,176,177,189,286,443,501,503,548,555],[94,158,166,170,173,175,176,177,189,340,444,548,555],[94,158,166,170,172,173,175,176,177,187,189,234,237,239,240,246,248,254,263,264,277,287,290,326,328,338,341,342,389,422,423,424,425,427,429,444,446,494,503,548,555],[94,158,166,170,172,173,175,176,177,189,194,264,430,435,441,494,548,555],[94,158,166,170,173,175,176,177,189,267,268,269,270,271,272,273,274,275,276,548,555],[94,158,166,170,173,175,176,177,189,281,332,548,555],[94,158,166,170,173,175,176,177,189,334,548,555],[94,158,166,170,173,175,176,177,189,332,548,555],[94,158,166,170,173,175,176,177,189,334,335,548,555],[94,158,166,170,172,173,175,176,177,189,238,239,240,244,245,498,548,555],[94,158,166,170,172,173,175,176,177,187,189,226,228,246,250,286,289,290,324,444,494,499,501,503,548,555],[94,158,166,170,172,173,175,176,177,187,189,206,230,237,238,248,250,287,442,487,493,498,548,555],[94,158,166,170,173,175,176,177,189,357,548,555],[94,158,166,170,173,175,176,177,189,358,548,555],[94,158,166,170,173,175,176,177,189,240,251,486,548,555],[94,158,166,170,173,175,176,177,189,359,548,555],[94,158,166,170,173,175,176,177,189,233,548,555],[94,158,166,170,173,175,176,177,189,235,247,548,555],[94,158,166,170,172,173,175,176,177,189,235,239,246,548,555],[94,158,166,170,173,175,176,177,189,242,247,548,555],[94,158,166,170,173,175,176,177,189,243,548,555],[94,158,166,170,173,175,176,177,189,235,236,548,555],[94,158,166,170,173,175,176,177,189,235,291,548,555],[94,158,166,170,173,175,176,177,189,235,548,555],[94,158,166,170,173,175,176,177,189,237,281,330,548,555],[94,158,166,170,173,175,176,177,189,329,548,555],[94,158,166,170,173,175,176,177,189,234,236,237,548,555],[94,158,166,170,173,175,176,177,189,237,327,548,555],[94,158,166,170,173,175,176,177,189,234,236,548,555],[94,158,166,170,173,175,176,177,189,286,389,548,555],[94,158,166,170,173,175,176,177,189,486,548,555],[94,158,166,170,172,173,175,176,177,189,206,246,248,252,286,389,443,446,449,450,451,477,478,481,485,487,494,498,548,555],[94,158,166,170,173,175,176,177,189,300,303,305,306,319,320,548,555],[85,94,158,166,170,173,175,176,177,189,217,219,249,479,480,548,555],[85,94,158,166,170,173,175,176,177,189,217,219,249,479,480,484,548,555],[94,158,166,170,173,175,176,177,189,373,548,555],[94,158,166,170,173,175,176,177,189,259,280,285,286,350,351,352,353,354,356,369,370,372,375,443,446,497,499,548,555],[94,158,166,170,173,175,176,177,189,319,548,555],[94,158,166,170,172,173,175,176,177,189,324,494,548,555],[94,158,166,170,173,175,176,177,189,324,548,555],[94,158,166,170,172,173,175,176,177,189,246,292,321,323,325,443,494,501,503,548,555],[94,158,166,170,173,175,176,177,189,300,301,302,303,305,306,319,320,502,548,555],[91,94,158,166,170,172,173,175,176,177,187,189,206,235,236,248,254,286,287,290,389,441,442,444,494,497,498,501,548,555],[94,158,166,170,173,175,176,177,189,231,234,241,548,555],[94,158,166,170,173,175,176,177,189,285,287,419,422,548,555],[94,158,166,170,173,175,176,177,189,285,420,488,489,490,491,492,548,555],[94,158,166,170,172,173,175,176,177,189,281,497,548,555],[94,158,166,170,172,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,284,369,548,555],[94,158,166,170,173,175,176,177,189,283,548,555],[94,158,166,170,173,175,176,177,189,285,338,548,555],[94,158,166,170,173,175,176,177,189,282,284,497,548,555],[94,158,166,170,172,173,175,176,177,189,230,285,419,420,421,494,497,498,548,555],[85,94,158,166,170,173,175,176,177,189,234,240,318,548,555],[85,94,158,166,170,173,175,176,177,189,232,548,555],[94,158,166,170,173,175,176,177,189,222,223,548,555],[85,94,158,166,170,173,175,176,177,189,228,548,555],[85,94,158,166,170,173,175,176,177,189,234,304,548,555],[85,91,94,158,166,170,173,175,176,177,189,286,290,501,503,548,555],[94,158,166,170,173,175,176,177,189,228,525,526,548,555],[85,94,158,166,170,173,175,176,177,189,299,548,555],[85,94,158,166,170,173,175,176,177,187,189,206,226,293,295,297,298,503,548,555],[94,158,166,170,173,175,176,177,189,234,261,498,548,555],[94,158,166,170,173,175,176,177,189,234,426,548,555],[85,94,158,166,170,172,173,175,176,177,187,189,224,226,299,397,501,502,548,555],[85,94,158,166,170,173,175,176,177,189,215,216,217,218,219,501,546,548,555],[85,86,87,88,89,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,392,393,394,548,555],[94,158,166,170,173,175,176,177,189,392,548,555],[85,89,94,158,166,170,172,173,174,175,176,177,187,189,214,215,216,217,218,219,220,226,254,259,436,464,499,500,503,546,548,555],[94,158,166,170,173,175,176,177,189,511,548,555],[94,158,166,170,173,175,176,177,189,513,548,555],[94,158,166,170,173,175,176,177,189,517,548,555],[94,158,166,170,173,175,176,177,189,519,548,555],[94,158,166,170,173,175,176,177,189,521,522,523,548,555],[94,158,166,170,173,175,176,177,189,527,548,555],[90,94,158,166,170,173,175,176,177,189,505,510,512,514,518,520,524,528,530,540,541,543,547,548,549,550,555],[94,158,166,170,173,175,176,177,189,529,548,555],[94,158,166,170,173,175,176,177,189,539,548,555],[94,158,166,170,173,175,176,177,189,295,548,555],[94,158,166,170,173,175,176,177,189,542,548,555],[94,157,158,166,170,173,175,176,177,189,285,419,420,422,488,489,491,492,544,546,548,555],[94,158,166,170,173,175,176,177,189,214,555],[94,158,166,170,173,175,176,177,189,548,555,669],[94,158,163,166,170,172,173,174,175,176,177,189,206,207,214,548,555,843],[94,158,166,170,173,175,176,177,189,548,555,768],[94,158,159,166,170,173,175,176,177,189,194,548,555,673,674,754,767],[94,158,166,170,173,175,176,177,189,548,555,769],[94,158,166,170,173,175,176,177,189,548,555,596],[94,158,166,170,173,175,176,177,189,548,555,594,596],[94,158,166,170,173,175,176,177,189,548,555,585,593,594,595,597,599],[94,158,166,170,173,175,176,177,189,548,555,583],[94,158,166,170,173,175,176,177,189,548,555,586,591,596,599],[94,158,166,170,173,175,176,177,189,548,555,582,599],[94,158,166,170,173,175,176,177,189,548,555,586,587,590,591,592,599],[94,158,166,170,173,175,176,177,189,548,555,586,587,588,590,591,599],[94,158,166,170,173,175,176,177,189,548,555,583,584,585,586,587,591,592,593,595,596,597,599],[94,158,166,170,173,175,176,177,189,548,555,599],[94,158,166,170,173,175,176,177,189,548,555,581,583,584,585,586,587,588,590,591,592,593,594,595,596,597,598],[94,158,166,170,173,175,176,177,189,548,555,581,599],[94,158,166,170,173,175,176,177,189,548,555,586,588,589,591,592,599],[94,158,166,170,173,175,176,177,189,548,555,590,599],[94,158,166,170,173,175,176,177,189,548,555,591,592,596,599],[94,158,166,170,173,175,176,177,189,548,555,584,594],[94,158,166,170,173,175,176,177,189,548,555,789],[94,158,166,170,173,175,176,177,189,548,555,1017],[94,158,166,170,173,175,176,177,189,548,555,1016],[85,94,158,166,170,173,175,176,177,189,548,555,1459],[94,158,166,170,173,175,176,177,189,548,555,996],[94,158,166,170,173,175,176,177,189,548,555,995,996,997,998],[94,158,166,170,173,175,176,177,189,548,555,567],[94,158,166,170,173,175,176,177,189,548,555,559,561,567],[94,158,166,170,173,175,176,177,189,548,555,560,561],[94,158,166,170,173,175,176,177,189,548,555,561,567,571],[94,158,166,170,173,175,176,177,189,548,555,560],[94,158,166,170,173,175,176,177,189,548,555,561,567],[94,158,166,170,173,175,176,177,189,548,555,559,560,561,566],[94,158,166,170,173,175,176,177,189,548,555,559,561],[94,158,166,170,173,175,176,177,189,548,555,560,561,573],[94,158,166,170,173,175,176,177,189,194,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,999],[94,158,159,166,170,173,175,176,177,180,189,194,197,548,555,1000,1001,1004,1009],[94,158,166,170,173,175,176,177,189,548,555,983],[94,158,166,170,171,173,175,176,177,189,548,555,982,984,985,1005,1006,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,1000,1009,1010,1560],[85,94,158,166,170,173,175,176,177,189,548,555,1009,1010,1560],[94,158,159,166,169,170,173,175,176,177,180,189,194,197,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,982,984,1009,1010,1560],[94,158,163,166,170,173,175,176,177,189,548,555,1009],[94,158,166,170,173,175,176,177,189,548,555,1010,1452,1453,1455,1549,1550,1559,1560],[85,94,158,166,170,172,173,175,176,177,179,189,548,555,980,981,982,984,985,1001,1005,1007,1008,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,1554],[94,109,112,115,116,158,166,170,173,175,176,177,189,206,548,555],[94,112,158,166,170,173,175,176,177,189,194,206,548,555],[94,112,116,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,189,194,548,555],[94,106,158,166,170,173,175,176,177,189,548,555],[94,110,158,166,170,173,175,176,177,189,548,555],[94,108,109,112,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,179,189,203,548,555],[94,158,166,170,173,175,176,177,189,214,548,555],[94,106,158,166,170,173,175,176,177,189,214,548,555],[94,108,112,158,166,170,173,175,176,177,179,189,206,548,555],[94,103,104,105,107,111,158,166,169,170,173,175,176,177,189,194,206,548,555],[94,112,121,129,158,166,170,173,175,176,177,189,548,555],[94,104,110,158,166,170,173,175,176,177,189,548,555],[94,112,139,140,158,166,170,173,175,176,177,189,548,555],[94,104,107,112,158,166,170,173,175,176,177,189,197,206,214,548,555],[94,112,158,166,170,173,175,176,177,189,548,555],[94,108,112,158,166,170,173,175,176,177,189,206,548,555],[94,103,158,166,170,173,175,176,177,189,548,555],[94,106,107,108,110,111,112,113,114,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,140,141,142,143,144,158,166,170,173,175,176,177,189,548,555],[94,112,132,135,158,166,170,173,175,176,177,189,548,555],[94,112,121,122,123,158,166,170,173,175,176,177,189,548,555],[94,110,112,122,124,158,166,170,173,175,176,177,189,548,555],[94,111,158,166,170,173,175,176,177,189,548,555],[94,104,106,112,158,166,170,173,175,176,177,189,548,555],[94,112,116,122,124,158,166,170,173,175,176,177,189,548,555],[94,116,158,166,170,173,175,176,177,189,548,555],[94,110,112,115,158,166,170,173,175,176,177,189,206,548,555],[94,104,108,112,121,158,166,170,173,175,176,177,189,548,555],[94,112,132,158,166,170,173,175,176,177,189,548,555],[94,124,158,166,170,173,175,176,177,189,548,555],[94,104,108,112,116,158,166,170,173,175,176,177,189,548,555],[94,106,112,139,158,166,170,173,175,176,177,189,197,212,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,608,774,1013],[94,158,166,170,173,175,176,177,189,548,555,556],[94,158,166,169,170,172,173,174,175,176,177,179,189,194,203,206,213,214,548,555,556,557,558,568,569,570,572,574,576,578,579,580,600,604,605,606,607,608],[94,158,166,170,173,175,176,177,189,548,555,556,557,558,575],[94,158,166,170,173,175,176,177,189,548,555,558],[94,158,166,170,173,175,176,177,189,548,555,577],[94,158,166,170,173,175,176,177,189,548,555,603],[94,158,166,170,173,175,176,177,189,548,555,568,579,608],[94,158,166,170,173,175,176,177,189,548,555,568,608],[94,158,166,170,173,175,176,177,189,548,555,658,663],[94,158,166,170,173,175,176,177,189,548,555,635,644,774],[94,158,166,170,173,175,176,177,189,548,555,609,610,612,613,614,616,618,619,622,623,629,643,644,655,657],[94,158,166,170,173,175,176,177,189,548,555,618,637,638,644],[94,158,166,170,173,175,176,177,189,548,555,618,619,626,644],[94,158,166,170,173,175,176,177,189,548,555,609,616,618,619,622,644],[94,158,166,170,173,175,176,177,189,548,555,576],[94,158,166,170,173,175,176,177,189,548,555,609,618,619,622,635,639,644],[94,158,166,170,173,175,176,177,189,548,555,608,666,774],[94,158,161,166,170,173,175,176,177,189,194,548,555,608,609,614,616,618,619,622,623,626,627,635,636,639,642,643,644,655,664,665,771,774],[94,158,166,170,173,175,176,177,189,548,555,576,618,619,622,644],[94,158,166,170,173,175,176,177,189,548,555,618,637,638,639,644],[94,158,166,170,173,175,176,177,189,548,555,576,618,623,624,625,644],[94,158,161,166,170,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,667,771,774],[94,158,161,166,170,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,771,774],[94,158,166,170,173,175,176,177,189,548,555,576,609,614,616,618,619,622,623,624,625,626,627,629,635,637,638,639,642,644,645,646,647,648,653],[94,158,161,166,170,172,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,667,668,670,771,774],[94,158,166,170,173,175,176,177,189,548,555,609,616,618,619,622,623,637,638,639,644,646],[94,158,166,170,173,175,176,177,189,548,555,654,661],[94,158,166,170,173,175,176,177,189,548,555,753],[94,158,166,170,173,175,176,177,189,548,555,755,756,767],[94,158,166,170,173,175,176,177,189,548,555,757,758],[94,158,166,170,173,175,176,177,189,548,555,755,756,757,759,760,765],[94,158,166,170,173,175,176,177,189,548,555,756,757],[94,158,166,170,173,175,176,177,189,548,555,765],[94,158,166,170,173,175,176,177,189,548,555,766],[94,158,166,170,173,175,176,177,189,548,555,757],[94,158,166,170,173,175,176,177,189,548,555,755,756,757,760,761,762,763,764],[94,158,166,170,173,175,176,177,189,548,555,744],[94,158,166,170,173,175,176,177,189,548,555,744,747],[94,158,166,170,173,175,176,177,189,548,555,679,739,742,744,745,746,747,748,749,750,751,752],[94,158,166,170,173,175,176,177,189,548,555,675,677,747],[94,158,166,170,173,175,176,177,189,548,555,744,745],[94,158,166,170,173,175,176,177,189,548,555,676,744,746],[94,158,166,170,173,175,176,177,189,548,555,677,679,681,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,679,681,683,684],[94,158,166,170,173,175,176,177,189,548,555,679,681,683],[94,158,166,170,173,175,176,177,189,548,555,676,679,681,682,684],[94,158,166,170,173,175,176,177,189,548,555,675,677,678,679,680,681,682,683,684,685,686,739,740,741,742,743],[94,158,166,170,173,175,176,177,189,548,555,675,677,678,681],[94,158,166,170,173,175,176,177,189,548,555,677,678,681],[94,158,166,170,173,175,176,177,189,548,555,681,684],[94,158,166,170,173,175,176,177,189,548,555,675,676,678,679,680,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,675,676,677,681,744],[94,158,166,170,173,175,176,177,189,548,555,681,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,683],[94,158,166,170,173,175,176,177,189,548,555,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"8caeb65fdc3bfe0d13f86f67324fcb2d858ed1c55f1f0cce892eb1acfb9f3239","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"e3905f6902f0b69e5eefc230daa69fdd4ab707a973ec2d086d65af1b3ea47ef0","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"80219a97fd3ce5f91af5c355a264844027a899b3d9a3cd41cf6f3bc5947edc95","impliedFormat":1},{"version":"89444c76f16bf7994e230d98e1bc3f01d654de04ef02f60430d9a98d5b450a8b","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"fac3e88881b35d3a757ed891ac912b2674792c25e2a1a74e1f5fbc72d19a9792","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"e1c1a0b4d1ead0de9eca52203aeb1f771f21e6238d6fcd15aa56ac2a02f1b7bf","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"6fe28249ac0c7bc19a79aa9264baf00efbd080e868dbe1d3052033ad1c64f206","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"c51b3c3f6c5aa5b121124f4b593996826aab90667f95de88c1ff13c1736e11ba","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"a74edb3bab7394a9dbde529d60632be590def2f5f01024dbd85441587fbfbbe0","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"00049ccc87f3f37726db03c01ca68fe74fd9c0109b68c29eb9923ebec2c76b13","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"3856f7d31d0c47ec0dded3ec552519a3cd6639c1ad7be279dd1b31abffd8cc85","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"4ebf086fa2e5ef2b65133a944cb3f8ab518a22087727dfbfc802a3654c396f2f","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"55cdbeebe76a1fa18bbd7e7bf73350a2173926bd3085bb050cf5a5397025ee4e","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"2f4e6b4d39426a1b85ecf4bdeb9dddbf4d9b3397d95d8555d46f925c9519ec7d","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"89d5d28d4f57e000b836ac273079be1b75710e28ce14750d081fb420d37e2ca5","impliedFormat":1},{"version":"fd4e24ccff3966390600d7f5d6aa1fed5a512e92ada735ea5fbc933d313ad3d3","impliedFormat":1},{"version":"b7cddfe1aa6b86b5fad3c9ccb30d05b3ccb165aebbf112f48d2d8a5f69dd98b1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"bd2c7ada3dee03653d3f601011d30072194bc3970cd93208f9588fbdc0c69347","impliedFormat":1},{"version":"e480da45d32313e7174b265674da504f075f59ef326852f0c5a5d863b438ae85","impliedFormat":1},{"version":"ad54850f61fcf5d014e11be80d2f46fea9265cfa7e77456da876f7833ef81769","impliedFormat":1},{"version":"6f7c9e8bd2b5b6a080b07080065f94900bd3c7e5ebbd3047bc33fcce2fab1dd8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"da5950ee2a90721df6f3fba45f5d05308f7e4c35835392215dd2cd404505e2de","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"f42d5fed19610d485c646a0c430e768115567d078c7fc855c57b0c578b3d6cd3","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"d5630f2ad9b4541e5ce891648121022f9412ecdca1820baa1f0104f70fd7eff7","impliedFormat":1},{"version":"4d15375ab13497104bc8fe56fdef2b5fd6853f29255737d23a33fa306ff7fd69","impliedFormat":1},{"version":"2cd3fc1d0d6a1e85baffd2d4f50f5efb192b5446eef567e97c94765402f0aad4","impliedFormat":1},{"version":"e4cbf2f1e89ecccaddd2c045e600ae41b732295953fb06247c7dcbc2d281ed30","impliedFormat":1},{"version":"6dcedaef57dff0d79a05ab0ab602cde74db803d1e765468bf91263786a383e1b","impliedFormat":1},{"version":"8c1697d90c394a6fd955b98eae01238eff628e129b987a68aea10f898a48e7da","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"d10d63718e1646c2279e3b33831f82c60e31f622b2b7020f1196409ca4c09242","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"a22dd55aa4d39906252000ab8e8a1b83b195eef7f4274eb51e457c1f11cf6580","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"612d9da66bb046a9c1e2e8d026245ded881fc4b9f98cbfae714415d57ee0ae0b","impliedFormat":1},{"version":"32c2ad9494dad5d11b0564a619fee18f388db6c1e9e2cd3c360b3122549691eb","impliedFormat":1},{"version":"6c301d40aec56a74ec7bd7324e31a728dadf9bfba3e96def02938d3d973534ec","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"25a5f6fd3a2243c859eddc99ab5fba11d970af2fe7a5df9c32b7668f76f97b01","impliedFormat":1},{"version":"8d207e1f9d2c30d6f77dfa693f3827c3fbf0d89240297e10bdfe1041d433df68","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"273782b8454e78f6a8b30d2cfbf6860499c930595095fcc1689637115f0eddda","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"7029e566b8df176f703fb59fd437a38670c7a0e02c58b2d66dfb5b2e2b2defdb","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"e9f147ecca73d9346a4c073432843c159ccbe50bdcb678a78f6da10eae2cecf4","impliedFormat":1},{"version":"de061f7d72bd65c06fc1419f841dfdcb29a8e22fe6fa527d1e6eb20b897d4de0","impliedFormat":1},{"version":"663beafc2446079574570cba86e9b15f986f908ddb1b01274509970126fee945","impliedFormat":1},{"version":"a3102887d5058bf4cb5b37fa6964c09e9527c42053b3b5c642b89878620748de","impliedFormat":1},{"version":"0aaaa1727edd29673d85c9b26d7ca4d54e5407a48586903c51b48b7f7d196f61","impliedFormat":1},{"version":"d35bca0b261bff02635758c48e8ab99c61c420d0dfabbcf467e847171d876b7d","impliedFormat":1},{"version":"3bc12c40d90c342ff88a3d876996c555ed5cbee5fe8c3308a240b321f401ee46","impliedFormat":1},{"version":"ba130768aae855a5477e9e148e5c879548e6e7ccbcc56fd1934c8a18ea5b7569","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b499af2054a037a162b3b72cd886f48bbf32a3502c865c6e29fac7d2ab3ce0b5","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"48773ca557b0319c2ee62ae249cf52a81709e8be139920d6479a66274de7c4ed","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"4cceef18d7f088e797a463e90b7a9dad10c6bc667724b7686e3e740ae00122be","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"cc1954b539604b1e562319119ac7e888172208b32ca873f9a357a92c826bd046","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"706dd95827e7ebaabda91d5db2b755233e0952d98570e9c032b0f066a15c1177","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"cd9304972e6d616197fb44fce00540a904f38b54306a1951b5dbeaf3c01ab5bd","impliedFormat":1},{"version":"77438e2c397a3db78407621cfc57241a305b310ddea2c185f1d555248297f587","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"a6805fcafed712aea7759f8bc731014f9d22738c1d6ef9d43b8091d1d48346d5","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"142617b3cdf902b69c6464c9fbd942b60ab3e733ca18c032b19e0f7e2adbefe8","impliedFormat":1},{"version":"0b603555f1881f87256ffd6344d3e3ed6d466c2e701eabf381f28be8c2125892","impliedFormat":1},{"version":"897e4f7662488e3ecc79e743bdd3b78f13bdb69a97851afa5b440c4211e32ea9","impliedFormat":1},{"version":"e2e1c6d3b2d93add5200bd7bc1a8cccb4e446836b2111ece45db8683a2c765de","impliedFormat":1},{"version":"251b03d5cd243854ce870d9a9a39f491faf69898c5d6b5eee28cc7649c57417b","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"2c4de79f406d137390608e8c0a44fba2ff8e00bacfcae7c9d1781fef10e9440d","impliedFormat":1},{"version":"07ba23a10465791be5d22deaf5ef7de7658774ddff53721e5ea17fedea1bc721","impliedFormat":1},{"version":"dca8c645c5afeb03b1ecedbf16323f33e7d0afaa6256c8e047e6e38087a97f53","impliedFormat":1},{"version":"775f181bd4a533d6f8b5e55ec1d9f1624559720ae8a70e9432258da26b38d27c","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"7715134a0cf07dd41a9da2895d708625a3a303a0385e355ecaaf0b8bfaef2550","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"cd8ce8d68567f62dd580b3c3c37777ac3f5b81944c7417f5ea83030eab533385","impliedFormat":1},{"version":"e5c939d896565dcac0f6fbdbada11284e7728ef26a069561c09aa5aa4a788393","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49af4b52f0d4d2304c5f2c6fe5fab3e153e0acc38830d0202821b877c097dd02","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"e68b8e5a1df7c1be2bc105141456ecba70215806e1c28bfbc5c12bfce4be6e68","impliedFormat":1},{"version":"511c8f02329808d47d00b859c532ae9115590048b17325a946c74dac48428650","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"b5f9e66625783eefcbe3d2da074b2e7ba2066d61ce3fc6ef4f22805ad946cab4","impliedFormat":1},{"version":"e37115962d284b9f7a37c2bdd2add50f88365dde41f5e0ff591ffc48a8ec7575","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"f89488602bec98a142072fae7ea5ba99431a569ff580c64b7be39896474799d8","impliedFormat":1},{"version":"bbbc47961f39a57df103cf4ca3bb8f8732b4b6678a18225a0aa76d59c466956c","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"2ffb043dc5163458e473b7010859f86e01dc4edffcae0a93d885d028b426a546","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"04b7b2e0832dfd3c31e81df3975e8d8fda28e7ff999b0aa2932608a8f6661d5c","impliedFormat":1},{"version":"ca2d34c6ed5cbd3070b8b6f32f42ae54adcc6499c1e4b99f0a5798b3f27cc653","impliedFormat":1},{"version":"9ec68995e66dd6b9dac834bf5ae85fde802714ea2e82151a5d1d53ef01b463ef","impliedFormat":1},{"version":"5c4d626b4902f2ef8a1cc146d761d276cef988016dc674e3b98fbad70e64bc9f","impliedFormat":1},{"version":"fdfaa0aad899524962e2955287b5b991ffe3be50f64e02eb60c933ca44644a94","impliedFormat":1},{"version":"53c972a0f9bc3a4ec70fff7314123ea8cfcf75b3703046f767d2dc1eea87b2fb","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"7303b45138d2511035056a5901a1490ebdcbf055cbb1276f8629c5121cbe733e","impliedFormat":1},{"version":"27f874cd5327507eeff699a74567f60c1215b94509f4308633a7b01922471ed2","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"2c6cf04bc525caf6546e859e8ef10bfb9573837ec0bc5ec7b53a7b1b8ca72781","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"87cc05fe13108f02e12da7e3efd8e360fef78d96a0c9e11408ea1b1b9fb3e03d","impliedFormat":1},{"version":"1abbf67c218d23c2ce76887caac2df6c7dab3d97ba2b65348432b876f510002a","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"dad97c99382889e9c7d1a9d8275500ff71235130fae9f8916fdbf3641d56e592","impliedFormat":1},{"version":"a6dba407fc287f1e25454e75028c91bbc00675f2d1c4e8b3edcc36c08611a486","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"c0723195c85e19656d6b5b9fdb81d3f3403c1ae4679e722c6ea058c516b38d12","impliedFormat":1},{"version":"b55eb9f72166093b5460d34b34f5d8699c968de3bc3fc696e40f2c93f2ebf650","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"1594da19968752a22b2ac48c2d0e60575700e745c577a8a4a676b841238ad5bb","impliedFormat":1},{"version":"e0cee12109e0a10a4c3d6769fcc7644b7c1ea7f52365bea51728f5af29f8a137","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"3536968defef8a75514f547ead5e2e9c1e984820290ec9b00c5fdfb6ef786535","impliedFormat":1},{"version":"d83773870080c30a230e322ce13a9c6f3398e8dacea4ea8a83e26370f3bac23e","impliedFormat":1},{"version":"dcfeaf98d66314fec29a9076c4290e45d0b196a65827becc19138e9c7b855f37","impliedFormat":1},{"version":"6849fe9210fe4946d5f085bfed36758f33dc6ae15a751338d178dd4daa017c46","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"ffae4e1e06aa848a1e4bcef162cd1c48e5909b26223515981310af9c036bdfc7","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"34e16eb7c31768a11a08aebcfb3d70d7b8f0b016197e98d8419e566ceae6d6c8","impliedFormat":1},{"version":"f94ec1f7e4b709d26960306c9082a7a1b728a6e13089346aa48ba57c74cbf47e","impliedFormat":1},{"version":"9a11cb4033405e96c247cd5aa29790212aaffdd127869e8a5219103f0b389fd5","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"aff5213585cb72e94054dfe17250ff315f3569b3919d1ef1ad235f37c4ee894e","impliedFormat":1},{"version":"fb2ea35e1be6388d722d7725e2b49c697d34d9c890c3b96758faaeb86d35cef8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"456006a6975b26c0a1785feddae165f6d307e2d601ffde27e21fc4a790e448a4","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"1fe0d18b111e1145a7e7601855bccd4ca20f24e3b9a5aba6bb1fa9d1a7059170","impliedFormat":1},{"version":"5632c3c26d420c063eebe64c45b1248b9492a67bf44f1d0c57e9dc8f6cf449bb","impliedFormat":1},{"version":"0df5aa619ab12993a39ea6dae062ee46eadbb4d738916460e636ada52bced75b","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"10ab7be91f87ebe8916b62cf28af2e45b5601fc7b0e311adf838f912c6b31dd8","impliedFormat":1},{"version":"bc636fbc08e0979ceb7eb0731a33000283d77a33b62e1f71ee65be50394e40ba","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"75bbd3be047d539988a0ff0b56384ef7a6a25f3b676ad96bee547d44c31622a7","impliedFormat":1},{"version":"42960001a776b089ade681ab5cfddc936e0afb0615133ec1841f3dee89d3e1bf","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"da47712b394d944328245482603bc6f416d3949b67c9392279caab595076b510","affectsGlobalScope":true,"impliedFormat":1},{"version":"37d0071d8f0a06dc55c2c5e0ec3391affd4fd107c53410bf358196ec0bf3923f","impliedFormat":1},{"version":"b213dad76ca37fd552274c9499056e1c0d9c1bd38a55bb7f68b22ba6b84c3ad7","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"5a49adaef698b7ad7e6127949fa1b0bbd3d46b7cbd11c54e392a4dcdd51f5190","impliedFormat":1},{"version":"6ee598cdfdd0fa52039dca135b3dfff7b49035dc13292143e0a93843e3861967","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"2489bf04d77dc025ba67f49f1a56eb24b9db477d5ff88123d887e163ed1776aa","impliedFormat":1},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"0b77b819b5417775fccb20c678293cf614c054a5b1a65421a5b933a9124ba998","impliedFormat":1},{"version":"eb5acb58487367e502d994b57e2c58255d8241f481ea8efa8e79af23af3f41c2","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"6715dc4eb59c8ea9abe2b78c235ed331dc710a06fe56798868dbc4d40cd1b707","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"5a3ea721d03a361ccbdd7390ccd75f6e84cbca3a3f01f4b331ecc9af31890c49","impliedFormat":1},{"version":"e7dfaee4af38d45b1cab8a1ee0b3bc1f85ddcf64545ed391d675d78ae6526274","affectsGlobalScope":true,"impliedFormat":1},{"version":"e8daa443eaf9a27fd382cc1f8ebe30330c0f4d89511cfb469166874806751d35","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"07ed3ddab975995eea41b22f3010506fb9f5fb301d04820b07d7a1aee5477d7c","impliedFormat":1},{"version":"969d8b0965849f4bae7cab0ba90bd1e1220e95999c2c6f01117fa7500901c017","impliedFormat":1},{"version":"6ec840ee5e2bc103f557fe38b1d585ee250540468713d7634ee066de372bf332","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"e297c0a524edee7677939122f90027bfbe5f2698939d9a85728e5044b39c7124","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"b62381cae176db34f003cc6172ee8f3e0122014889d66391aa73698105cf4934","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"41eb514d9ce0a6e87957f08a4b7af70d93f87637f37dee706e2d92a6601c25a9","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"7bd01f0f28cd3aeb2046274d85208e245965f6f2948edf4f7b2057bcf9f22ccc","impliedFormat":99},{"version":"d2f2cf2b8cc92bea913cda4a076e0f790b23a21e84f989d12f0116a7fe3906e0","impliedFormat":99},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5b20bc288ee49989c95b20847fc93b96bf61cc0845598897a6a53a967dd7d07","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"b8fd21e843bf2d078a96fdd1df42f22aa48b246ca368f78f856ef91ba372c830","affectsGlobalScope":true},"7b550dda9686c16f36a17bf9051d5dbf31e98555b30d114ac49fc49a1e712651","68937fe95088ffe97ac986edf4dcfb88cfe71df2e67db91ba03ee165779dedd7",{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"16fe60bb544cfedfd2b5bb2f7d0b3957be7978706d57d9f06edc9c0c8dbdba23","impliedFormat":99},{"version":"de4a612aa8f1704af486f701e21993c786ba7d8cfed55fffa6684026aea2346e","impliedFormat":99},{"version":"c73fdf42528325dd17940937ed787b15ae3445c6a2dae1a2b74bc4d87d337ca2","impliedFormat":99},{"version":"e8e17dfef3cfa9f0847ac93dd535a9896af7fb57c1a1b164484bb1b0ee4a25d8","impliedFormat":99},{"version":"51d2ffea2d1ee4a81c775938588c1e16620281adb60cbc26579a2fc6baa10bd2","impliedFormat":99},{"version":"148debd12783ded0a60d115daeacd8136f77757ae89a05c4e18de6dd77646fd2","impliedFormat":99},{"version":"0088b02dca63c47b273a140d0a3944bdc6dc2eb765fff0ca98e3c3a2786b3a5a","impliedFormat":99},{"version":"a651d06b780fa354231f19b040cbcde484bede3218885752b4f9e9a8f72d3b5f","impliedFormat":99},{"version":"06e26f75bed4c8389a8a63f0e6d6a9068038873dc95d8d1338e8c370a0ae8bc3","impliedFormat":99},{"version":"a2155e2675fd1af52b0b70779371c28611cdd1076b29d0f68bf93b983e5ddce0","impliedFormat":99},{"version":"4a61989c1daaed8bd7a2c7d87f9fae8d98f1205b3de3b3131478f972d997380b","impliedFormat":99},{"version":"8543104cedbc17231ef94fe02530b612c466d436be7201a8a94f65694cb7875d","impliedFormat":99},{"version":"c074e054c9db79055d37d7d70131e9a3234b8186773b3edb617c13f80bcf8774","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"aa26f7bd84a3ea2b59408071accf147ba8cb0d5991aabf1b9ba22d66ce36086e","impliedFormat":99},{"version":"5c3c3eb0a2b0ac00106e768c4920c56f2629545e9376649eed9671057469586a","impliedFormat":99},{"version":"5a9834c603c65aee5cba0c1d6b3c7aee85cdc7862832a23165c6aa4139c165f2","impliedFormat":99},{"version":"a40c35037b336bca6ffdd1b3e1384b1c1ac867a3f1588f70d1d6d6f821f49385","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"8887e70871f697fa42ad7cdf32168db60ec2d6760c173c97973e35377fe5d83b","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":99},{"version":"a0e59ab6a42cc19a9d9fe4bd8eaacb9de9a12e25d4bf5027bc721df36f1a842e","impliedFormat":99},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":99},{"version":"be836d4f889a447168231c052f4fd5f3dc34538fc158d293120106a13dd2c8cb","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"49ab4f1d153a252779958fc87b700743d32b5ffa42addd70ae23ad3f429daa5c","impliedFormat":99},{"version":"53cf4076f42b29b8d411259d168d51b3a0274c42c8814e5b44dfa8803a35d4fc","impliedFormat":99},{"version":"a39461ee1f27cf3e6cfd63d21045713d26d521da55ea4d8efccb705f689e6dbb","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"2b39c6cf59088713babbfc3e20ee85f1375d40e66953156fa658346b8346f24f","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"4ffba3c5848b4fe62ee59b754fd5f256ad9656a0db6d37b9a2a8cb40dfc7ac21","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"59e5e964b84fdb2378e9455e4e59405030e4ed2b4c6f891ce395f17796af3cbb","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"90ba95a763101bb61b8a799731a2ed60b5016b8135c1a2d5186862d4b534d4a1","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"7cd92bc6808967f175063fce65f8d1d60a07d752b79ed20aa2eba9a2b8ed0820","impliedFormat":99},{"version":"59de9e07114b1da6421bf15359a951f1b762cb19c3a6abfc2e671a5ac2599dff","impliedFormat":99},{"version":"84e977ab810a75dc103b38cad1e019d3ab8f4ec9042b8f09f117b7d18c4076f0","impliedFormat":99},{"version":"9b98988fc27cbf7f08598871f275999540bcc267a8a47453866a198f93601959","impliedFormat":99},{"version":"2b1795d67cf9aa20167c72d795d4db6badea9cea646d9181869b5ca08c778479","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"a9c8214ef27f5762c807802fd468b50fafae5c909d4376b67b3bfccaf8f99e13","impliedFormat":99},{"version":"94c8f4fa20a287054630c9521a9c5c7e8f74e806bcc8444503b6dc378853dd09","impliedFormat":99},{"version":"bf5b6e99ad75e9caaad2a66b4cc05e8ab73c7ec0ba58525eb6be7baa49be0cd2","impliedFormat":99},{"version":"632711a108accd4533a327ac3b60d0518fa536dfdc2d9e146222c16aa348506e","impliedFormat":99},{"version":"81b5530dfc39e1946dd771dd5526b84710e11d6a371812b1dc2e8eef9e12aa61","impliedFormat":99},{"version":"5d0aaba829cea9511506bc2bcda7255ae1764366b8a7809cbd6dc220531f360f","impliedFormat":99},{"version":"cd71a7eb14e09b7962e8955c6b0376be99044d814e7d60d7d1e9d095716b89f3","impliedFormat":99},{"version":"681cf9768031b874fafdd81a20e8a20ca6c92a5cbf12e86532a69182e315b065","impliedFormat":1},{"version":"32727845ab5bd8a9ef3e4844c567c09f6d418fcf0f90d381c00652a6f23e7f6e","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"40ba6c32eb732a09e4446ade5cb6ad0c147f186f9c9dc6878b90b4418ad9f6ea","impliedFormat":1},{"version":"fdd814741843f85c98281522c58f5a646590ba9019fad2efaa95987655e0611b","impliedFormat":1},{"version":"c78aff4fb58b28b8f642d5095fc7eeb79f00e652a67caa19693af1adabb833c9","impliedFormat":1},{"version":"f80a08ced8818dc99359c0acd5b3f12762e1ce53758007759b0d4e503cbf4a5e","impliedFormat":1},{"version":"37935fa7564bcc6e0bc845b766a24391098d26f7c8245d6e8ab37bc016816e94","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"0f8a263f4c8595c8a07de52e3f3927640c44386c1aa2984de9eae50d75e613b2","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"346fffde7c32da87c2196eb7494422449dc2ca82d3b4e6bf55be1d1a33ffc2b0","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8b5875e4958528042103fdd775e106a7f76bafc29709f0690df9a7d2241d52a7","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"aef26cf95593c8ace1c62c4724f9afac77bdfa756fb8a00613cd152117cb2f43","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"268fd6d9f2e807a39a6c5aa654b00f949feb63d3faa7dd0f9bba7dde9172159c","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"36eb5babc665b890786550d4a8cb20ef7105673a6d5551fbdd7012877bb26942","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"e5b810845dedef5aa0d682efa61be20dcb2e28c27a64ff5cc4cfc122f3f3980f","impliedFormat":1},{"version":"7a8ec10b0834eb7183e4bfcd929838ac77583828e343211bb73676d1e47f6f01","impliedFormat":1},{"version":"b07f64ff6ec710998a62b07377fbda0ab4c313ba1f0055bfe9faa22cffedd47c","impliedFormat":1},{"version":"72ef861333c7870a29fc0e248129c4afc0741c0716a74c3da409d05e41ff1322","impliedFormat":99},"49641a5eaa0ab31c12d312d53b724213afccbbbcc8e43a1119479d5e71858e61",{"version":"168aa44771e55cda628eeb5fbb6f626f6f263d827967b119e8c06abd7f1362ec","signature":"435a1e418e8338be3f39614b96b81a9aa2700bc8c27bc6b98f064ff9ce17c363"},{"version":"174e5bf2d8c80c676eb42959eaac341892bd946f18b68d2f5efe99a38b152fb3","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},{"version":"54460343eae2288e42a041a096da60709dc8150684cb79679770b68a46a33601","impliedFormat":99},{"version":"2faebfa830ae4cfbfb58e48b0ec20a2a63882d776f0ca36ec7155d45cf1b7f2d","impliedFormat":99},{"version":"dc6e3ce502651ecc523590588213bfb986c04b60f5265d37f76662b824425903","impliedFormat":99},{"version":"10a7c14e787e6b77f7079b80a8013f8349b37d115673f59bb7d5c17616df4e09","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"f329dfad7970297cbf07ddc8fce2ad4a24e2a3855917c661922ef86eb24dd1f1","impliedFormat":1},{"version":"841784cfa9046a2b3e453d638ea5c3e53680eb8225a45db1c13813f6ea4095e5","affectsGlobalScope":true,"impliedFormat":1},{"version":"646ef1cff0ec3cf8e96adb1848357788f244b217345944c2be2942a62764b771","impliedFormat":1},{"version":"3cfb7c0c642b19fb75132154040bb7cd840f0002f9955b14154e69611b9b3f81","impliedFormat":1},{"version":"8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b","impliedFormat":1},{"version":"d16f1c460b1ca9158e030fdf3641e1de11135e0c7169d3e8cf17cc4cc35d5e64","impliedFormat":1},{"version":"a934063af84f8117b8ce51851c1af2b76efe960aa4c7b48d0343a1b15c01aedf","impliedFormat":1},{"version":"e3c5ad476eb2fca8505aee5bdfdf9bf11760df5d0f9545db23f12a5c4d72a718","impliedFormat":1},{"version":"462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","impliedFormat":1},{"version":"5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","impliedFormat":1},{"version":"d0570ce419fb38287e7b39c910b468becb5b2278cf33b1000a3d3e82a46ecae2","impliedFormat":1},{"version":"3aca7f4260dad9dcc0a0333654cb3cde6664d34a553ec06c953bce11151764d7","impliedFormat":1},{"version":"a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988","impliedFormat":1},{"version":"b58f396fe4cfe5a0e4d594996bc8c1bfe25496fbc66cf169d41ac3c139418c77","impliedFormat":1},{"version":"45785e608b3d380c79e21957a6d1467e1206ac0281644e43e8ed6498808ace72","impliedFormat":1},{"version":"bece27602416508ba946868ad34d09997911016dbd6893fb884633017f74e2c5","impliedFormat":1},{"version":"2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a","impliedFormat":1},{"version":"82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46","impliedFormat":1},{"version":"b4966c503c08bbd9e834037a8ab60e5f53c5fd1092e8873c4a1c344806acdab2","impliedFormat":1},{"version":"3d3208d0f061e4836dd5f144425781c172987c430f7eaee483fadaa3c5780f9f","impliedFormat":1},{"version":"34a8a5b4c21e7a6d07d3b6bce72371da300ec1aed58961067e13f1f4dc849712","impliedFormat":1},{"version":"ad763fa0c24ede2b818eb6598c12dd581451f94688fb9ed963beba20d513a7ec","signature":"90ec9100c29e008c3d9194acd818e2cfa6dc6e177154bc8e10c5959aa35619ed"},{"version":"779a08ff2ae8855cf826d32062335e0d09d86ca4ee4b288b2cec6dd8d5ec338c","signature":"2ce3ed9a3d1ebf72e4a45464c2407c21c315fc9fa491ef2b02f3608b07450a4a"},{"version":"32ffc97bd9635972ee63e77eff2ef2839aeb2e842822a492136d822f70681655","signature":"83eb632f831cb93e76e3e2cc7d8f4917d3893481ce5a15751bd0685330881ad9"},{"version":"a756b102aea002f51675c9febd4c26527ef3086bc672951b654830fdbef92373","signature":"21aacd1a7a2350329abc64f2a4578e9603ea963d797fafb8672981192c61c18b"},{"version":"7c8545a7c4ec6978ebb4af07475d76007eb084c4f7f45aeba7ee817eeb4316e1","signature":"37eeb4730a8634d70c51bd1933939e66cba31f30600246487f3568cee7742b9c"},{"version":"243d22ed8f1d2e7fa0d99e4f25a87b791ebf16769fb48de846c9ac34ed07903e","signature":"c7ebc7a24a51984ec8231424b974dbbacc456090c53301dd1d8502ed95083a1b"},{"version":"62fa0410d02bd18aa492a8c465834a3bb8691e9478205041e297c08f30d57f46","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"464f35f5b7856d95d7277dc3472e8edc4c8a232b12601b667da28c44f98e0294","signature":"27c3eb3de9a7dd7e04b74bf3e630702bd7946ac349ae0ae66174d26591b25ead"},{"version":"b7ca2f47522d4ea41e65ff92c4c6dd9c4c8260da7c456a7631a9c88dc056b4d0","impliedFormat":1},{"version":"4f01e4d0959f9125b89e5737eb1ca2bfa69fd6b7d6126eba22feb8b505b00cde","impliedFormat":1},{"version":"4363a1adb9c77f2ed1ca383a41fbab1afadd35d485c018b2f84e834edde6a2c7","impliedFormat":1},{"version":"1d6458533adb99938d041a93e73c51d6c00e65f84724e9585e3cc8940b25523f","impliedFormat":1},{"version":"b0878fbd194bdc4d49fc9c42bfeeb25650842fe1412c88e283dc80854b019768","impliedFormat":1},{"version":"a892ea0b88d9d19281e99d61baba3155200acced679b8af290f86f695b589b16","impliedFormat":1},{"version":"03b42e83b3bcdf5973d28641d72b81979e3ce200318e4b46feb8347a1828cd5d","impliedFormat":1},{"version":"8a3d57426cd8fb0d59f6ca86f62e05dde8bfd769de3ba45a1a4b2265d84bac5a","impliedFormat":1},{"version":"afc6e1f323b476fdf274e61dab70f26550a1be2353e061ab34e6eed180d349b6","impliedFormat":1},{"version":"7c14483430d839976481fe42e26207f5092f797e1a4190823086f02cd09c113c","impliedFormat":1},{"version":"828a3bea78921789cbd015e968b5b09b671f19b1c14c4bbf3490b58fbf7d6841","impliedFormat":1},{"version":"69759c42e48938a714ee2f002fe5679a7ab56f0b5f29d571e4c31a5398d038fe","impliedFormat":1},{"version":"6e5e666fa6adeb60774b576084eeff65181a40443166f0a46ae9ba0829300fcb","impliedFormat":1},{"version":"1a4d43bdc0f2e240395fd204e597349411c1141dd08f5114c37d6268c3c9d577","impliedFormat":1},{"version":"874e58f8d945c7ac25599128a40ec9615aa67546e91ca12cbf12f97f6baf54ff","impliedFormat":1},{"version":"da2627da8d01662eb137ccd84af7ffa8c94cf2b2547d4970f17802324e54defc","impliedFormat":1},{"version":"07af06b740c01ed0473ebdd3f2911c8e4f5ebf4094291d31db7c1ab24ff559aa","impliedFormat":1},{"version":"ba1450574b1962fcf595fc53362b4d684c76603da5f45b44bc4c7eeed5de045b","impliedFormat":1},{"version":"b7903668ee9558d758c64c15d66a89ed328fee5ac629b2077415f0b6ca2f41bc","impliedFormat":1},{"version":"c7628425ee3076c4530b4074f7d48f012577a59f5ddade39cea236d6405c36ba","impliedFormat":1},{"version":"28c8aff998cc623ab0864a26e2eb1a31da8eb04e59f31fa80f02ec78eb225bcd","impliedFormat":1},{"version":"78d542989bdf7b6ba5410d5a884c0ab5ec54aa9ce46916d34267f885fcf65270","impliedFormat":1},{"version":"4d95060af2775a3a86db5ab47ca7a0ed146d1f6f13e71d96f7ac3b321718a832","impliedFormat":1},{"version":"6708cd298541a89c2abf66cceffc6c661f8ee31c013f98ddb58d2ec4407d0876","impliedFormat":1},{"version":"2e90928c29c445563409d89a834662c2ba6a660204fb3d4dc181914e77f8e29d","impliedFormat":1},{"version":"84be1b8b8011c2aab613901b83309d017d57f6e1c2450dfda11f7b107953286a","impliedFormat":1},{"version":"d7af890ef486b4734d206a66b215ebc09f6743b7fb2f3c79f2fb8716d1912d27","impliedFormat":1},{"version":"7e82c1d070c866eaf448ac7f820403d4e1b86112de582901178906317efc35ad","impliedFormat":1},{"version":"c5c4f547338457f4e8e2bec09f661af14ee6e157c7dc711ccca321ab476dbc6d","impliedFormat":1},{"version":"223e233cb645b44fa058320425293e68c5c00744920fc31f55f7df37b32f11ad","impliedFormat":1},{"version":"1394fe4da1ab8ab3ea2f2b0fcbfd7ccbb8f65f5581f98d10b037c91194141b03","impliedFormat":1},{"version":"086d9e59a579981bdf4f3bfa6e8e893570e5005f7219292bf7d90c153066cdfc","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"cd66a32437a555f7eb63490509a038d1122467f77fe7a114986186d156363215","impliedFormat":1},{"version":"f53d243499acfacc46e882bbf0bf1ae93ecea350e6c22066a062520b94055e47","impliedFormat":1},{"version":"65522e30a02d2720811b11b658c976bff99b553436d99bafd80944acba5b33b4","impliedFormat":1},{"version":"76b3244ec0b2f5b09b4ebf0c7419260813820f128d2b592b07ea59622038e45c","impliedFormat":1},{"version":"66eb7e876b49beff61e33f746f87b6e586382b49f3de21d54d41313aadb27ee6","impliedFormat":1},{"version":"69e8dc4b276b4d431f5517cd6507f209669691c9fb2f97933e7dbd5619fd07b7","impliedFormat":1},{"version":"361a647c06cec2e7437fa5d7cdf07a0dcce3247d93fbf3b6de1dc75139ff5700","impliedFormat":1},{"version":"fe5726291be816d0c89213057cd0c411bb9e39e315ed7e1987adc873f0e26856","impliedFormat":1},{"version":"1b76990de23762eb038e8d80b3f9c810974a7ed2335caa97262c5b752760f11a","impliedFormat":1},{"version":"5e050e05fe99cd06f2d4ad70e73aa4a72961d0df99525e9cad4a78fa588f387b","impliedFormat":1},{"version":"4ff327e8b16da9d54347b548f85675e35a1dc1076f2c22b2858e276771010dd2","impliedFormat":1},{"version":"f767787945b5c51c0c488f50b3b3aeb2804dfd2ddafcb61125d8d8857c339f5a","impliedFormat":1},{"version":"14ab21a9aeff5710d1d1262459a6d49fb42bed835aa0f4cfc36b75aa36faddcd","impliedFormat":1},{"version":"ba3c4682491b477c63716864a035b2cfdd727e64ec3a61f2ca0c9af3c0116cfd","affectsGlobalScope":true,"impliedFormat":1},{"version":"b222d32836d745e1e021bb10f6a0f4a562dd42206203060a8539a6b9f16523f0","impliedFormat":1},{"version":"a3f6d8995864820a0207b7ef4ce1ed6a8dd2fccc7e70d015da15034807c38e1c","impliedFormat":1},{"version":"651df11341eff0b769fb83af75b1872e6cedf406674c5eaa2650551aceb5a816","impliedFormat":1},{"version":"07ebbdb7c2d53d5ea6ced77bc9efeaf047da6de8593be42346a5784fa8871229","signature":"f15b4a91c10bf30ff3708a5e3968c0a52ca4e86a4b9a5b1c4fc8e9b5f1292f21"},{"version":"eea1925d19682b88f1004cf6d620f1b8804379c3089cb3af805204795228ddc1","signature":"c788a956032e3d31d6b5f98a301941197e46d04ae68ad49a403f353405037607"},{"version":"7fe9d657e96c9a7c00c2b4b661558719f371995052348a27e47ecae052f329dc","signature":"f8da50cd80c40c61a7cccdcb36106cb8221bd19d4525d30d75575526310f35c2"},{"version":"1e9d4c3b066e0228765cef074a9bf49d0b8d3af461c97f2c511e8f7110b56235","signature":"e59faabf094dd75dcf08847ca1b8ae16daf269ca02c744c521b38ed5d297578c"},{"version":"9e8f19cd697c9fdfeadb3559f0ca26514075f7054ae1ba17e8fbc8684d12eb5a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9dfe98d745dfc706198852e032c3c936092a4484951872c0d18639cf2ab698cf","signature":"326009db2b0a0f3290cbde2271f91e51869148a3e286ccc3ac41559f6c830642"},{"version":"44310bdd7b563dd9b2df0e1b0c45a789492e6620b4718778fca88e029f1765f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"030523c514236148e0f25975ef74b2490c391cb9596cd668728f7db5b0702e08","signature":"411166ca21d7ae80b737bb1c7e9900e897ac7b27c06647063730ba8207522e46"},{"version":"4109b1fb0cffaf1f2bcaaecb49378d7971b795502484bc2dbe0d4deae60c8057","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ca52a606c31ddaae5f7c4055fccb3563cf7e85601c96dbf28b7d0795d2d21af0","signature":"82d82e5e9d2c282a1dddebbcc73d9ff5c89f60859cd8769d0f6eae7b76f3f4d1"},{"version":"edadad7db67a0db1a7d474ab075cc15b975d78780218a1860e14516208d7389c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78b3214dba4e86c41e4cdf5de5dcc853806ff74fad5df3dc1087cdbe19ad00e0","signature":"5c4de1b33c2b0c5b0b823fca5ba9e5dde3c995aa8a2c42db12b2e8b64f408805"},{"version":"37702123f9a2c93d39aeeec210394811ee157b6e32b600c5e083d271370f7733","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"45b82fd88270dd3d0aff39220b341eca789f77c82813dc6695b2058479d8dc28","signature":"3d67e5bdef7b1039301b51fc5ba5303c133b863f30e0de39c8aa1db5d897f1b6"},{"version":"15631c00468c358645d55b6267edf935ab0e94dea8748d619ae23a76f7388912","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f212078a0ecefabbbbc627b89e28c871b559c1c57a4dcae32c8d7c2b6c4f0a00","signature":"b495b6770d46b4ebe3dbe0c1f6d64e8b50ea14ea8d2b63377813ae35672a4535"},{"version":"ec4e1ceba158e76dce8afd51f6b13caa6b67fe8e9d0c0686448b77fb76586adf","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9376f1759d1a75cabbab4521ccd44d0ca28e6b99c53820622d1b95cf759796c3","signature":"cff79d4609413671705c5c6a3938e666996270f808bc0f7306a97322fd16220c"},{"version":"a3dd5bfebf022e5ba282b11bb4eb12ee3946347b3c722cf9a63fd9212a2460bb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b562002637e45c4b3fd1a788528c278413d425c3402e0595539490fa670dc56a","signature":"41ab7d48373752fdda9cb29625f8e6caf1aa2092e6ea95b52f1275a2241193d6"},{"version":"355c388a5063eded314867e35c8071e8c8b1bb497d8332ecc0ae3d560193a74f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"5521661fe646b6d63cea776daf417bfe4093d24f190401ad3ebbc95ceb9157db","signature":"3cf58c6e2f986bec77722cf989aaf33509dd005c81cc2d52d40f76f08d75bc84"},{"version":"69ca880a3a93b31b209252e06877f4a79f9a1fc97d546257492c0734ebaea3f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a9a302a8cd4d0c581404b58535f90adcf072b9e3a2f5a09c31ddaae37ccf673e","signature":"c5d60e5f8e3d05ba1fb0a462cd5ccbd26c7eb4c5406441d8803362183520748c"},{"version":"fdaacbce1a58559bce1d428a5bb445dab6f82a051a66d516ebf3d00265571caa","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cadbf01db107d2e9cceb5ecdd3f8f0084d996f07f2fca714409247deb26f0b85","signature":"ed905f29ff05cd1d3cf260949314ca896356f5c8bc011fdee9d75cde0404c6a0"},{"version":"a1d1b662c8d1e1d8a20612adb2c4cecd718f3b0e57d283cad95afbee8a61d132","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea7a4796f350344bbf39b19518d78169bd66a819f787b0a4d1a6ad642dbce7a2","signature":"8f6ef9414b1d23dc0a927160ed27770f93c1fe749a9af0e626f5a0e8918d5fc1"},{"version":"1941ed0a7c9ff9106612c8fcc6ce92f24d656769a5d739599e4a6c34ad478df0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cc5bfa36a8f86eea23a84b78f27ba0f842426cefb5a13d4b53b80a331aaaa268","signature":"3dcde6c75b6c7fc3bc7fca2ca1273467974a8b54061edd648ed854bc4c5d842a"},{"version":"246ed9157ed5791bffc9037a1aa2a1b709a0fc75e67102d13e71d899dbbb5e0a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"27494de6a730122abf7b860cc02bf6ce805359bdcdf09095d6e9c9c63418aba2","signature":"959594ee2bcc74404b29a3049c85d0c7f2ae48df7e2b725160708a4a725fda88"},{"version":"5c7afbe31fb1e68671c798cfc06221cbade2f174e243566cf5b8fd0763e4ed52","signature":"233c0355b5b37601f3d2cb2fb9b35829c28220fbf3acc7314eb1d67509c3a2ce"},{"version":"433609291756b990304a258d06f4cf73d1a677643a8053d3a99fb435c668a4bd","signature":"9b3d204f4e4f901d253d5847771729af0cb7c39742f7be0ad468623b34612284"},{"version":"8062a9596b370d008b1cd13429487a2ed9d35b28dca9a0f0aa78609fd0a96f9d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fcc8fb0967eda9256921f3cfc521d49f3d473232d4c754c09de1d50faa996e35","signature":"37e053a2b54ed97cab1c0e0e6a0ed4d610d6011b24e4cef7633007fb573468e1"},{"version":"b28e8eddf4e5bc7659dd8369e31a71ee482d55ecfa6b84831fcc80228518b701","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"21382d3d9886a24696afabde6ba98ac0f2caedc6e62d848be109d4e2025bbf80","signature":"1cb1eafc264cf66f6de9548387f23915f3ae8d9c9d09a61e2fb253f96bac2397"},{"version":"1b04df451640794168e4fffb092f9823a6694c11d3daf55b4994f847a2987322","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fac17c78b79e957a330460096e2c511a77d7c22835a137659c599427ee68fb5e","signature":"c9b3903dbe7bb883044f93988644ad26177b48141ba87185d1961e89dcbe12e1"},{"version":"662d45c385c6903b15921917388cbcbf6fe34fa1720ab9c073e628496af5e56d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"50fd5c95036fcdd7101e2cdd2a336fe0d11b9cbfdcbd474d74a917c355ebc7ea","signature":"fc1013b6b18ebb6c273407c0999e92a60c29dd73779f14f7177a2635eb2cfceb"},{"version":"72121c537d221078e87c97dab842628674750a9d6aa2cf1b6166cfa664ed7382","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ef6f6d882e73aeae8a7cce4a2492cbf0e4829fc963e79312e443a7a16a6cfb3f","signature":"3ef82075343927dd481ff965c2af0f6ed87106013a8e27638a4df73e242a6592"},{"version":"c8d012f3fdd6a52336f10bec7e4509ea2dbb0ed1f2e996eac4b1eede14f9f88d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f59d410bcaf6b219caf38e6e9a39866eabbcc1106f74ad50c914e7e40cb4a24","signature":"0df22e821771646f47f81523421585e601758795cd91a75d41f4979d1f4519de"},{"version":"c71ae4fc28d6b2f5b78a95af5335129851cac857d19b25d5f06c8b014d3feea4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6bbcb670c4632ba69c7910572999840ac47a10cb49a1c437c5a52a40cbcdf10b","signature":"7df0381f3f4101bc88fad9da0c420421e97430cef4742bb04cbf41cdadb1da6f"},{"version":"8b59281611e7ebf3a35207733d85f8b50a33cdb5d08492334db32622e0cd3212","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"044b593c01d82184a7e0fb5c2ac98447d44ae217f1405f239c3e1e62ae379abd","signature":"922164a6b539bf53726ce54aac6bce9f648e478e5708604f3987fac02099f050"},{"version":"ab88aa6a3eb5f4f91c1a097e6b6002d35bb3b7517e1ce49c742a96d203758de2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1008e6f116f8169e368d70feccb510f8400b9fb204a699cd25cfeee835c88cc6","signature":"b0a0d651b6a03d5b1ef718277284d2b4a765def58e205311d2f1b825eea30c64"},{"version":"4455c74ed4fddb9ae9ba8eb37188fa934b6cf7fcd005053ddfb7dae7e5a6f072","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3eb6dafdd02c962b39b09be9e31c695603b58c794e141d4eed2f49318f0af433","signature":"1b21d73c687df72e347c8f38b456988260af0b5423ac03d9aa92966719676d87"},{"version":"9b44504aab0385b4934bf502c8c06a5540c50a0090a34f4c2fb50e43e03f5505","signature":"eb8914c7f3c2b49544d286aff55eba63c69d013385296fb3ad07e586e76000be"},{"version":"1f08dcb29ad688246447d04e3b7e2d5dcf65311c36c09db4bce1bca316f0201c","signature":"35ad6df8b0834debb0585c63d2bb84bb81802a7a0424a8f4ce6b0acb0513619f"},{"version":"61cb83a110e7a89d2ad5a0a4bd91fadb619619a98a16437189a856b51e1e9e0d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a794572bfb0885ec1de20b701e498a415c0480e4fb6c20dbaa125b374ae691cb","signature":"46d3c81ba82aaaeabab3baa813cbb84b4c9808ec7ad495ad5fdc27d9358aa314"},{"version":"1b5be2e53f79f1f20967a235c011841152b2ca9f79b3b0640468ba8a8cf5cd7c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"016e4353e0414c98c2ed2eb75df1509a38b457ddf000e9ff98125f82ea42c335","signature":"767c0245753b6e027a6f0550eb7b69bbd09d3f758aa1e9a07dcfe0992c0f629e"},{"version":"f76cdd7b076354cf26a7414ce5ebcef0a2a3384fdb6e406d4976c522349b6c93","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3ef6579bdd5883302539b5370c5670331f3584dff9e2ef14bab39991f00a29cb","signature":"1e894cd9840e407527a3f67119371f90d7ae5f8179905de7776a345ed4af6d32"},{"version":"325d7dc15c789cc6358327f36e7cf2a916e969017c2b78decf9475af91dc86c3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3b642738f6cd019494d207949a63b67b6ad9013bb3f1fb5f8fa36687879db5c4","signature":"f583f64899de9c7804febba31f2808aad09101b0d3342e42793c8588ed608f63"},{"version":"492bb2bed9c43e010cd0a50263969a75a72c6279f67654a6bb7def1a9cf74571","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"35a644763f50424937b17109eb552ddc767270a5526a0a827810ab86585dc53a","signature":"1b4159a10366adf4fd777a6bd595b4b846f1d3d37d07498c3498fd4b71e0f813"},{"version":"2e8ff3406c54b9e6ab83cde71a8e02eec9796f58626bfb39825d11e4e03d8bbb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"eac3a35ef0fe0ab083e8cf36a29a821e014dce98f56d597ed2dd815ff4cace46","signature":"7a79bbeb7030ac58672abb37f4776cade6988cd7cf9a535cffd548658d85f1bc"},{"version":"628364563c513db63865187fbf6afd635d3dc6cdbb8472e639a2c60fc3c13b86","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c39c138e6eafd63689438247533cbd792f2487cfccf0ae812f9cde3087d5c1b8","signature":"2a88f7e64f6f67e4eb7770b1678fc459b8ddb3bed0cb40734c9c257313affd02"},{"version":"dd81cddda6088f01807ea61dcc81cb09805f09cd222c1cdc07783262cea74cd9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c81045d6db59c1c83c84797bc1f78422a691ced9ca9a3554e2f8ef9aacc7dcfa","signature":"441f2bb4f78eb9fb86d27aceee4d92908b532110aa6b7fac80adede58bab926d"},{"version":"0496434a31fc8cf9e490e135e87ec3202dd0298290fcffbfbe4d93b3c180756d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bce05280900b4eb3333d10064531569c9fcfce6aa7aeca1ca03b2120539892a5","signature":"c96264be178c5e42597043c462b21cbc073618f43d5e1e88bd32a516068a2380"},{"version":"13c1ff47b7b02fad7d84109dc37613ba445a7a084561485dc9aef335c4c94a84","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5a22523635ca6f47c20b386b010ca1258aa19af5f4299f8752809c599315bcf","signature":"48c3a71b54800c134fcff4becfedca8347ff86645e004f8754d1e2b1385d1e9c"},{"version":"4895c3e936eb6d99b94541c611983585d9b5d805ff5a83b8f710e27f52dbafed","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f8f48136b4a2c472edf03e168e658ac758173fb38773f7849009ee0b39a81e5","signature":"d5b2231a838d0dbe8d3c0076afbb7e0544b2e318632b638adfe8c5743ddab6e2"},{"version":"cb12e3fd9be59bf0e28d6f18ebecbb52f8bd90258cf6dd2af77f7b12545a45bf","signature":"ff15a056114c88b5f7e043ca0ba2cc2a62b4154e462c09a965df6cbb91cfa4b6"},{"version":"1bbd283b943af0cce0c72cde1e843e55093235b7dd3fa7d22dbe957039c0ec76","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f69209ce0e934946c859c4cc6248ef4a2dc528f5baf9b4fcdea5cf3e08d9d38","signature":"e8f8aa08e63443a0cc63ce2f9fc9582addd622528fad2ecfc413b2a91c688fac"},{"version":"b1a319734a490e072da3cdb0e53c3d90aa63250323e29ef7d7d554acf5a174d1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8e79adf7f7c76bf3874257d3692b76744e5a6784c91cd27f93c30a8a5cc842eb","signature":"2c6835a2dd6319674e4b339215c071bf5896757157ae471324b1bd95868a5285"},{"version":"117c70fd7a19a5369ac35034f89ab731c6f6629d791e35334fb307b7042f89a7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad15d915012090304670ff62dd5e48d7694c11787f4884f51cf80f873dd40aa4","signature":"ef3c092bb7ed970d2273e55a61b12bc4741bca2219e1fe703350550c99ca6f42"},{"version":"38786de27bad4d667abb5f948b2357fce8de7448a52feb500965735a5f3a5a6e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fb5ce2f104fde415289b4febf27da970e40afefe42b87b15db28818ca9f94132","signature":"ccefe690307d556c5e44dd7ffb7d23faa0a3388926925ed0352107cd2efc4d9c"},{"version":"cb504109f1c6b549fde2eed46b44eaedb73ee7754b3f3a1c1c32bc5dd1693a8c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3613723853e81414c2e1220e3e0a696a997cc49e4390060b6ac7fd91aec150e4","signature":"0334a8b41901a52fc9195c66c97cee6aaaecc5aae79b9fc31f245a5df48b3ecc"},{"version":"d63995951dabffa413e3114fc0d39108a1c183939ef54616a8019d6061ad0651","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"082900437524e8bc6903f0d26479f590af1814cc5080fc502e07d38c951500be","signature":"24564adc2074d75d218bfe8b711580df8848afbd3ce2890184cd98e76529d387"},{"version":"1bbff9fec02470dc8837722310fbcb7de786f75d442f3e3116a291dba247caaf","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"34eb44fd7813a487540b6afa04da3175388b1841aa3612569e8aa07652363b69","signature":"9bada675e959a3571ad60f30eaee87c92b05fea94beca1f7fd212dc65734177a"},{"version":"fb371cac529d70b2eb453e41a785acab5429502dbc8655db1bd8ad27e0808825","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fda5dde4fccdc3b8a2fa710f43940d74532a63d457c6513e3ad065c3e151a31c","signature":"31444c6666f2df000d4c29edccbfa15ffbe4dd5ce7237cd47478a0b81b2f4443"},{"version":"e3348585394b19f9dbbdc623c9c267562bd28d3dd089b4259673c5bce452eb38","signature":"eea3073b37074a4dc266f913c309381cd52887cb77f47c2b85d4390dca658dd7"},{"version":"5ae8a275a25f0b2c93d2c11a3aedff3e73cea77f41ac7f49bb5b2d49bc970174","signature":"955903ca3f9b55fe1bb48e3d152fb647f88edad09aa6ec695ff8dbd8eaf9ebac"},{"version":"da528774ea74ae18aca17514a05c547a648fa364f68982ce02b56e9aaaa7b7d6","signature":"a9779efd000ab950e2097f789266a26af006c56a54d7bac966f2162262d4bbc7"},{"version":"0ceaa48c199fa1aab84399d7ab8ade3302ba2515e61956cadf13e218e3e3bc70","signature":"cf4519b131be7adbfefddb180ea2e0a1f5d2f520954d1acc4646aa4dedcc0eb0"},{"version":"57f3de13ad44d856ed5e540986c352003cf45bdc0e8a6492c38c31c60472c7b6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"d7030b68636fd9884a76ed71a2d9e081b22697c77cc0b05112f95a569f4f39cb","signature":"fb09ff619aa414f4b2d422c19ea116cacc6fec3220db1dd2b51e2ce8b7b1812d"},{"version":"37ffcf089bbb1b6ea4d4340c2b30c5110b76d6ea1b09b6d7f0cb87485976616c","signature":"a4c188535b372f30652897819a780b1631b6353cb42171a93dd2570c6a2722f8"},{"version":"852c367110c2934ed35a33ad276111aa9b3016ec92b36b86a37b43642ad9458d","signature":"92a24950b269736d532ea9daf0cdd8ea7361b70095f3468e59afea80af884516"},{"version":"5f1a16cce7b79b724102f3b562c5795f38cf4f9541c761d689e12ce450fcb19a","impliedFormat":99},{"version":"e004826eac62081f867c66dabd92d3ef7d126d93a70430a2c88429228c3ecc50","impliedFormat":99},{"version":"38d6857b58d2ac42442e396311c542062d4f0dad40f2adb496dd5fd0756ee400","impliedFormat":99},{"version":"34b7d1e2d15845cf08bcf5e3c01adbb92cea1ec27564ee249ba486cdfb28526c","impliedFormat":99},{"version":"0d0861810ff9d344ab37f055edbc4d14e68e2fa18e113ce8cc33aef9bb500b5f","signature":"2791178671f71d9b41ed9ef814e549d85cc6a77c61e2f8a87ec25f2176d9a3fc"},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"ff4bfe3c147eadc1b722a8ac9e4d350e66411b5c9022eabe5f4ada871c9adc41","impliedFormat":1},{"version":"f1b1170112db2e1a93ea317419a35652f0e9a87ca1612674f514e6f93791d4c2","signature":"512960c0e955a2324b34354dac25e3e4d431a1af4cd33077935eda5e95c8b7e1"},{"version":"87b826b5fbca67c0a88166bc23732a2c76368a2e71dcfb60ea756869b8bb1552","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"385f46cc82c76869625987ab0f448faec4f6b8534d14f59bbe986dc08212e42e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6d261c7be483027c9d281ba71314350ad8ce66efabf46f7b6099f39fe3b8d218","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c02d45c2a6bc50186c1972f44c31ff22e887b30205ce84607bb4661c0fdc9846","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7fc06e1e53688bb32ccd86e730e08bacd38d9ad7ad006a234211b230825efb2d","signature":"d95aac1823e54b4183acab8f7fe3bec5dd7bd4aa297f56004fbcb972d299e377"},{"version":"c2175e53ec5b411439a03c757c499d2bb30c48bef05e83f372b5095fc2f8fb3e","signature":"2c2a937dc3435a48e0ac2cc67072dad512dc5240282c217955d3fb55e2b6a4eb"},{"version":"6c05d0fcee91437571513c404e62396ee798ff37a2d8bef2104accdc79deb9c0","impliedFormat":1},{"version":"5990e5575acfc932889f3720cf5915b1f3514d66b9d0dbcb55d2b62130b2afd5","signature":"ec851dc73402d7cce3fe5da65eb1f2e56235ce4533de1c561ea9edd6ac4b941d"},{"version":"9b86423edac57f38f2652f623e0043eb2b47c1b97ed308fe893b84b68b951984","signature":"a36586c8dde641e45ee945ae65b13a4a1ab367c309a0e34d5d61b642e4b192a1"},{"version":"9e1b2a2af3ce2f7b7fd5add5d78fb666b55c6b2d5c5f7d981fa07af20ebc575e","affectsGlobalScope":true,"impliedFormat":99},{"version":"b51bca9749817b5ca6f945534921cff40d00fb1e44378011e8f0033ddb27a9cc","impliedFormat":99},{"version":"2e82d0ebe6ce039ec74ca4d1df4facdebda4efa23e60a6216b4176bc8f4a0aac","impliedFormat":99},{"version":"f92f4e4f821a787525611edfd46e7376c1bcb4565f70b20cc5f055b1164a8f17","affectsGlobalScope":true,"impliedFormat":99},{"version":"514ef5d527919c910089364684e1b562d5bddac33242680f7249b200c0e3bcf6","affectsGlobalScope":true,"impliedFormat":99},{"version":"65663900ecef4e8c5b485ba06ccf30feeb4cbfcd710ea0ca79a5cf22c974aa83","impliedFormat":99},{"version":"0295c7a5d5d956391ab9bf0410e73a89e25fe26810f9a1d823cc794d682cdafc","impliedFormat":1},{"version":"19826a846db870c2261a3c4cf0695df889d9fe3eebe7775f3f5bc76fe7ad07a7","impliedFormat":1},{"version":"e04cafd03370139cdb0c846273cb19eb4264be0073c7baf78e9b2c16ffb74813","impliedFormat":1},{"version":"7c01c77fb7d8664daa64819245d785e106e0a3cb6e43da64346e4400d7fa9401","impliedFormat":1},{"version":"8c2ca98f4713d989d610fbd38a44316bc43c50aa26983e62dc31002f32ce63fa","impliedFormat":1},{"version":"ee931610d1cf7a6e666fad138187751392fc88bee931b94ac8c4571208dc7370","impliedFormat":1},{"version":"53543b3b64e624a81fc5876da6d72c94dd87655e7afc10988cf82ce7cbc74180","impliedFormat":1},{"version":"967e68e99b8a80551837321442a0e2f12ef50aa1ce567ec991ac6bf062a0c7cf","impliedFormat":1},{"version":"144ab2f3ef7404caf39c6acc88d248d7e55ab3dd1c4c0d89367ad12169aec113","impliedFormat":1},{"version":"759002d4454b851c51b3585e0837c77d159c59957fc519c876449ee5d80a6643","impliedFormat":1},{"version":"07c50b6db67b8b943aed3e410bfeebfb6d3ba1fd1e2819bc889e48f81e94ed2d","impliedFormat":1},{"version":"e3a5287471fb08f053c06fd998632792aa5f022e45278f1e6dd55fb2fa9e7362","impliedFormat":1},{"version":"28a6c8eeb48e165920067b9193555649fc43c2a28c450f23f622e5eb043d9463","impliedFormat":1},{"version":"1147c3efa5a256bcd6a3d2cfaf764185b7120bf985f8412d9bae596a0348f77b","impliedFormat":1},{"version":"490cb6f5e6c90c4eeab1d237242901fc418bd1f08345523beaccc7357122166e","impliedFormat":99},{"version":"cd4908c240506b0aa278c4c8d6bc1705acb41b03fc5b1d86a47f7a9f76f0b075","impliedFormat":99},{"version":"e663cd7abf76422500d57d6500a546fb51e79a095c23914efc33ef73a32f4f8f","impliedFormat":1},{"version":"1ac8996804c14b6ad107fa17f47cfba0f9a4e6f5bfc3ca8b6e6f6a4cfa636398","impliedFormat":1},{"version":"7a317ff3b65c289d4344ea7b6e0a9793168703a79bf1ee889b3ce379b58a54a3","impliedFormat":1},{"version":"32f007650e1b504bb21fa424a3f86b956599d6617e975f0b81be6a2b9b9852ee","affectsGlobalScope":true,"impliedFormat":99},{"version":"d6d1d623816f9e679cf3ff0205e0e37009a6dabe3e6038224379ed0e937f2e20","impliedFormat":99},{"version":"00ae115ba20d5e4665270d1fb9f60cd1909f902bcae5463c6702f3906eecd44d","affectsGlobalScope":true,"impliedFormat":99},{"version":"04baec2b0229844d351e2187b3da5df20d9b862827cb6bf501819c98ae22811c","impliedFormat":99},{"version":"be0244e519bef160f4b48d37b9e5dc5fda6625f16b3902d7de821fbb2ea39857","affectsGlobalScope":true,"impliedFormat":99},{"version":"29db2c1e2e934e0c8cdbaf8f7f5601e1b4dfdac35f0c839ebf585ab242f1476e","affectsGlobalScope":true,"impliedFormat":99},{"version":"8663fa4279da9eaddb386c1bc28de7f5b433dd2518763ebf185ed198b4b60aec","affectsGlobalScope":true,"impliedFormat":99},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"58eb22ce690f7941a952f307d51a210195798c9fbc381470fe7e0adb228d4c92","impliedFormat":1},{"version":"49a70606a1a2e65aa6daa491d863c7ff09336c69eccbfd88800bcfb487933fb8","impliedFormat":99},{"version":"4b11433ee25f0442e19488e266448ed50106210ec1bf70279fc60b270dc56c1b","impliedFormat":99},{"version":"e134052a6b1ded61693b4037f615dc72f14e2881e79c1ddbff6c514c8a516b05","impliedFormat":1},{"version":"af5fd0c8cde550fc2c081a18517fb30954461a16daf2ed7969db3805e5a8d7d9","impliedFormat":1},{"version":"d4da3464d2c4b7d80d203ae64bb5c75a66221db5eee12a4ef2c4ce2c9d19a821","impliedFormat":1},{"version":"db820c42ed9b21f2d81ba48889aeb1317c710959346b042f07cee69a545f1326","impliedFormat":1},{"version":"695f2b1040663810ccac946cc87dea5236e3a74efb4bf5964def8961309634b1","impliedFormat":99},{"version":"8cf30ed8685586888fe2a1f781c7bf433c07422648a24c05aa4761253f8e8fec","impliedFormat":99},{"version":"ac21bc74759274985ce9b5d4e4c46dfbdd345cb6439c856941193be9e9519cdf","impliedFormat":1},{"version":"7459d85c80f2971be954b562724106b13d5a2a57e8bfde51723e94e838f6fcbf","impliedFormat":99},{"version":"c24ab9ac84d65b417a807ada25456697bb2adf1189fa80cb240625dfb3e61c42","impliedFormat":99},{"version":"03952163760a479246853f7ee56fd231d703e8d6f78deb353c7892efeef3db3e","impliedFormat":99},{"version":"0d121799b2cbac398e84a4708fcd82f980f33bf3ac7721a3cf9db9f28d9eb2b2","impliedFormat":99},{"version":"b45c2c7da9952665f78397fccb03f67535535939497b11308c9409c8505eb32a","impliedFormat":99},{"version":"9c3663fa36fb976609d8fc43372ae38dc2e066dab014a955f40ae6430733fb71","impliedFormat":99},{"version":"e38a172f8912eebc79671e07b81687a304a9d366a47933fde9f97ad79f8ac08a","impliedFormat":99},{"version":"2fbe402f0ee5aa8ab55367f88030f79d46211c0a0f342becaa9f648bf8534e9d","impliedFormat":1},{"version":"b94258ef37e67474ac5522e9c519489a55dcb3d4a8f645e335fc68ea2215fe88","impliedFormat":1},{"version":"219758c1721d5c22bb0d09343123f45834f0abc46ae721c951477d9727d78957","signature":"a13e991d818ac84d807cb05e6c0a62c8a9528cf740d4fc75e1932273282e3f8b"},{"version":"689244c57f5bdc0c89f511d55220c8c4d0f54107084628e65fcd2330c4faaee0","signature":"53e6a8c9b6e07251a846bd666ac8edd440de1348020dad87cbfd1c5c97e42d90"},{"version":"c840e12da509f2c771dbc356a23750227a43d1864640c7e6404b8f9ec86b3871","impliedFormat":99},{"version":"6ae92eaaaef30fae975de604d3af31d5b00eca7f02d89fab589152df926685fd","impliedFormat":99},{"version":"26cfaec143443411bc7d5363f274f885ced430b8f4bee25a81f7827248848d7b","impliedFormat":99},{"version":"e3b3780b68c70ff42894b68ee8f49186ef3335fc86a9106dd07e11580b460cc1","impliedFormat":99},{"version":"459fda119c27213d3defde220c051414020029abd52fb6fb9aea0beedc512c6b","impliedFormat":99},{"version":"76c44fd2592ce9da919eec96ae48927ed2d8c6ef85875bff636cbe83ebf59f02","signature":"58a5ec371db12fd72d7b69a8c237fc87c5a131763b45d262a3d191c5d1356d6d"},{"version":"2b4276dde46aa2faf0dd86119999c76b81e6488cd6b0d0fcf9fb985769cd11c0","impliedFormat":99},{"version":"38d4cff03e87dc58bfd50ffe5a3fb25e6e6d4136a1282883285baf71d35967c5","impliedFormat":99},{"version":"5ecea63968444d55f7c3cf677cbec9525db9229953b34f06be0386a24b0fffd2","impliedFormat":99},{"version":"6ea9c8bf2ae4d47a0dbc2a1f9ac1e36c639b2ac9225c4d271c2f63a2faf24831","impliedFormat":99},{"version":"a3d603c46b55d51493799241b8a456169d36301cc926ff72c75f5480e7eb25bf","impliedFormat":99},{"version":"ad98c359284db8c984e88949b2c3394e4a35158880767b772491489788a6c5a0","impliedFormat":99},{"version":"5c117cca0b75ed634fe3085142a931df2e2214e26f2bbcb34c592b767f13c1e8","impliedFormat":99},{"version":"773c18e2bcc18598df8f8b2be930eb26b22608edf368e42e9ca3484828ec4122","impliedFormat":99},{"version":"37f96daaddc2dd96712b2e86f3901f477ac01a5c2539b1bc07fd609d62039ee1","impliedFormat":1},{"version":"4faca872dbd194a17b3ee267bd8ddc3daf3d16df96f4e43a02c7d9a862022c4f","impliedFormat":99},{"version":"f89aeba83b1744a0d697c1b7fb8a06d8dc4cf7c0d469d2419772121f499efa8e","impliedFormat":99},{"version":"144a4e5780b800c0553949169f50be285eccbdb0298afd83ef2ae03fef77e2d2","impliedFormat":99},{"version":"66aeb47bf8638d6767f7b4ff684c2d794391c981590073025e98f98e1afed499","impliedFormat":99},{"version":"26748898fec8579096c776866e8e6f07754845b3d08f5ae98c3a59baa9e85c2e","impliedFormat":99},{"version":"6d805abd62920edbd9ed4b20be26d040d01529f3ce53fdab9ca4d0fa9b589f02","impliedFormat":99},{"version":"edbeff52e73b0ff82898142aaf8ffb336910a8ccdfc31b79960ea4ad4c9f407b","impliedFormat":99},{"version":"b64a8c7a27133db3f181199d21c0c582e86f38ba57a031a126b77229526b4916","impliedFormat":99},{"version":"4d857105510df8011cfb5b3769dec55624a1df92e85d399cd03bc82bb89d090c","impliedFormat":99},{"version":"8eca47167dadd486582ecd4e41f7fba6ae66cc4a4c5202f1f7acf34129a0dadf","impliedFormat":99},{"version":"755fc4f6a7aca9da7b38e1f7a39abd36538a72711763c741f1bbbf36f999b654","impliedFormat":99},{"version":"c4e79e849fd94553e07d7386552c472ea4f4fbfc9d280c012018fe7a6c50e598","impliedFormat":99},{"version":"062c0cf9641ca90ff3ad8edc61c2e06299fe6585fb9a4014a8acdf7f11810d51","impliedFormat":99},{"version":"f68896096cba0f06ffbf39a67c2280f6f2e5b90c75db56a3f9ae5f7f3bb54460","impliedFormat":99},{"version":"97b02501eb45f487174d5a0ff89b6a95690d50e9eae242e2162118edd5f2705c","impliedFormat":99},{"version":"425a03d68f43164e0214b1c333cd58e777d4186f412b530467c18ef0d2b37a80","impliedFormat":99},{"version":"f9a591e5fe0be6728cc84e70325aacafffcf203b051ddef37d65651b43c05056","impliedFormat":99},{"version":"293c0a3e323608f4e20667acc74d7bdba727db954b840ee1db03f2fd5807c761","impliedFormat":99},{"version":"de8b4c367880fe92a0a740b706f08a46d1cf9e3981d55c2701e82423e81ef0ef","impliedFormat":99},{"version":"4a056a71ffda9ff3f2adec60c0189c906f7e46976a0c6650fa196674ff8c4dff","impliedFormat":99},{"version":"3a3fd6f5ca85ceeb293f2a010125f9455404958122b6dd0ba0b34f7dab74feb5","impliedFormat":99},{"version":"42b58bc8da11e9181ecf4ac498d41c74930c73c8ebef091474d0f8cf971b50ac","impliedFormat":99},{"version":"9e003336714371c98108af3dc53342829039327f8b1db97d826ca81805746b7f","impliedFormat":99},{"version":"d8954254110123f6f5c9fda6280674f6b853d47df7fc41c2090e741dc8b6b627","impliedFormat":99},{"version":"cc141bc69488825723b0d638491da8779c4c3e515712d5e1e63709e0136dfb2b","impliedFormat":99},{"version":"df600fddb7f36e4c4b3cc4e36073d0099a5b6352b88be02decf63869c8886313","impliedFormat":99},{"version":"9fe94c8f6b36cb41acd30d89567761a52246932dece21e1ce104baa2e84b07ac","impliedFormat":99},{"version":"5560b9471d610c2543dfaa66a88f7b7cf05cdac61a698d4106b3845b7b7b3b29","impliedFormat":99},{"version":"e80a0ca19ba1ea6d815b5017ce248fc817c076522d1b0f42f062a784897a30a7","impliedFormat":99},{"version":"e96bd939a55117abe6ccbc02839f2f4d9ce3893368fea528ca91c59ebddf496f","impliedFormat":99},{"version":"81f6bf27eedb1ed92466abfcee33795a6b2304691ae01f42e60f8c76894fade7","impliedFormat":99},{"version":"8ae2e6733fc535c43c514f871d28cb0fe5926238dbc3d64c978a1f6f00d73296","impliedFormat":99},{"version":"26a0c2d883e1ed55ba00810d957dedcde5d16d637e33063686e2bc3f58a5c64a","impliedFormat":99},{"version":"68099697ac4e919f6f1832389f32eb67d3d94fae744967f33e0cbc049a222a3a","impliedFormat":99},{"version":"bfb900f7de2066a4be644c269285fda8ccca40b065476a27b082173014d00467","impliedFormat":99},{"version":"fea750ade335a63f707e7fd7410cc11b81805f6b454880e1ed0e178c08fae8ee","impliedFormat":99},{"version":"7e2b8299e85423435784cc6244e2d559ea862d226e7b0ec871c6a53f88e5139f","impliedFormat":99},{"version":"51b6335f5a8e177306647558a3eefa1f6abe259b283c6462223be3d7d0f33300","impliedFormat":99},{"version":"31a63b6b49338cc6ddb1a318beda72d3d8fd523e50f8dd5d5ff3accd39a490d7","impliedFormat":99},{"version":"8fc7615fb8fa095406f5a58b1b817b9a685c516058877a3d2c06d4545fdf5dbe","impliedFormat":99},{"version":"f70e9fcb8172de822a729503eefece37f169e063cd7f97a978e3913dc872a82f","impliedFormat":99},{"version":"d5da26af31358a4883edb6112879018b14c7c1fbcc457aa36961b03ee17bedea","impliedFormat":99},{"version":"4e93fb2d2c59bbc1f1a5211b36c447efe4d0af568d682ef1e5eb5f84ca6ccc2e","impliedFormat":99},{"version":"b6cc07bc56c7418ab500421d2629c26acdb6ec2e9af85c5e2c5d7e8c9f92566d","impliedFormat":99},{"version":"3d13fe973e92e708ad3dbbf1b2385bb799f8e70c8da71a1ac72fcb5521c8a5e9","impliedFormat":99},{"version":"16f3f66b5182e57c554d0e374e29fdc0a899c1321b3f94fa997d19abf9faf931","impliedFormat":99},{"version":"df459c67b9eef71318851331b23926961f0476b4b9d1674addcb627eabe583b2","impliedFormat":99},{"version":"b93915733f1802a8d9093116a5e660385afda0f72a5bab9c9bd356549a61f1cd","impliedFormat":99},{"version":"1dfb1491b28cf3734f3c07745a56d7758a12d546f81a70fbfe8ee9faeb528c54","impliedFormat":99},{"version":"42b00a511331c7f79039ae3be83fd5941b212ce193654c3ac5d82157227b69ad","impliedFormat":99},{"version":"1a5914065c7c6e2b6e98437c4ba6059bfeda3441f0f55cb07189df1e78698922","impliedFormat":99},{"version":"66d3f42196f32c639d8240bbd520852abc024d673c713bc5ab26b4bdde750a74","impliedFormat":99},{"version":"18ce9175445b3edec11bbca1c682a8f3919e74fb3504abd883ac462ea8089a0d","impliedFormat":99},{"version":"aec96f94a27fc61d5f4ead202628e811199f887d48bb7614b7e1706b629cd1ba","impliedFormat":99},{"version":"2ab01a0368f65b3b891e25416ae785dca54808f70d8f6204b99589ed4f7f1d2f","impliedFormat":99},{"version":"efd87ca2190e2d0f905e30343443c8d2f0d32cb2b1c9e05b2cb97b0061abec08","impliedFormat":99},{"version":"3323edd649d2c85d3528912cdf6fb770258d59d75241575fcf25c7850837de79","impliedFormat":99},{"version":"e0c8deca5262879f11ee84fe6664dcd197a60f546930363bf2d78d37c3138c69","impliedFormat":99},{"version":"3b8ba0456ce7763676505d103eb336a6b97659d6fc8a7d7d186558ea0b8c7ea5","impliedFormat":99},{"version":"7bae4a3f50a844fcbdc504d717f5f13dd7178ca99f131b230305db6b55e1dbc3","impliedFormat":99},{"version":"f3a4668046e42e9f5ebfdc8b79238eb27c399443dd1dcfcd4c4c3652692ed031","impliedFormat":99},{"version":"07160ad59d49c6f2999f9703542918df3eb1ef93857bc560b4baa7c6775d22ce","impliedFormat":99},{"version":"642d33e1e66ab109cdcc5a5b5535535768c8b642fd5ef75de4d77de3a5632601","impliedFormat":99},{"version":"c23b9684ab48ea6956f0a510c3cf724277fa457efa7ff85d5907f4e4290ba0aa","impliedFormat":99},{"version":"3859635347ecc10d4ff5dd68bde66420f5a8539b7313dd62020aa025208d2222","impliedFormat":99},{"version":"f49e8feb6d7473579a5ceb5872598bbc1be3723da653993472720629c72fa0ba","impliedFormat":99},{"version":"14c727434cfe6a078b51a88d005033ad01a76bec36292d7b47369d82e48e1cb0","impliedFormat":99},{"version":"a986f6af9c8f27ba98239933aaf4e47368f3d89c4cf3fb37e80b2556d73bf253","signature":"b9acca28c776aec8d087257885a24491333a1727c1c7068f5de67816e6b85de2"},{"version":"110f4d9864e203460330d19430d757517b191830fb36819624529877e9f75c41","signature":"969ea527258e04281d145910d569e1a74c7156bb8bff03cec975e60b839ed69d"},{"version":"c3d577953f04c0188d8b9c63b2748b814efda6440336fa49557f0079f5cf748a","impliedFormat":1},{"version":"787fe950e18951b7970ec98cb05b3d0b11fcdfeb2091a7ea481ac9e52bf6c086","impliedFormat":1},{"version":"13ceda04874f09091da1994ba5f58bf1e9439af93336616257691863560b3f13","impliedFormat":1},{"version":"47039aa29dad4b33704e9ab56ca4ea4c918aaf86afdcd4e059f9dc6045850b6d","impliedFormat":99},{"version":"433839016857a5a785134d2d5e760fdcc9819d241a3ffb5cb76985e666a34a8c","impliedFormat":99},{"version":"ef23a7b386401847e3b4c3ed1b087c32d56e7307b2782aa2f0d0ff4f02d70473","impliedFormat":99},{"version":"2dfa6d1aea770ef46bd51a79cfb7aa10b9ae90c9971eb53b4f5def028425c800","impliedFormat":99},{"version":"66cd137411911fea6db4b352a98279470e386eb3b1cac5db3de1efad678a9015","impliedFormat":99},{"version":"f8d95db2d66d268765d447b554dffdb3f1cd2a22e8da7f6f57dfdcad6a19a1e8","impliedFormat":99},{"version":"32d14a5cf8b4430d2c5df3cd85a3e48ed3d780e24eb1dc08628cb8ddf2393b9e","signature":"9a38ce0e9264512c2af793453b7a0503e44a60a25100f53e3fb8d7b0c1c95d53"},{"version":"1dc2686db29ef5b3a12b29d6d402e8769314aff02b29d8cfb32676d189973e42","signature":"cdd90dec979b4367fcb144d5b08f8311cbf35a855f5aa80182bc418dbb3cce10"},{"version":"01eaf9f05e7800d7b5a21489b8dbe095467a5642abc55ae7b5acef6bba76c439","signature":"a4e79780a39c0fc2e61bc66f20a63f11515ae8cda63852bf56f75664aaed0438"},{"version":"2532372207d5d5484e67bec571c9d80da4c01a0ad0033b900f0881ff224651e8","signature":"a7b2f4e64735319a876c55f776eeccdab6cc50e664b55b1b62a27aa7fd09e743"},{"version":"532069a8eae99274aabc8f7eddb0562f92390ff4f63ee88165b81e7f41ce6b96","signature":"fb69d502157f1cf71cb8c737f6909c2e82f2a53b8157f840411444435f5da3d1"},{"version":"15abcaa279117eb516a90c09c4b60c53fb29c1242c7f67bb1003e0cce06f7d19","impliedFormat":99},{"version":"97a13c9556181373f99ab03f369ba031615835ba233563c6ce2dc3a6880a47da","signature":"bb4b02b4dd58d4e434c952da55797cf052d30d36b09bd14aadfbc70a037c84ce"},{"version":"8c8f462d06caec0a7e03f6e2d32880365a1b080b0949f17143817580c289544f","signature":"5056e9433f06040810240d3c09911d34ecb07fdc5a27820453cc1ea4708775f8"},{"version":"cc3a63721349e812f6ed685eebfb017325f36b0b40023f5bc9e68ea769d1851e","signature":"9bc26c030aef4b75d3bf757291b79add9c525e1ed6a83579c53c5077feaab77d"},{"version":"410b82507292ff0bb031a6beadaa7fddfa9646f3a0c16bcc4acbd3ef989c19fc","signature":"2e8ace1e09235d2b98e61fab48614e4e94c36f427c3c815b436570eed7068a41"},{"version":"3bed67fcc8c4bcba76bddc0693f9c428ce26d1d32571a1f21324842876c9d367","signature":"45d0a7610c0ff4db43d228e6c1f823fb3eb19f262ba58b54eb1ea0a39c2ef7ce"},{"version":"3db2d7065844cf28db1eff8a77477e58ee31f9e3ae4df9dfa042c2c964f83a3a","signature":"2dc7085a3856beb3291a4eb024a77421349c5b201844fa96a88b879ea9ade85d"},{"version":"c922c00c12a7f430fb02d99ef71ff861e8c2c29860df31b2dc3c6f922e7ec101","signature":"ebe17c0f79d00d4336e2868b21aec2251ee4d5a74c81d1380236997f5226ec89"},{"version":"e48ef81520ba3b6403efe621d96c7145bab0e6e498e7b2ca0de415dc13b6848d","signature":"3e394036285671868dc3fc182e60f36301bb4f0e58e32d802510009a51e3f7e9"},{"version":"7f19b8476658d25ff197c84030e58cd7395059d876a54630e025951e474ebdae","signature":"cbb6e618cfae37c1feb246b78260428cf2bcba79a0c9bf1ac60d511a4158692d"},{"version":"faf9a217d8d237b02ab6d95508d8736ae431bbeb38d98885eb5b8fb6dbe48cec","impliedFormat":99},{"version":"da08c6b6a9bce701e6d2775ac410ddb2a7189e19b020abea06c1932226248ae9","impliedFormat":99},{"version":"b2e5691a1a795e58a63d10b907248299ca96bb3433b7fb252279b4f00cc3a5d0","impliedFormat":99},{"version":"4244c6a12dc14768ddc3ee2737ccf1c6c0d07353785d5979df7d250eda461a61","impliedFormat":99},{"version":"cce820aba9ba9d1984461c67d0d543d8eba7ea25c6a1be7a47c31cc18907a631","impliedFormat":99},{"version":"3e7e929f14bc384ed6da8ef0c7dc3aae5ea191494c9554e4327186ef40db2573","impliedFormat":99},{"version":"2abf45177ecaca091e103f4d9557a7e8524d160b28a8dd457dcb7defca65245c","impliedFormat":99},{"version":"bb2b81bc35df3045d588a60ac6649c7b45f539c855660642ff1c12e9b83fbf1e","impliedFormat":99},{"version":"acd4a1daff6df3526b17094f8f30ef3c9c19c9bd5bb95c3fa46a1b8037290778","impliedFormat":99},{"version":"b719a8ea8321a8d636c2765f8b5f8efd20a43f8f7922dac4f2f7ce7e361ce233","impliedFormat":99},{"version":"2673620ee9cb5e62777ceeea4e84f0c53a2bea44d74f103ece49bfb7efe95aec","impliedFormat":99},{"version":"77d542e6871aea7774c7482cdedd9ca20cb03145ab9112e6845a1cc77b7b6c00","impliedFormat":99},{"version":"ab2a3d386e021bf5619d581692f14aac01bef911457d9960ef6c21d9b97c1c06","impliedFormat":99},{"version":"0f2d99aaa9d3574828b6f8c1912208e370a041d640497679ccf4a4f5e073efb3","impliedFormat":99},{"version":"466e0183c786efd5a3e1b69d6e0e445c77d771598871c9929137184206362b80","impliedFormat":99},{"version":"49149ce82e10dfa69eb4fb250755ffe74107279286aaf8305ffeb201a0a96658","impliedFormat":99},{"version":"ad06dc58b3a23a89eecc369690d5cf5ec2d141009fff32cdd68b94ad8de375c3","impliedFormat":99},{"version":"f2b23f711dc15c9ba970626eb5e654d4e7401177b13d16085751f40dd1d77d17","impliedFormat":99},{"version":"b958c46e432e3a260af58b20ed0fdc4f3ca0c02bbdcbf12f63113c1a1932b57c","impliedFormat":99},{"version":"7e560e86bc8f0793454de297413f0ee411418d71b0f002563ddffb26b2f8a438","impliedFormat":99},{"version":"808a1b31fb436c3f1c3bc9307623234442cdb742fac3cd39bd0e8cf4797f86a4","impliedFormat":99},{"version":"ec8657eae496906e9c2bf18ddf1f276d6472cd7b42975c609fe49db7d10c4716","impliedFormat":99},{"version":"8834848635faaf4a4c1b77936a3790515df4989872d12e1f586a903ba8a96d32","impliedFormat":99},{"version":"3c9e08a6171b77f9409f6fbeee92cc7cec90295165305f4749c15b66984094b7","impliedFormat":99},{"version":"d5f52fef25eebd772b28fe3acd0ef103cb3d338c73fd23dae81235aa3d3216f3","impliedFormat":99},{"version":"0c622f51867f7ec248ee30282d7358ffd4365023107747a8837819d38ae07713","signature":"73cc5ae3d7988a184c8adcd808b83f66c1d53fbabb555e00f81a610607fbc767"},{"version":"d9bd97b61a4c5a17c054f0b35a3d60ab0ea7b7cbeb3375ffc6f04b34a84cc137","signature":"f538f6763d0744b10bc6d491d559ea72dd3ce9fa645858ef79d8bcbac6877bf7"},{"version":"a0d8f36c675b3001a7722e9c26d87775473dfafb3d81a185f9c03920fa5834ba","signature":"a29e04cba58c684bebd17bd32f46a53acb5f8b1292a42d53b1a18374b80c0fe4"},{"version":"efce9dbb7e2a327391cdab96ba61fd870a6e9ab0ff198a35a0a794227a0b5573","signature":"c4a7c080c4efc31d1142796434513a88f078771f34b505eca798ce5a9bdfec31"},{"version":"ff9851aceb984155932fbf414e751263fd0b58e760f95640a1e6fd49572f1aa5","signature":"34aafa201e480563bf9f474edcd99b6c10c798eb18891de1f65e60f032fdb482"},{"version":"8221a836fcbef6908900340d3be08fbd839a7eb6046a4711914497616a5e4b1a","signature":"1c211f72b18ad57348fe0b7544548f7b330b4e306f7ff4974a0db403df514a51"},{"version":"63868919b8c92bddfa4ea91139ad9d62761c1a2cf3e4553410be3053e3c6c281","signature":"29b3c41bea83cfc6ee031ed69ce943d1a494fd352b60d673f0142d0e42c8b1d2"},{"version":"641447db29bb1eb3a9adafb6a63da7ba11845a0931de5d938804f38dd442d0b7","signature":"729a8e79214634324fe502fe0d85c64ee79bc11cdff7422acd64e22b4ec8bfbc"},{"version":"35d2091cafbe10f5796ea9c9bd08920e6a0b79a3877af0b2294d7bb46e35e87b","signature":"1cc4f87b153eb468fa435aa970bb93a46db33760cb339b641220b564f615636b"},{"version":"3eca09f3c02f96d38d46e308c5b3222716c84c379def6dcfbbb9d649450c6561","signature":"f60810de973f028b077417c92c4b2ea04ea8b87f33b85bfa5d3674749da37b7e"},{"version":"c035ef058f2451c04bd992c6281f8c435170298f50fa4712912053145e37a0dc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7bc503f605613278cfded74b9afdf98c3d708b835ef8856d44f0dfa2597306f4","signature":"453c00e11f1ffe23f85adca0e32b862b41c8f4dcc2dc969bfc9d666b303d7005"},{"version":"46488fed628b968b68977175df4d41ff7c305d305728a8556b0de611a79013dc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ec3bb34b23743c95ff292c3ddcf5ddfb4441630f3c71848fc5bbd18f3b5b3ed4","signature":"e41cb0738e24140c475bbd859b8ea3e99fb2a4dc5ff2471987e81f9a15cc56bb"},{"version":"26f0cfcff1aa76b2735a34b64fbfbefafeb9d807026576025521ba6abf6f422f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b57861e1b6a158a3e81044be6b1d5c9e94d968ba63c3e3f3de0b9e37356ced56","signature":"a458882652ffcd89d8e16df42a8723b41ff0875fd6dd931d012fd5fc8972cfcb"},{"version":"b5dd7ca3d7fc3468d822dd471aeef7f87ca35d1581866cf41ad9496eb6eb07c3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e930d0e5f7fe3aa18efe59bcc0d93deeb3b992cda037075f4b733b16bbdcb648","signature":"4476440845d41c5d6e22a9998d3c66cf2c750fea47d0a811c1ac0dacb15c1a9e"},{"version":"4c5aaf60156e5ce2f38171f24e1ff9079c66d3471e3c2a8e4cfd4e8ba04940ae","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b3162954559ebcae366f6a6a8a3d8e1daa86b03253a9f8b4667856d576632a8d","signature":"543ed74211b09aedfed2327ab5190ae5aae8020489cf9bcbcd7b6f5fed43b9ca"},{"version":"9e205dbaf6d76983bbd008f5858f4503edeb4e63d22144f395989b04aec80b49","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f5a301a7f2a1e24bfa948d720ad4a3e443966e1bca19c1ef21e870218ff00f1","signature":"1a745f1ceec14fca346b02f22a21bc9430d1114c92aa54de6a01026f22b40f91"},{"version":"a560680227e92043871bec12eb705924803cb7f51d3bd7a249746a5c66a777e3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c5d49f9abfb3eb1234bde5a49f8ccf74cd03992aba80e23a7f6127d932575173","signature":"f4bd3dc3555d2785ed107499684380e787ef42e1d786213f6105ad72bf5dc7d4"},{"version":"c536980a4710245b5d3aec5bd1a30297e14f2702be362fd88392a17beab701b2","signature":"a8ae6f331461073a442c9afaa1ca7cfeca78af9c137afa6e9233fbde0895959d"},{"version":"74310c6edad1f850774ca58fe5ed0fdfe5db3c9f142ef3a089666329d5dc2ceb","signature":"fc2d6dd4b30084cf2e4b653436bd05ede2b6d7513592c8c63d3b151d3ed30312"},{"version":"9e1755385eb94c17983b911ae3d6d4c6e012c725f4f211973d96917f49c8523f","signature":"ebe1835cb536933469a079046b10933b8c150543290b84088db4bc39d574c01a"},{"version":"9add9c771bc92c7e00c8bf360b43481847fccbd41a44825d5593dc9545f39661","signature":"5b348bc254dbf64472584721811d1c4365a8b9655c3b7fd36c313b0bcceed20b"},{"version":"4f77c0bcd3ec67f16e10eff3576c322b47d299948aa40885f9dfce3aa0b67fcd","signature":"244ebaee450265298f56f1b7cd1172b2daad3c8e1581a3c34bc7df376f9994da"},{"version":"ae859e9c1338b0569335d7532a3f4755d9805084b7e832afeb085b5451c9a918","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"67a4656148e1c74d57a43bb300ba8eeaf7435d364a0cb97abf326900d2bfce62","signature":"4db5a07ddcc6b732c3100ff4b1d9952b26f9f20ffa545bc4bfcc587e9264f614"},{"version":"3e49c4acbde5add78f1859975ed20c90c4e2c12678cb1d8e5a86a2c199f7176a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"aa8a81b075a4641ed6545c89e2faec209cd0cfe8c226ba06140897f751d59c02","signature":"97e3b787148ed980ef6fde084417456b56ae13963a7026a4784eddfbab619759"},{"version":"c0a9c345535170bf25c312955c7da4611e4e15cf2d5d09ee79473987c1839b67","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bb72eee654836edd2cdd7cfea23be9215922a20541b0830bdad6db53276935ec","signature":"e9c030958d79b92ea2f53fe5166d6152502804435ba1ab04301944e43d5e4f4f"},{"version":"861a4ba07da50ae79873f895baae98f82143dfb56cee577c271d0b7b22a877d3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9cd9eab693864566ac38836fdee14a3710fff747fd7f1a480caf810c1a8e0483","signature":"94f2d0b85003e9a9e6aaeaa55259a1772bb0407ac23842d1a4ba89380230a266"},{"version":"451305998accece385013ff11837798fde76a871cea8e3795a366d8fe04e5a13","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9d28b54569f4cd1c0c21d95d0e70f6fdbfd792121604b95d7c2d067ee0f0fd11","impliedFormat":99},{"version":"b92dbe495c1dee9f3cd7c1466fdf02476f19ae18a7ecc829e3ed8a380b2d597f","impliedFormat":99},{"version":"bd3e3a447a34cf3dd875ed550ea349589b2dfcd6b3a14abebdc946366bd812a4","impliedFormat":99},{"version":"6b100282ebfd5bfa4d8c72ad5dd3e9d325aa32d0a53623d6171fc2cc2899fbc4","impliedFormat":99},{"version":"29297b3a1e4b3503eef0ec9f601f2a8da149c8074a0813624be8ad4889038ba3","impliedFormat":99},{"version":"5cf87865e3a8b12bb8ddfb311846ff2eae24983faa365f53d9224fb182cd0269","impliedFormat":99},{"version":"98ef9c3f5f15c18abcd6fa9f12e93e1bbd608225501151603cce97642dbc961d","impliedFormat":99},{"version":"c80a56a10f1a8e01c4b7f08df6e9260ae076c910402dae8173fa6c7dcacc51fe","impliedFormat":99},{"version":"becca0fa1bbcc55ad69918f65f771b76904ac98b2563cb34f26fbe240c3d4db3","impliedFormat":99},{"version":"32b344c3765dc7c383516f3326c108ca84c33df44ece8ac789fce47d47ba8810","impliedFormat":99},{"version":"7e2442c976852a201ca2f298e1c57bada7476b501c865b1070e22d969d712740","signature":"f774a7e8875649db68f5952801f001f3c316560ede472e5663af7a8c9c18f938"},{"version":"4a021f26fd7efcc21acef07c38a3d57ba0ee1a966cc032004afa66c4f95f08c4","signature":"aa333ffa319c18796c9104d75082dfd218334109f702eb6e9ad369beac518833"},{"version":"f2b3e4319e013e593b8de35be84fd0b88fa97b67f870cc8af6fc801be093d95f","signature":"43958dbc81fcbf127e4104af0acde2116a5b0c0309ff3c811ecda47d849b324f"},{"version":"8673f100e1085ad46e0d8bcc16a112035488ff7baa89e9d3dd80d2ca1d9e6e22","signature":"ea7717cc4a65e1030dbead855313a4928f5b4804bad8165ef65ed75aca4a9de1"},{"version":"c0facee0934c3c35c24074d238318c624368a5f0650f7c5d7c1748ef906d7b7b","signature":"ddec8a41401ba491e6e3260495d11cf559f75e0d71c458891dd3d04302e91506"},{"version":"fc88b086529a46b3367bcfe3b0e6ed59421ea8eb5543921f185fd635243d0576","signature":"f7b8e8d653f68258e733978cb10977ef6502ff093127fff7c67c603258fbc0c1"},{"version":"3f567dcb87f20c3667daa8cd5e5e8f7f4062666562e3109af5cc2b0ff2c09378","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6f620a5da77a995a0557604c177f3126db4e04ced4b1ee133d1150c7e9af6687","signature":"65c0f4461b3b871b9fb8f09dfb6c6995231ce5c81756f7e2ad1443a8bb811853"},{"version":"9851df53571e11efea88950ff7cbc8f1c5d7dd14dcc3bf980b1f0829ea47bf05","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c145a1b94e8f126435db2db121bfda1831340de88737157b679a5e0a048b936a","signature":"9d3470ece4f34e063923ce5e95c6a2857dbe862ac4160986f6ca2ae7492a6584"},{"version":"83d57a594c11bb32c425b6a6d174703fc55681163bd57b494ebb03ef2f21c046","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"936a2ca9806a50b4e9be526c26e35798afa2a54e33e7ac66e63729f507f780e1","signature":"26f1753414b839143161e2cd5dc04694563b6060def71c7c557d41ad5ed670f5"},{"version":"ad10a345fbd8bf945c6722e75aedb0e84ce9e79a81a5e83a3fc36b1d01236607","signature":"68d3cb371946df9752181f498258b83df47a83c43b7c65fd8d9492ccb2cc38ad"},{"version":"b8b6b94c2e9eec6c2b56b0ef2fe797f151cdb7637e86baeaf6b4a850312fd9c2","signature":"2a894521fb9e328100a2fcb5ec9a06f498a1e3e7e85eb4552833cab91539b432"},{"version":"9172402390149d6c229a21dd3eb44adc22121588ee668f2bcfd760604de0d2db","signature":"89fd106e568e14763b9d88d102f99226f38f2f33a7f155fa5a5156b3f97a918b"},{"version":"ac71393bc1487fdb8618f761c86fb8de6768869ecf5a0af3df13b3fa5bc65d6f","signature":"35b620e13b6f758ca46caeb176e8a2058a354b71e6ac9725d85beb980e6429a2"},{"version":"35426102a525d13bf918c3913913d51879ec724f4e5cce01ab46803355e803bb","signature":"d3970af1c43979bc3c6f599f83b2d116723d4e75d157d840bbce825d4cc013c5"},{"version":"477e78351d1ddbbdacf8412cbc8e609d9cd36fe61690ae39e99be4c1e4b154b7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f3b79149a1e0f40f7a1b0cf5639c9bc8face06ac35e45a3a8fc9ac1c8649643c","signature":"e0cd82515bc8fcab3731ecf190854ca399c8fc231f83204b33430230974d12f9"},{"version":"594fbfb2b900134500e75b54d71facf66760bc4642a17066223a0399d0d13096","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f0fdc6461bff42d5541014d1c542a00cfe65384a57804db91b9b29e696671988","signature":"7cfabcb3c97a54b4e9ef65bf4c7497a349391494470ebe50e2b9640d09281937"},{"version":"c728712d0c33f525cda352b5aa9e8ffa7b240b041b984d75c8cc3ccce815e07a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"131befe057a5c020d346de1e9b9f75dabf9c8cf59f2266a9f0e27176c9c541a3","signature":"d6dd397c037c7ba718dac429f0779b6155c56c4a7a34c2b170599e670fc59d58"},{"version":"b16dd1286b3e030ede9d816904b15f4407a0fd925790379dbf0ba70a5887b182","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"41a3b967d70a34e703928622f0d3aca29b16d734336e19428f187d526f7365cb","signature":"e06b7e8020b282098867bf306f99543f3c5d681405c54d0ac70ea5361ff69ee3"},{"version":"0a9f5f674ed91be163df17e25e2dc2abd3d3d0c40d4b77029a11540cf2451c20","signature":"236d8105049232dd31d36b2329448b12893997f9d24dccd7b2b32b4906865dea"},{"version":"a42ddfb05fdba9d21b0ea09accbf2895722d81dfbd5ed38bb8506a946128a11c","signature":"2506217fe22f6a8e513b7b6967ba39d5ec2dcc3743711598517c53cc68d25fe8"},{"version":"77f995463d80efedc47f622c76a8bea0ede10272acc3c7af582dcd5207d30ad6","signature":"faad948aae2979c150d527b12809c76e0743cf37da0b322bbe9cdd3c653dcab6"},{"version":"a9067b4b6dba53667265a0453ce801d57f11f9be1cfd148ed1d1bc79e7a3cdd8","signature":"019fc063be62cb841a476836a1945b18ffcd3859ee8cb647f4f7595f1252bd56"},{"version":"e4661c0527cf99a803f7bbba2fb0016ddf5ccb73cf98496ed3a2c548b45fcf6c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a09c62dab83937f41b59f55f977cd324d8552d52ab718e8ccacb73b9ffcfe2f8","signature":"f57171caeab1d6dce3893d2ad203128a48bb522b73d4562df6b7d6d14073d300"},{"version":"1baddbe8a6db04ca7428d63545e4ce62ee4f02d6a3b6d53b67a9afee24b7e9a0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e3ddf1c4dd38cc2753022d4c792779e6679ba914eb80f1311d0096acef093084","signature":"d5ecb4d8ab5bd5fe8da42222cff9ca3e010ca2937a3e4f50393564a587b64863"},{"version":"e5362de739bcb309194fbf3040bca329820e0a22a64b05541f659da99ca41e97","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3e2a55dd3e9fd11aa6bc0efdeab42d2a2a4bebe0c9b0827e64700eb867e0e512","signature":"8b9ffa5dcf765665acab2a0091ac825c3d1ce9630a1a8c32201efcb6a957888b"},{"version":"610be97d1c2d516a9dc114dca6efa3c4fa2937ba7442cd75b4360ab4f0618d5b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9c144b3bddd8918c6c67a6332f3b37521091ec69238bb955d934c698284ba929","signature":"06cd193d4c4e6940be1724e7b05f69862ad5478bbec59330e1db1cb4118d6d78"},{"version":"6f4bfbb4cf1d1c613c69cce29d5d6ae6fb1769e9221f7fade9f04958473a5c61","signature":"ae3e12f3c039097ee481c8defa0ab07d60ddca0d31fdaf26256ee1245f192c70"},{"version":"160f09da555233cf373f0db23a6fa066dfa31caaf752c3c097f26bcd40a7d148","signature":"3523fc8a3c6937588fa23496d6b09929b2379370370fb97a64a3414699aba4a4"},{"version":"51f2fb96ba5f2b0bd8bd2b81ac11a9960e65eb69b09832668b3bc48b2459c8bb","signature":"79a68f57fe1f9c742bf6923d57f7e92c5b62603b12b899ae169533dd6082a796"},{"version":"169c1094152195286e228a52b63aa645d3fb1ed768050ae03132ba44482752fb","signature":"f8031c5d8ade1b3f8a7f1f69bde71c3b62611226c6e2a9e499b7f9f6ccca47c8"},{"version":"3b1ff19587d5fcacf45a3ceb96684cadab07bf4ec6921b300bbf842436fccdf8","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e972be31cf6fd527685144bede5500f756340311b279ec7e0a2fedd1c22e2332","signature":"bd777dde2efa6743bafee805dc193e2f9fc27bb906eb504bd4092358c087d9d8"},{"version":"d749f24c658563616b334c9c568c8bcc693aa694fe198fb795178030fe293a57","signature":"a11d9515f4c5d19671b6c37944bcb81a2dccc0a56706f8852d131b2abec95abf"},{"version":"b67c073532f2441b3c897c1cf1b66e745f8ba56401bb7425ad47532e0d13d014","signature":"32b449f30d9776d304580e6f60603441d0b83760886003d66fee8f76740dfd34"},{"version":"b85de74ecd6ec138f1c11d97f44dfe699cb5abbcd9cad5e5e1ebaf38c620bcc4","signature":"2d535a9817e3d46b755503968e6f8c3ea57d16d7ac23cd8a7a0552f52e13d4a1"},{"version":"e6d6b98def1884c4244983270a5a2ec0a12d9e0b5f6084c8fec813cf8a191fb6","signature":"361558614187493181e5d1edd5b20f45a02a2fb65a8da11cdcd34d31d015613d"},{"version":"ef98c8b35dfe4bc2035ebc94670489af69dfdf623d2b8f42904965b4f96e4e1d","signature":"48b3e9ea98f1738cc20345d7d720efbe21371ae9748b964437b4a6f3c16d273b"},{"version":"b65dd9a55b58b3d2a6896c8f774a211d9517c230289371bdc80eddca9f303594","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"969a1c858486ed669e7247f5d8a95fac07e4a0b9271833a9aa225230831bbe5c","signature":"654116ab23597a1ef8427f7aef38e90812d3c3d4ab00af62aecb0444d4c44a7c"},{"version":"1ac3e56e87a485272c20b38c8125c79e42a1e16ed05373784d6ec84302d8148d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"764477ddc5f1dcb0b5b5de50649f16dcf4d36842e02e8ade2ad9b25340b8450e","signature":"8bdb56dff5459055b5a21f9fe895e95219f3460f641181f4bc37b5d5b3cfaabf"},{"version":"26fae1ef5da0aef3777e8f5447ae0f766c5f4eeb6036caf42ce5e0780ad52328","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3f7802b51aba52d5c4e91d59c4892e44c338dc10cab2560b8143979d990d75d3","signature":"b7d80dabdb6e9195629a2265e9cdee7afb8c8f2406cba5f1ce75ff62ec116091"},{"version":"738e8b1de143e25f4958482f0b3df6d5f67a094d15c1b6c73b397b37f388e7d0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e0cbde227bad31a3979ce1452ddbaaabd18727c91ab6146918c79b003c30b831","signature":"ecb61779944dd6d04f594dd817e2f127ca0c200ec71ae38a0a1f5fdc178f6f14"},{"version":"573abd03ce0bc9cc3a30747081bb6b5ecaa16ba1b59a29aa922fc1d485ce3b6f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"74a2404e9e409051b600c8abd0d876686ad1731e392393503dab8ad59d050038","signature":"d9c851283d7cfab93e0a2e5a988c442c7072087ef70fa8961ef6fd24df49bf0a"},{"version":"bbaff6c7e4288b46ba6e26ebc491e9128409f19403eb77d947e466454d0b183f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"13471ae3aedc24284930a5bf581a0e615865098436be58eea44fe0329855abc2","signature":"7b9d793ad8206eb5be40eaed18277e6126f7ab546b27c3e51413556f9bcb3225"},{"version":"03f7e0ae462d4b87ebd0c5b8d6779d882844a29c255ad4cf5cb2e2a0225f62a3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"04e75818d473e1616641bd374d057a812dd180459efe7d883ae185204d0f1a49","signature":"3c9c67a52e0704dd321352b7e2fd8918795b4fc4e44b7df370eec0f7ab16129a"},{"version":"2ae79a27ce4b25d6fdadb97bec297bef61f1c322403beb4804937bc554ed5a12","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4846776e25faf9dbb036333c666d964402950e66ff3b833bd5271ba18c19e14e","signature":"19123916b8cd7ba8b4872eaae51396cf72bc03d7946367ccfc3042c46f960d27"},{"version":"9494ffe3aad78947e0bfee0b7a914a3fa86b334b6144dad176b32102671fdd37","signature":"c33f8676872363b71b9aa3e66904c567efc1e6b405727401fc79e64c7eda06d6"},{"version":"54b65c57e0f5e3075fbc16e09469a25fa8d672c97845ce7fe8557dcf26ab98b0","signature":"f3c49b0c61f163d65c6d4db11b650595c613e1fbf317d61c73ab1ac6dcd7fe27"},{"version":"482e2b694ed8337c104285b0ce6ab875d7d979a0402f77b2948aa51449b6fea6","signature":"7f6fe6c5958772c345b151123e80fba4599da1c468af7058379d9b0426c6eebc"},{"version":"03a5203bf7f9fe0910eb22ecfe3230ba64e857f1b894445e91b262ebacad97e1","signature":"42adaddfe174cb67f75b599b3d7523fa842a74240d9586fdf18a0d3e77067309"},{"version":"a0099846790996a5432d7cc8225e35606d5ec55531a0333eeed9b2ccae8b872a","signature":"301f1efd2fed246d9e9d867f0584ccfabdb6ea9c13dbc243ec75fe604fe4e433"},{"version":"a403a473febb7938def5984571f440859986ba3368cd57b170e2228cdadbf4bc","signature":"7a3d773a0215697ed69ff6e87b6e6174b03312bee975ab8c7270e3d67de2a3b9"},{"version":"a1b8e47d183dc1a02170eb7888eb33440b141907251848bae238b00e21d01f76","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f911612fe6fbb59498bd042d845e42091b622ff60d62f62899ae9c9061da04db","impliedFormat":99},{"version":"df8d828dcc7036d38beba1b517c8bb7cbe632e23cd13819aac0b7454714d7ea5","impliedFormat":99},{"version":"6b97572b898559bde5ead60c3df72bea74dee67a9497f40ca2063af7cdf84bcf","impliedFormat":99},{"version":"d4c1d20b42f4c4b6710aae2ea61f16b05688871fc88c50164be241001c2ed2fb","impliedFormat":99},{"version":"4e93f1e1aed54628991b8092a5eff614ebd874ab3ff1f18903fb1ddd7543a45b","impliedFormat":99},{"version":"3758c87b27039448554b38154244bcc524735bee89342626ff9eb05b8834d05d","impliedFormat":99},{"version":"17e94aa1431a293fbb231edc284dc251555908f46e40b7648574725bd7acfbc6","impliedFormat":99},{"version":"0e1cab758e8f8c92c6d1b8b2a51fe46c31db41c462aaa21bd05929e3facd8b1e","impliedFormat":99},{"version":"6d37cb963c5288b5225af7fa9d072c429920718fa87d8352f72b5b80978aaf0b","impliedFormat":99},{"version":"e429692aefa26a42888b1e63959688bc0c75ef34fd3eb7c246f32b8872aeeb31","impliedFormat":99},{"version":"619d1633f5f39ba40bbbdef8613baa01cf242a1f70f9c0477b28a3211f2370fe","signature":"498e25119d58bd28ece6da55dc0c395a6f7cc59897155f4da92bd6eab82f6b46"},{"version":"370b83d6d96110053e63ee78938fafe0a9c893b6ddc08e7477bf241b91c20c83","signature":"8fc7d39a45aa3c24992fe36bb7839ec341dbcf7bd4e2987b5a2783b0c3f397dd"},{"version":"8d695b3774b714f73b6cbb390389ad2f3193e4f4f62c7e34984ad2326c888f18","signature":"c9a8078413759cfdd3f720dc9e034c98955887cfd68817fe231764fcc0f6ea08"},{"version":"cecbe4e72992345ef522f338bd7e7338a069d800a6b1d38539b639f8b83a0367","signature":"bd5d8d4128b3b2a4a38ca364b140dab68c68b5f19d3882d51cf7c47b9392aaf6"},{"version":"c7d3d5fa3b5916a86b4cfa15ff2287c4b99033974634248c4732d848045ae612","signature":"f6f17bf71d975dbadca58a5c181bff6014b9d53db53f3404d80d73a6cc2af328"},{"version":"05343aef23c8990348d58d87fbcd47d64a2bb6c0e4eb98f9af633bf747b6cf73","signature":"bd9cebf5aea15036a99fbbab8914601e163bfb3fa5a8bc5aa9822713d39fe71e"},{"version":"3a8f2320562326dabbe6093b3c61740997757cbd16ba12864fc9fd9363091e97","signature":"d31ad77eafe5d65cb8e7eba8ec6958e3406b3f74876df2675398dbf17c0dfe91"},{"version":"a538311c345949cc9c25d9fa687e6ded7d306ff4ef3bbb47ec1dfe850a309581","signature":"7606b20785e6f62ee46fc1890e53a4a824f59b6442389fec7db7ad1eb73fc022"},{"version":"b34890404502ee8a71f01e9b32caf020e71d89588173f1f5f10b830b11776f58","signature":"3faafdf02a62868541af7bf01d0aed7a5698fc894a120978a62ea1ad4492fa84"},{"version":"48e589080f69a8cd40fa2a08937dfd5003a4b8074620623b29906a74dc439ea6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4aafa314a294c32ff59181887260bd166656c18562f1609d9cacfe2106e9d5d7","signature":"b04135d4b71449cd8e67a8878a8a58d5ecd65c77011972258f65d26e7cf1b9ad"},{"version":"c82aa540e266447aa1cddc9419e8763ab8143e49f720043ff93816b6b02f98ce","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ff8a3540a563165756b576980733f694086d2194be10c5c7e9b51ffbbd4c0fd1","signature":"c977839e9336e5541d8461e5298712a6306a506d762c73564eac9bebecfb8aca"},{"version":"e16dcaaaaf9b6575168b27d0a18175d21a430371f30f625dced8df4d1e228cb0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ab4f6860892a6d59e3d8c04a9f195cc27c9dfd7d8bdc9005618db993a393c1c2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1eb99355d91f03273d1068f8bfe1f99f83ef677328ea3eeb3fe0ef47245e0ea2","signature":"051b94a6cd2f3cae511e63f7b5e573ddf4e06d04a8658973f0fc1967cb72d294"},{"version":"63bd40218c140d01792a2649faf74327ebf9154e8143e77755a020515592a8eb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e04960cabaac62010909964ae19863386b41d43d39ab9fa1675a2d042900c910","signature":"f6289ac2f311cde6f534de63f64ab970e152123f5876d0f340aa8944db87c143"},{"version":"f4170fbf34ce6b0389878f2c475ae2153f42a2927e847b6e9b14a2d321e8e2c0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4aeecef6615b62f1c87505550a7293ed79d384e547638ef3368f1e1139a676c8","signature":"1163c69c506c6a8501957e3982395d56ceb11ae2d16db771929f78d495641e56"},{"version":"b28890cafea8971468be201da9a41f14e03f9df086ab516cb833e1b60b26d64b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ca3cf79417333e61e5ac11b3ef5196fe1aaeff717131e3359158328b97e8522f","signature":"173cc8c851f783e54b101f62d9dda7563df9d7bfaad2b647aebe40c4f5a60953"},{"version":"9d9681c05ec3d7727ae1468afe5f2d2e10634be8ed29c5c2a9ef1acfccdf7c64","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"0d968953ea99e3c652c4f27655bc3d19f8bb456866c9a434ccf6e83a7f249ffa","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"89b02824819d6efa8dab377a621cb6c2d09c5a51607c55eb276c8933f8e42fb5","signature":"a5df17ac4af58d3d3e09cde22268291b429cbcbdfd2d128ad65f2889c07bff92"},{"version":"48c9e5da30b52e7b8a2605c9a99a3062f788c5f4a64df383f553fc1bbc6bc586","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b114175f37c9c8d6d6e61c219110ca5f09e77a50a983950bae6c9830e5e672b5","signature":"778194f31c2dacc8973beb8904327d3a7f982c97b5d7462ccf937a581429fa8b"},{"version":"741a3d45b5ef82e29381353d96494eee81a2d9dc6c0b7237ff9e915fe4fcfc69","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5ca6296c0197ddc5dc8e2ede74bc07e8ffb010e402d9031aad05935bd8fb4ed","signature":"194663ce8d586caef675a246b39aa86f7ab30c653d3e2401766d0cc52bfb44b6"},{"version":"3114c03fd3ec537066470342f8587a5512f0adacadae4a5d1b05e275cb01704c","signature":"33f04edbfd8687165c0b924c9c7dcd233048792911cb852ede409b2ed9982c2d"},{"version":"4624943f01c41a6ee4eeb326bb99144ccc7f84bf09cea72b04a5b216b30ea70b","signature":"dfaeb1e2d57e53b3f2b4c2a7506e46f1128d33c6b935f8e236e32848f26ac536"},{"version":"5bbe04c91ee41b0fab6b12c5b9660dd8c26cdf1f2bbb75690b0d3c99f16ef1f1","signature":"01a84e145cfdc39ca7318e8e46f431e49b289a7715c8cdbfe1297a11399e3931"},{"version":"2fd50355e32aba3b252e58b1b6f84078381eaa7db835503154efd59eed269633","signature":"aebcbabea1a5fcc3989730c68500831b6bf1c8763e30523acce69d25a85d2709"},{"version":"b1ba8f7d8664b1965cee1720697efec2df263e7e0d31242cd4f8ab0a3891f1a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bfa544d9b15f168f41ed03430d49820b26ffcd012207c46a1625c2ef403a4f2e","signature":"7751ad05cbea188242c53dbb5b29b765e6160303b8f1efe34743caf20bf51abe"},{"version":"5641e8f1469ec49f4f2b5f27e4865cf240ee77fd012a94d23634d384b18a3e2f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2e2b1d8190d03c28abed18f5c4f7768339162d0da92b5619376bd0b9eea74831","signature":"40d2d310f365b4fb5f2796ae880901fa7c87f904976afc3525417e6b651442b6"},{"version":"25230ddd8c1fe680979c15aaa771e4ac15e6521a729c2d59d8f244f4c595a15d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"23c1fdacc80645510e9484fa710a3140dd6d9dbcf7e71e50be44d57c2f78e335","signature":"3ffa82795b12f4ae2f96768e399eb093048f4f3b623ed238d10f5ff1e839c725"},{"version":"1d03cf8d6ad3d5d370b8d05a88b3b08cce493aee26bcc8d0ab2d5cb6587bf1fe","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4ab0bac2aa8b6558968c5169e3e1f0a204317ef57e8144f86eeb558cd8af4eb5","signature":"4632114f72f93bcce0d7fd6f0e7cc0caa5d1ee5d385e2679f5caf11c0ff8a38f"},{"version":"65313f8f52bbc60e351bba4070e24c0ccc46fe73b7b9bd05875576625dc6534f","signature":"bb1c48e634d76de23d15ccf97bac8bda66c8fd514959dae65494c9cde2808609"},{"version":"5fe78dbd5c690b4fb35281b33854ba229a7bba7ccf9a188b147c84abcffbd4ba","signature":"a195b7a56ca781d46d3d7a556ef5f6af6e6bbff20ac71ac60e497829247752d1"},{"version":"62219a83631748f026e8af7eec54ddd58f85e7809d21ed684e0759a06e3ab5fd","signature":"5422c05a52ae74c87c282beb2561e0f26ab52fe73a9b9575a8fc2cb76cd79d76"},{"version":"0dd0a78c6b5523926fae4be11a8132035f0f5a988c2220c22a94d4377feb5251","signature":"b08d0d2cd36b530a9249aeca87fac3db99074107fd570ed0b1dee24a6720c5b3"},{"version":"179ec2be3ba2bd6cef09e1943bdefce84888c53cbf75dd0cc66e25fe73066cf3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2ca412b5767dd81af30a59e60e3174c04cf59e49c89d675404abf5f3c5fba8eb","signature":"0b91a9b21cd5ad0cd265376cbaa5ad1cd6202825416c4bcf1609c6673d78a0be"},{"version":"d48da27c508d6bffaf72588e0d9fc749adff03b575cc20d9027c063c6c7c5a98","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"412859bcb9fca9198bc3ccf1c96237fff62f03345ea3c5f96f18c49462b2259f","signature":"920e406c286c1b48a00a712bd6869df6197866c11966095b4fced2a4d6942401"},{"version":"f1722a5276aee04daa7145c8e2bd29a24820e9657f251528eda17a30d7de94f6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8bc45826c7d7e7f553304d8a4b37b072b5663a51f2d33a389dad7cf75ce03423","signature":"48123915b9ba16e4bc3d308eb1eb7aaeb86dc0430b97fa83e8cbf6a29888d05c"},{"version":"28ed21c38dc56a3115c873e312aa3335aac2a11cae23253653bef25537c7d3db","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"894b2568a3f56d106d65d2e46c731cc8cc6b335a9c1dfee76a6193eaa79c9f6b","signature":"02ef334a3a2f20926816192ab2941ea3cb09636faa1286c2296be828869f03bf"},{"version":"5f15566bc9158450ac45ff3e4e1c2a3fcc0120bb19a18ce9258c33602ca9dcb6","signature":"e6a550648e241a622a4c69fa2f80e6ff23d38e05582c5cfbf4f2c0cbb96863d7"},{"version":"1941e12ac7464555d86b70e0a6302b91b1a1407ba037fa16c60790e15f3e2e93","signature":"a0cb0206a91ceaee5103cd13a3370a74249b3f45b576ddb1dd76804de2302167"},{"version":"706976704dfaddf508c933961bcae27e0053d8c772238b757605ad22c7e2f1d0","signature":"0dd4ef3114c29c6efb50dca6d70b26b848ce37d839f02976161da75d2d59127c"},{"version":"8139d708b00ccd6b7db5f14a87c7554acccfa089999f5e6727e681fb4c999cb2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78d99658239c9fdd6d38802abb0e872202991c0a074c96e7c4056b9d422bceba","signature":"eaa105ab2d02c7c8ec63155bff6c1308b210e9ab6448bec935ecd3438e85d36a"},{"version":"cea7c4314d9f8462751a38f2fbd7b1aea8c1312f1d0e90df77b36e8302afad2e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"23fb0c2cf17a963141a6077b1f5cfead30943c4eb120bf81bb75ab5b3e80eec4","signature":"15a5a3d534b880dbef913890a97fb50db32658c03917ff3861d604d440a90907"},{"version":"6911e9471f25ba8fae13de0c072b72c66dad502e40f8b6c8740d5cf9ba0f2b3a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b9039512a27d6a5a687011f0af8ccb0adcc07218a8ddbb4b5ae02d62fece2742","impliedFormat":99},{"version":"d11ebedbe79c2e165456a142132ab453b2572938ed76a6fe9324914363b7d6ac","impliedFormat":99},{"version":"f6ba66f3f6c4409e878f48161529c585b3c0e687c8917dd8f6c6464fc5cd4f8e","impliedFormat":99},{"version":"0366e97d9c966d748ad91b782e8ce843ebb692d93a1d211ef5b84cccbe8f53a8","impliedFormat":99},{"version":"56497e1670f16495b1f17d3f4ebda63e5be6fe2fea9f26f0ff3e38568fc95f4e","signature":"308695a8fda6f510e51efb0430009a61897fb5067344c8d84a84496de1274a3a"},{"version":"0e6023f7b750aa03e8871830a8d02d5635ff10de1bab7e00040adb7b9852feb5","signature":"76fc5947686d1f4f95854ebdd328b964d50dc1067e43149867290203d03238b1"},{"version":"9482082c63c541680d7501d2a9c68dc9adfa4ae76e7b6cbfcf11839da9620f78","signature":"776f4607976ab87e84944a655484a4183fca74b8cbd17cb07d9a11a6ad28c12d"},{"version":"8e8ab957a73b47d61ab55b6c968d492d1d98a4271c1ea7bb9044b98545d75a5b","signature":"7017943360e6ac93765cab271d3b04f1b54ca26211e099453e4c9051b4d25df8"},{"version":"f736e672c15e293f44efda0d8f530cdac558d5396f0e7b2d666875a8b16e7943","signature":"178e81c7913720c7c7c48b66c4abac8bc4643bcfcd0633e0055079f724f4b73c"},{"version":"e551a312691df9bb4cb420112f040e571203a8dfc74f0989edc2ef475c5ecaa9","signature":"3f89da23f0c251a47f8534a0b51510d1d782de69990984e592c2cc629746140b"},{"version":"de0fc40bebc22ae7b3014a2ce51f29e5623be0436ff6a3cc858fa6417d8822ad","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6dbed82deb6e075663ddd982c37d544858cf5ca1a172298db093e8a49ee35cc8","signature":"80a5ca154cbf85949324181c775bd2ec7ec77dbbca27a29bbacc8aec4613d811"},{"version":"f16b100f317a322f5529e296714dcc99c19e9e5fb5916045624f8de197c02772","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"77277a37f6635718d6bb01c118b46c0f39c00417713978b61bc6ce5daca56939","signature":"26968396ba8985bd4a1c3e6fdeb001801e0c9e1f159f1cd4d6863bdb2276b9e7"},{"version":"483799096ed70750cb98ed3a868e8d5fa1df422756c75dca6bd4a182bca1a410","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6684293008b434c53e960a1c87f9aa2279730b46aedc963206e05351a21aa911","signature":"67fdc67fa1fd9a9022c6fce8bcd6f18b41747340fa20ec6a9a9f2e6bb274439d"},{"version":"97faeac00a2758e5c4b496f6b47d4a1294226a42b7956bfb0062600b548e52a5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a5438e90c6ba81b6a34080ef1cafd101ee3c0aae9ff6031cd46bb8d59b81c3b2","signature":"eabb04fad55013cf12ee2e212586c4bdc9b245a0a6d4fadf3bcea95bf6bcf7c4"},{"version":"f02c001f18a3bc1d300b7ed52f97c4938b06c50fee761a7053a0183c6e7849de","signature":"53454454c0a7ad3aeef6f7e336a87accb3dd1dcc2abcb696e71619d10e047268"},{"version":"7da4b13f583065968785e4d99cbfffbc337fdb1764bf2ba2b3e63fb2a0249ddc","signature":"e043f7aeb977f236d72c39317e14816a043cd4dd3fcddea658ae06eb78524b21"},{"version":"19e5474a9d8c0a0dd667ec0c343a424ae6e3c4897898a00f440a42dc9976ddd7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3c392554d5bc28caf0738f363702e0003c38f10c280ac114141d373ad4703f29","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"da0f84fcd93700b4a5fbf9c6f166a6cc19fc798231bff56dd1e3875bfc6966eb","impliedFormat":1},{"version":"634ff08e0143bec98401c737de7bfc6883bfec09200bd3806d2a4cfc79c62aaa","impliedFormat":1},{"version":"90a86863e3a57143c50fec5129d844ec12cef8fe44d120e56650ed51a6ce9867","impliedFormat":1},{"version":"472c0a98c5de98b8f5206132c941b052f5cc1ae78860cb8712ac4f1ebf4550ca","impliedFormat":1},{"version":"538c4903ef9f8df7d84c6cf2e065d589a2532d152fa44105c7093a606393b814","impliedFormat":1},{"version":"cfcb6acbb793a78b20899e6537c010bfbbf939c77471abcdc2a41faf9682ca1a","impliedFormat":1},{"version":"a7798e86de8e76844f774f8e0e338149893789cdc08970381f0ae78c86e8667f","impliedFormat":1},{"version":"eebc21bb922816f92302a1f9dcefc938e74d4af8c0a111b2a52519d7e25d4868","impliedFormat":1},{"version":"6b359d3c3138a9f4d3a9c9a8fda24be6fd15bd789e692252b53e68ce99db8edc","impliedFormat":1},{"version":"9488b648a6a4146b26c0fd4e85984f617056293092a89861f5259a69be16ca5c","impliedFormat":1},{"version":"e156513655462b5811a8f980e32ccd204c19042f8c9756430fe4e8d6f7c1326e","impliedFormat":1},{"version":"5679b694d138b8c4b3d56c9b1210f903c6b0ca2b5e7f1682a2dd41a6c955f094","impliedFormat":1},{"version":"ca8da035b76fb0136d2c1390dda650b7979202dbe0f5dc7eaefcde1c76dee4f4","impliedFormat":1},{"version":"4b1022a607444684abeee6537e4cace97263d1ef047c31b012c41fdc15838a79","impliedFormat":1},{"version":"dd0271250f1e4314e52d7e0da9f3b25a708827f8a43ceff847a2a5e3fd3283e8","affectsGlobalScope":true,"impliedFormat":1},{"version":"47971d8a8639a2a2dd684091c6e7660ec5909fed540c4479ca24e22ac237194e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e1075312b07671ef1cbf46409a0fa2eb2b90bb59c6215c94f0e530113013eeda","impliedFormat":1},{"version":"1bfd63c3f3749c5dc925bb0c05f229f9a376b8d3f8173d0e01901c08202caf6f","impliedFormat":1},{"version":"da850b4fdbabdd528f8b9c2784c5ba3b3bedc4e2e1e34dcd08b6407f9ec61a25","impliedFormat":1},{"version":"e61c918bb5f4a39b795a06e22bc4d44befcefd22f6a5c8a732c9ed0b565a6128","impliedFormat":1},{"version":"ee56351989b0e6f31fd35c9048e222146ced0aac68c64ce2e034f7c881327d6d","impliedFormat":1},{"version":"f58b2f1c8f4bcf519377d39f9555631b6507977ad2f4d8b73ac04622716dc925","impliedFormat":1},{"version":"4c805d3d1228c73877e7550afd8b881d89d9bc0c6b73c88940cffcdd2931b1f6","impliedFormat":1},{"version":"4aa74b4bc57c535815ae004550c59a953c8f8c3c61418ac47a7dcfefba76d1ba","impliedFormat":1},{"version":"78b17ceb133d95df989a1e073891259b54c968f71f416cd76185308af4f9a185","impliedFormat":1},{"version":"d76e5d04d111581b97e0aa35de3063022d20d572f22f388d3846a73f6ce0b788","impliedFormat":1},{"version":"0a53bb48eba6e9f5a56e3b85529fbbe786d96e84871579d10593d4f3ae0f9dba","impliedFormat":1},{"version":"d34fb8b0a66f0a406c7ce63a36f16dda7ff4500b11b0bd30a491aa0d59336d1f","impliedFormat":1},{"version":"282b31893b18a06114e5173f775dd085597ca220d183b8bd474d21846c048334","impliedFormat":1},{"version":"ed27d5ce258f069acf0036471d1fbb56b4cb3c16d7401b52a51297eca651db62","impliedFormat":1},{"version":"ec203a515afd88589bf1d384535024f5b90ebe6b5c416fb3dcca0abd428a8ba4","impliedFormat":1},{"version":"32a2a1374b57f0744d284ca93b477bd97825922513a24dfe262cbf3497377d96","impliedFormat":1},{"version":"a8b60d24dc1eb26c0e987f9461c893744339a7f48e4496f8077f258a644cffab","impliedFormat":1},{"version":"3f9df27a77a23d69088e369b42af5f95bcb3e605e6b5c2395f0bfcd82045e051","affectsGlobalScope":true,"impliedFormat":1},{"version":"9fd080a9458c6d6f3eb6d4e2b12a3ec498d7d219863e9dca0646bdee9acce875","impliedFormat":1},{"version":"e5d31928bee2ba0e72aeb858881891f8948326e4f91823028d0aea5c6f9e7564","affectsGlobalScope":true,"impliedFormat":1},{"version":"9a9ba9f6fd097bb2f57d68da8a39403bbe4dc818b8ccd155a780e4e23fa556f2","impliedFormat":1},{"version":"e50c4cd1f5cbce3e74c19a5bbf503c460e6ae86597e6d648a98c7f6c90b596dd","impliedFormat":1},{"version":"fa140f881e20591ce163039a7968b54c5e51c11228708b4f9147473d06471cf5","affectsGlobalScope":true,"impliedFormat":1},{"version":"295eca0c47be1191690fd2fe588195fff9d4dc43852aceb8b4cab2aa634579f0","impliedFormat":1},{"version":"59ee7346e19b0050508a592702871dc943083c6dcb69a47d52e888115d840781","impliedFormat":1},{"version":"067712491fb2094c212c733dd8e2d56e74c309a9ce9dac9e919286b7245a1eb4","impliedFormat":1},{"version":"a5eae58ac55bd30c42359e4b01fb2be5eddac336869d3f04ffb4daa54b58f009","impliedFormat":1},{"version":"d12d691ef8933e8db39f2ca81d6973940ff5e37bb421752f5b6e7bc15dea3abf","impliedFormat":1},{"version":"4c5f8bd9b3a1aae4e4fddfee41667e495a045f73ed603993038fa6a8ba92fa14","impliedFormat":1},{"version":"dfb274ab0f319cf18ce7152067c25f984c7fd1924fc72b3f66734588444c934a","impliedFormat":1},{"version":"108c8c05cbc3fbbbd4ff4fc0779c9bef55655c28528eb0f77829795dc9f0b484","impliedFormat":1},{"version":"a7e5444d24cdec45f113f4fb8a687e1c83a5d30c55d2da19a04be71108ad77bd","impliedFormat":1},{"version":"41ec17e218b7358fcff25c719bc419fec8ec98f13e561b9a33b07392d4fec24c","impliedFormat":1},{"version":"23c204326746e981e02d7f0a15ab6f8015f9035998cb3766c9ddbf8ea247aea2","impliedFormat":1},{"version":"25f994b5d76ce6a3186a3319555bbba79706dac2174019915c39ac6080e98c7e","impliedFormat":1},{"version":"dfa4e2c6a612d43851ccbc499598cb006a3a78bc8c7f972c52078f862fa84e47","impliedFormat":1},{"version":"02c1705fa902f172be6e9020d74bcd92ce5db8d2ef3e1b03aabc2ac8eb46c3db","impliedFormat":1},{"version":"99d2d8a0c7bb3dd77459552269a7b5865fa912cedab69db686d40d2586b551f7","impliedFormat":1},{"version":"b47abe58626d76d258472b1d5f76752dd29efe681545f32698db84e7f83517df","impliedFormat":1},{"version":"3a99bbbbbf42e45c3d203e7c74f1319b79f9821c5e5f3cdd03249184d3e003ce","impliedFormat":1},{"version":"aaacc0e12ab4de27bdf131f666e315d8e60abec26c7f87501e0a7806fc824ae6","impliedFormat":1},{"version":"3b4195afd41a9215afc7be0820f8083f6bd2e85e5e0b45bb0061fb041944711e","impliedFormat":1},{"version":"108df8095f5e25d7189dd0d1433ac2df75ec40c779d8faf7d2670f1485beb643","impliedFormat":1},{"version":"ddd3c1d3c9ff67140191a3cf49b09875e20f28f2fc5535ae5ea16e14293a989b","impliedFormat":1},{"version":"7b496e53d5f7e1737adcb5610516476ee055bf547918797348f245c68e7418fe","impliedFormat":1},{"version":"577f44389d7faedd7fc9c0330caf73140e5d0d5f6c968210bff78be569f398a7","impliedFormat":1},{"version":"3046c57724587a59bceefadd30040d418e9df81b9f3cfd680618a3511302ed7a","impliedFormat":1},{"version":"15ccc911ed15397e838471bfe6d476c28deffe976c05cb057e6b1ea7491242c2","impliedFormat":1},{"version":"64b5a5ebdaead77a9a564aa938f4fb7a45e27cda7441d3bee8c9de8a4df5a04f","impliedFormat":1},{"version":"a48037f7af5f80df8973db5e562e17566407541de284b8dadf1879ea3aed8a2f","impliedFormat":1},{"version":"dab97d96ce986857150db03f0d435b44c060d126b4a387c7807f4e9f6c92e531","impliedFormat":1},{"version":"85f39366ea7bc5e34b596fc97de18a7e377856755e789d8e931054f2191d9b8b","impliedFormat":1},{"version":"daf3ea3d49f6e8a2fa70b7ca1f21bd97f1b65021b31fbfccb73dd55f86abb792","impliedFormat":1},{"version":"b15bd260805f9dd06cd4b2b741057209994823942c5696fd835e8a04fb4aab6b","impliedFormat":1},{"version":"6635a824edf99ed52dbd3502d5bce35990c3ed5e2ec5cef88229df8ac0c52b06","impliedFormat":1},{"version":"d6577effa37aae713c34363b7cc4c84851cbabe399882c60e2b70bcbb02bfa01","impliedFormat":1},{"version":"8eaf80ad438890fe5880c39a7bbf2c998ce7d29d4c14dd56d82db63bd871eefb","impliedFormat":1},{"version":"9b3e7f776f312c76ac67e1060e5398d7ac2c69d6a3a928a9daaae2eb05b15f56","impliedFormat":1},{"version":"202042eccb4789b7dee51ba9ecab0b854834ea5c1d6a3946504bfc733d4468c3","impliedFormat":1},{"version":"2b2ef76a9f36094b07ee6f76a5ac6903f2f65c0a20283201814a8d1e752cb592","impliedFormat":1},{"version":"8882e4e087d0bc8cc713cb3d8090c45d33e373e6f5c83e0f8d00fe6a950ef875","impliedFormat":1},{"version":"6325b4daefd385c07b6949daaf20a2723a75b3a86159f2bca3c53fe70f98e603","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"882b28abe64dae4932c83ebb71e4155da340929fe08a2055f3e573ef17f70fc3","impliedFormat":1},{"version":"4a3e425808751200a7709671667ad3d7e7cbfd0a06d469cab42adf06c2601f4a","impliedFormat":1},{"version":"401da46338f5b4f97c2a5f8a0faaace045c51aabd751d2dc704159f64feafe89","impliedFormat":1},{"version":"c705d4594093bcde53fc292c5526aedd3145170ceba73a9476ee97de6a915fe2","impliedFormat":1},{"version":"5ba85c521dc7008fef9e3d23b0b505fac64368af31d819fbacfb170045e383f4","signature":"9810c75cae8b5bb5a64856a1249d92898fe0bc08fb7edfdf26e7d1fb64674978"},{"version":"ec8579872bbc5589d66bb4cbdc0ef8bd681d79b8a814fe2bb392f02986920468","signature":"b2a6477636bdba25498d233db9cbd9d4aaacb75b90ca6fd8168554a8f6a1329a"},{"version":"a17a05c8f917ec2c80f31f3aa0c16d9742b93288832f1f9297e0765b2e3526a4","signature":"805a5ea0e11b94dbf29528d7ae91318d07549b5c4813f106921ce6abe3abe3f7"},{"version":"9ee1560888daaebf156ffe288e58a1b02b7127405caec41d3fb966af67f4b5c2","signature":"d5492346380d075b56787e452acfd7bfea5896bea56f64444d9170b0d003c9a9"},{"version":"4cd85a8230d17c0e948b99d06021edff6a35340e511dcd49a30b5f3a740fc5d7","signature":"96205459c4d05027bd9f523979b9e020162812330652fedd004ef39343cd8cb7"},{"version":"887cfea1dbb87aa317bbfe0ad3e85fdeab7e0eafd040d8339fc3e1ae43ea95dd","signature":"9b365a218919f6dade45f83796f8f4b0e820871ac894a1991010fa77ab0619c0"},{"version":"7999dc74b57cc2dfa1640ac746bb755420a4375541732f4bff420751a375d7a3","signature":"ece1d6742ed1d0860af638493866df8b946c3f583875e62e22264d7454f8bb46"},{"version":"6856bfd11dbca6b6cbc812621a11ed11c4b4296d7b971c33e2ffcf1b7060a5f4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"83530f88eb02119125b1730602056e1dc2a7689fb1f4e3a8eff57602e0cf8791","signature":"e23e63f94215cf92ed44b17c81a5cc023571e6e57668d286d0f66dfb7fd29e64"},{"version":"02dd581476d418e9d891f9bc234ba68f0a22bbb92bd4fd5402e62367572cfa3c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"08eb7930ae7af32aa4070200e2209a82ca0d5191f4c6a56dcb3d7af666b61d10","signature":"0cc69bfa0dc106cc3e9f90bcda9fc24745adb88adc9aed46cec3211ca418bf30"},{"version":"d779cc3dfaea48cc4e8d9225bce2a930fe7f5c19d35f1362bdd915044261768f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"dd218a0ea3d23ec928bbfa257718e39a6064f56115d0e47678a10a5f86182920","signature":"3a12ea40f44d366677ba0e45a0817a8b4b4efb30aa40fbfd559c99465078e197"},{"version":"86153db5e23d65b4cbae5b7ea8e730119094faa1865aec8373cdca4849c08d7c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3f8c06236d42491454328842e4b8d96bff6ef3d6ba897677cf012eae9a8a8531","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fbad2026e5d0aca8314c0e69b47a599e03a96a00bdb09bc5dbeca747aa71f1a3","signature":"dbf7d075c346bf692337c676353683710738fc6cdfed3b25b4ab26858a25014f"},{"version":"d20530ea19f436fd93bdd18d96ade390f04712323e4d8811bd2f8453182129e3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4e27d5f69742c8bdf2da332909d403778a263ff986536d9adbc768a0f9eeaf11","signature":"768c2dfab2b90c6569c42a473907528eb55a45c3ed0d6256e35a8becaad3d8c5"},{"version":"6a3e54c5512881e1a8a4a0199482a0e68dff96200fbcb7a08b38a969d2cd6699","signature":"cfaa98bda2da5362425dc511b0c2447d3b25087f0b74c61e1990f47a20913766"},{"version":"c635a564785c5f590f4ece775c5df77f97d17f34c48a649678e845510f1b446c","signature":"21b4f8dcaed267776a667be6fa7f28b8fd59c1c8c0fa2cc517c0192b8f06c31d"},{"version":"799f40d6da71436ad2d692688e04566bc8c1fcd1926a254af306fa8ae9ff4aa8","signature":"995274a829121df36f8d9cc22f7a63ec3bd987fcf732eb65962dea5570751876"},{"version":"2cbdee534591575c81aaf2d99884635361a510a470bb83dfd2269eb6c92b2261","signature":"ff371292b082e5e53f6fdce86ad4ed7771991b553916741aaabcb63196b69d4f"},{"version":"510a6f4314bfbc0b4ee033439734a3c3c3a7bdf0705145e3c654ef372a3b579d","signature":"53e4c6a61b47ecd85ceb70d1d4512130dcc531c850b3b6bfa0ee82ff83a70698"},{"version":"e65cb4ccf7c87a1d754e9b9a6e94d084c7ab28e16f8a78cfda92a6f5918a69e0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"613bd3011a678abe67087d230ea80ffd528d4e28955502fe7f4774362e77bbcb","signature":"49ef7c07fd81b587af6908134ec0e249b2fe2ae26d8326492cabe8030f6e8693"},{"version":"bc9c4225c4c1dab675f49a71a8d971eee3e2e543f1771c14276b45010f6ffaae","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"47684074834c5e06d27d5b3e0db002782ec0f2dea069aa2135e590118c9362f7","signature":"2823ac5458951858044bcf11891ec9ea7838615d0bdcba0817aadc71cf1dcdef"},{"version":"41a457cad7ac02cacd0c9fccdbcf80d2b44bd92bd0d4875b69c94c364a6e401e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9f9c05a43a0a2d8e72c00f9173e85b6fad81e8499961cfa611fa905a257c638b","signature":"17604e5e845fc8ec8fb28f8fd4c99fce6f29b8f67eec439bab4fcd479ca8a50e"},{"version":"5052f6d600f9d1e9cede06feec1faf439378112b9c3c64e9311e6b495df5e393","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6a3e54c5512881e1a8a4a0199482a0e68dff96200fbcb7a08b38a969d2cd6699","signature":"cfaa98bda2da5362425dc511b0c2447d3b25087f0b74c61e1990f47a20913766"},{"version":"76b0c789fd0a3b5335a1cf8aa8ec3a07c8384c22f7a3de18e858f57442de30d9","signature":"315bc7c950d58772966319f8401256cbf36525ea88146a7f78073fdabb3a9388"},{"version":"3ece61c6c08abccf9a57edbc4ad491f7fee0976b510f2eb9a902989353ea1796","signature":"13dbd27bb1d1ca3404cc28d37239454129f6ab96aa921294af5bdbc0acfd4c6f"},{"version":"1ab721520d98f380fc2eecbea24559da263bb12c054ffe407653e20a779d33b0","signature":"4a6172213bc97ac83a39d9c3ba59a98239476225d76cd68004f66e1e0adae847"},{"version":"069b2f5c89a854b8d0777017d287ce6603eb74f8db3f1c207a699c9d830ed276","signature":"b562f48096a09af4b58c0f90e6f1921065cbf0bcc25fccdf75d21d5faca587a5"},{"version":"33173d2b3c235c36cc9e77882f146d21e429f0383e2db5483b14ddefe9a3b066","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6e4c5d3ba96c381b0534c4985f099a284d77b69d2d6c2da77f09bbacb0d0b183","signature":"0d9f9121e609de171240a484d3f39b5060bafb8d8534d3b6e95addc07501583c"},{"version":"ce73073c216074a4015ff67a3a4fae8cd6f2b27cfc3b61df2c82f86b8ff90b6b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cfa4b4101c50834e5d7173fbe21ca806365ba53e8d8d11285310670f043f7a8a","signature":"c0d9f1c0d6656828f9d79b6d490457d5c98fb93c2e8ef8e3aeab889bbc57c543"},{"version":"31bb743670487e571f1477dc0100cea116bf9f13f235dfcf7eed78353baafe3d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"843808a59deb22ee90a34b86a22487ca183dd7316115edaa12707a8ef764ed00","signature":"eb0881c5e929fd7fb41dbaea899cea8453ebbff25a53b40bde3e722dca90b2ad"},{"version":"faec6f376d10c439d4c1a16a386e1cca77353c9d330431699e300ee0dbdf6045","signature":"1f71876bf509d705f6dadfd7dfbbb169e6b2d024697691d6037e9ddc3c0c577b"},{"version":"7fb02a4d0ba956a908489ad5a14afe53ea8a6af1671f9563790b8d51adf6a18e","signature":"9cae4fbcaab119861558b35bc06b902c5e12b56cc61fe00b09b74c1b2753ecd8"},{"version":"8a4ca198b7c28931aaca474d2e53f36e111005a8f01f59ff146d96dadef42044","signature":"102d68887b41127a6195e88f8f68bfc1ed172389c1398c194218f8d6ca22cfc4"},{"version":"45a4b3a6aecea5c2a988907602b904792f62a432fbf60b3a2567f8b8f80abcc3","signature":"5ea51e2a055800151ae8c66f7c757c61af6bb7e5aae59d3f930aa2e3fd2aafcf"},{"version":"c8c23d40c91ea3e482e770a495036472551e4e3d63805d7e59f6b529aa80ee7b","signature":"566de8a1ba7227562827de899c257a5c4cb2f52f688526226ff9a3a77b6d81f6"},{"version":"aed84edf8705c35fd9505a2f4fabfee7e40fb40df3c604546c26cb6a75c82b5a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a21b16e26fefcf0fa3501377571130b6b1dfea4e4e39ae9ca49e5202d397d289","signature":"3b3104ebcc3528ad6233c8b56eb2851ba71709eb570c6f3375e5af1ae4da6e9c"},{"version":"1b4c4ddb69c3c6aced74dbdbc0cb0952d54c2f3f525f7539dc7f8b879ff398ac","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"d4aafc88f449b3dece1139b101063857a243632c11cf46d7a237947c60aaf73d","signature":"ba2014afd9d39b84d2a3d448395b5bfefce1b7a643bb0b65a7b7bd96e871b48f"},{"version":"699b6a641c44aec0620bcac676d4ba9c952935d59ba95995ee2f95cf0df9e4b6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"5a85448d059a9707654aa9862b15002204eeba605295f72b7adfac3f06b15e5f","signature":"b992b8d365a25f19870ac2673d0c934bbc07693d412bc6e0dc3348690f080e65"},{"version":"cb09494c8782717711e8cf5272e3487b93ba8d06b1f739e547d9685bcb23350c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"219de40f19c7d6f958c19b656a60c6111e9c0b134b3621fea6a921dab795575d","signature":"b372ed7a377647cbf7b25df64b2801a596b28a57f5ebe7ea2ebf637482503238"},{"version":"644b85acd27474f4ca95c499429ac548bbe2a4502ead86204fbe319762f2478c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad9eb59127a4d0523892c0849ad0a8ece6d0affc43f1cd31cfe822357e2825f1","signature":"ca2d5a2abb4eb48f173d4015b325550d8a0c2b2bdfb9c5b36f9a61531c06668f"},{"version":"cc3127d12e27b24862881f34a1b8819deba2c72cbb5f82e9e09f481cfb9f08ec","signature":"6297275caa40f3a2b188c436d6c30a93ceefea5342c419e8b696ee5c9f8a5595"},{"version":"94fab564ab8fb55af74a70509403921c86fe03d19633811a40f959fbdd1b2730","signature":"f96f3f7e1612d413c95a66e638a4fec032fd9653f3a6210ec79e5830fcfe5761"},{"version":"b6e6b0a412ff6fdb88ce7f49bd887583b1ceaf5855b6180fd820b266a46c83c4","signature":"675dc0498b58f7e94e3b28fc542c50db2a2176fbe2a5f5013013b1033b688ba1"},{"version":"38d9f96b332c0b24da4d3a3367626af48004b065bb7fd1e95c22b88da4c4986b","signature":"6af03c8615fb7afb7f610384616178c60e5cc43124d3ace60d60b5fea8308af3"},{"version":"48a2da34607388aaa01eed1bd40fbdc9ab1b9de9a82a882e8f2b9099e7dd61e0","signature":"cbd70b0bec138f5d35584afa22ceee7eac21d579f6a0052a4d50fe445a0acdd4"},{"version":"671d5db9155f51909fa66748be739332b1052b841bd3a896606d02ddb689074c","signature":"2a33371cb98b40596420d07b9ff28fac6bc0da83e6f774350376538f255fea8a"},{"version":"3369191f57b3f65105ee8f2186a7b2d7be9750a53574e5bd309be00825d5c244","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"997c98da784db39942db1efa0d850088df1da2bc1a04c9c7fd90c9a2b21904bc","signature":"295371aa99b13288c58c44dbcf52c22c9b6ff0bca44b8a544ce31ef710033ff3"},{"version":"f74357d99a2413be1b753a97bf2c436cecb581d8fbb9631e222ace9226e298d9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"dc5546c1213036db022c5ddf2879e8937abf18a8c8251a0560431482ad46b357","signature":"ada185e845c9ebd71e720a1794e7fc1d6362fc35692d62a918b965dc0800285c"},{"version":"213211567b80be96c91882e0ef5517ec58c3a4c137e497fda3531298462f2e93","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"edab3e0f19ae936330c970a57fde46cace366feb30992e241ff4a216c5265bd8","signature":"233d2d80a6eba34812e44e27ddc1e0d04c95aca7ac4d981ab432dde3b6ea6ee2"},{"version":"3a1b6cae1a558f9282a5e02fef2c73127f796239c52f6499e2d360e2f96ae68e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9bc5f0f923a7e8c610dd1a4931effd93e741c3d214fe9c92ca33849ffb8602ec","signature":"777b1090495d3301be05b674c1716d3dea2caac13ec0a8afdf5d30a2c02fc4b5"},{"version":"d9bf100319a2f94c431bc7e65433ba0907fe9479f865299be35e924b692414fe","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"57f89d3e2d3321849da9ae7f95e4b1e066a46d9f9ad2918ac92e5c162a55a41e","signature":"6f937a36a0554c0813743f5ada9665f6bcb67d0d2839ea9c404a5aec7feef8de"},{"version":"4836c444f4aa4a95580aef5461a01e64308da39bd0cffcafde960eb359092b30","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"588addb4eb7f34d4c962d2cb0c45ad24698f7a70b5d8c7973297a5f5ca940f3e","signature":"b32d06279ac12db33a254ce2424c37b0e757502d55ae1dacf7d77240842a07cb"},{"version":"1bdaaa929028e63260ee0b975c2287efd7f44c1696c9673be52e412d29882012","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cd0f1cb9083130a1097493a9f6cb24131a4e9bb44c6a844f8599508250d6c480","signature":"a0126b2500faf07aa59d3709c18de53b15162f63373203d0784cca319b1e960b"},{"version":"1a6791eda07ec1773ac54d49702a624735b74311e380e93871470a211137bcb6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"72f8b6e281615cecba1e5507d2facd65664e5d6cd86d4e9a93d1f7098418086c","signature":"ca1eee8eda1dcec392d95c606fabf41ded7f9e872dfe8f1a9c52db8795af332d"},{"version":"d5ef090496ad9a7606522d5c3e876387a14efa0e1047364dab0a177f234d7907","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"899caf3419de9c34926f6706348ef57abbf7db30c87003627e598a155b7e0c78","signature":"08b19930b4ba7c5a6eeafab4eac1c3b2ff7be1cf733f566fa4c8c93f3618d847"},{"version":"0921c871035f591a549407a9333e8bbefb59252c66d9c37b5378f705e9b5225e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c561cbe6385a58164f5f8635fd27ce5f9f742aec3bf1a7a0b03c32ec3199ec31","signature":"3050c3f3df8238f4ff45a8f8f38ea5ad94ad7e2b9bdc259d0aaddcc511dc91fd"},{"version":"c0a07e39216c64633b28b826115227a3bee607d69a5272f88e2c5d8da3a5932f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"64f2ecf30dcc0ea64804a831a9a8da446b6c634d0dfafa44a2a1f5235558bb5b","signature":"b4bf6c60a7c19eabea44ff7ccb1cf608d20fa0a72a93ab170587ec1eabe613c6"},{"version":"efb5303e134c6c6348ddbe69c79bf1bdfde3b061f6993cf29684b9a4d68562dd","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f24bb51cea61986ecce977b2bd9493e322386b68e10d5d605d947873e2804e3","signature":"2628e648dc231f207f77a9996a765453fbc5b751716c6d14d06d82f46c8834ce"},{"version":"f1a039a40bf3f6dffa76f07e26b71c4783377396501c12e2a1961bdc1190643b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9de5dc3aa0f8d23427555e44776e3c84c6c3f013de25a43173fd63750f9628a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"04471dc55f802c29791cc75edda8c4dd2a121f71c2401059da61eff83099e8ab","impliedFormat":99},{"version":"8e5a1adaae977af48997dec89d6cc6075aee453b661faf7252b4b3a44c7f2bbb","impliedFormat":1},{"version":"5c54a34e3d91727f7ae840bfe4d5d1c9a2f93c54cb7b6063d06ee4a6c3322656","impliedFormat":99},{"version":"db4da53b03596668cf6cc9484834e5de3833b9e7e64620cf08399fe069cd398d","impliedFormat":99},{"version":"ac7c28f153820c10850457994db1462d8c8e462f253b828ad942a979f726f2f9","impliedFormat":99},{"version":"f9b028d3c3891dd817e24d53102132b8f696269309605e6ed4f0db2c113bbd82","impliedFormat":99},{"version":"fb7c8d90e52e2884509166f96f3d591020c7b7977ab473b746954b0c8d100960","impliedFormat":99},{"version":"0bff51d6ed0c9093f6955b9d8258ce152ddb273359d50a897d8baabcb34de2c4","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"13918e2b81c4288695f9b1f3dcc2468caf0f848d5c1f3dc00071c619d34ff63a","impliedFormat":99},{"version":"120a80aa556732f684db3ed61aeff1d6671e1655bd6cba0aa88b22b88ac9a6b1","affectsGlobalScope":true,"impliedFormat":99},{"version":"cdbc46ed576c5d7c0ec7f51c628a136ba25cfa050148d5ceaa3ccfc2f228f91c","impliedFormat":99},{"version":"5d2bcff208b51a9e89e69c72ef77ee0d8ec631955709d58f27d6b28cb9f500a2","signature":"871edfa067149771f512d8a77c2167eec9e532825fac0da705a31b62082bed71"},{"version":"cdd2c537282e5b19b1334da23868924c3b24efd7c1c81973d6943fd0816be588","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"42e73b2f164621c10cac49c2df0666983657dd29932a8fcc52951040c57c3f89","signature":"c968550f9d6302f54147c45eb86fbb741711b8fcde9470071d016c5cb45306b0"},{"version":"7b1fd7f07aa61be279f29c42f7dc8a876478f9a10cc49bbda4551db640a771da","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7e50d2807063b83ffafd2655d9928e646d9d061cae4e68af73ecb132af825ca7","signature":"196cc58681e570c9eb8969a03df1a4c09ca1871fb30c0fa8731f16b76031df42"},{"version":"0d82601d36ed5bfcd384f00489072984fc418c09861bda1187f515426901cfc6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad42288f8c9ebfd4451e4256a2f091cfc26b958d29db3612c19efbbb476882a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"59b54146333f3ad62db9f636c988f09378b61c55c4bde0541602011d458a1ca9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b7724c5f8c92bf25a109abae1de5f939f4b2f08421c288329bbc01bcc515a265","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"f486c50bb72b9ee6ee6ee90d4547d215a99b43dcf475839c945bf4f6359e44d7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8d7cbeea0454e05a3cdf3370c5df267072c4f1dc6c48a45a9ad750d7890443d7","affectsGlobalScope":true,"impliedFormat":99}],"root":[[553,555],772,773,779,[802,809],[860,962],967,[970,976],978,979,1032,1033,1039,1116,1117,[1127,1131],[1133,1141],[1167,1204],[1215,1285],[1296,1356],[1361,1378],1456,[1461,1548],[1561,1571]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[1570,1],[553,2],[1571,3],[554,4],[773,5],[1178,6],[1179,7],[1140,8],[1180,9],[1181,10],[1168,11],[1182,12],[1183,13],[1169,14],[1184,15],[1185,16],[1170,17],[1186,18],[1187,19],[1173,20],[1188,21],[1189,22],[1172,23],[808,24],[807,25],[1175,18],[1177,26],[1176,27],[1197,28],[1198,29],[1190,30],[1199,31],[1200,32],[1191,33],[1201,34],[1202,35],[1192,36],[1203,37],[1204,38],[1193,36],[809,39],[1194,40],[1196,41],[1195,42],[1222,43],[1223,44],[1217,45],[1224,46],[1225,47],[1218,48],[1219,49],[1221,50],[1220,51],[1233,52],[1234,53],[1227,54],[1235,55],[1236,56],[1228,57],[1237,58],[1238,59],[1229,60],[862,61],[1230,62],[1232,63],[1231,64],[864,65],[863,66],[866,67],[865,68],[870,69],[869,68],[868,70],[867,68],[872,71],[871,68],[874,72],[873,68],[876,73],[875,68],[880,74],[879,68],[882,75],[881,68],[878,76],[877,68],[884,77],[883,68],[888,78],[887,68],[886,79],[885,68],[894,80],[893,68],[896,81],[895,68],[891,82],[898,83],[897,84],[900,85],[899,84],[902,86],[901,84],[906,87],[905,84],[904,88],[903,84],[908,89],[907,84],[910,90],[909,68],[913,91],[914,91],[915,91],[916,92],[911,91],[912,93],[922,94],[921,68],[920,95],[919,68],[918,96],[917,68],[926,97],[925,68],[924,98],[923,68],[928,99],[927,68],[930,100],[929,68],[932,101],[931,68],[934,102],[933,68],[936,103],[935,68],[937,91],[938,91],[939,104],[892,82],[941,105],[940,68],[943,106],[942,68],[945,107],[944,68],[890,108],[889,68],[947,109],[946,68],[949,110],[948,111],[953,112],[952,68],[951,113],[950,68],[1245,114],[1246,115],[1240,116],[1247,117],[1248,118],[1241,119],[1249,120],[1250,121],[1239,122],[954,39],[1242,123],[1244,124],[1243,125],[1264,126],[1265,127],[1257,128],[1266,129],[1267,130],[1258,131],[1268,132],[1269,133],[1260,134],[1270,135],[1271,136],[1259,137],[1261,138],[1263,139],[1262,140],[1272,141],[1273,142],[1251,119],[1274,143],[1275,144],[1252,17],[1276,145],[1277,146],[1253,147],[1254,148],[1256,149],[1255,150],[1306,151],[1307,152],[1298,153],[1308,154],[1309,155],[1297,156],[1310,157],[1302,158],[1311,159],[1312,160],[1299,161],[1313,162],[1314,163],[1300,131],[1315,164],[1316,165],[1301,166],[1303,167],[1305,168],[1304,169],[1317,170],[1318,171],[1279,172],[1319,173],[1282,174],[1320,175],[1321,176],[1280,177],[1322,178],[1323,179],[1281,180],[1278,181],[1330,182],[1331,183],[1324,184],[1332,185],[1333,186],[1325,137],[1334,187],[1335,188],[1326,189],[955,39],[1327,190],[1329,191],[1328,192],[1283,193],[1285,194],[1284,195],[1342,196],[1343,197],[1336,14],[1344,198],[1345,199],[1337,200],[1346,201],[1347,202],[1338,203],[956,82],[1339,204],[1341,205],[1340,206],[1353,207],[1354,208],[1348,209],[1355,210],[1356,211],[1349,212],[1350,210],[1352,213],[1351,214],[979,215],[1368,216],[1369,217],[1362,218],[1370,219],[1371,220],[1364,221],[1372,222],[1373,223],[1363,224],[1365,219],[1367,225],[1366,226],[1378,227],[1375,228],[1456,229],[1374,230],[1377,231],[1376,232],[1469,233],[1470,234],[1461,235],[1471,236],[1472,237],[1462,238],[1473,239],[1474,240],[1463,241],[1475,242],[1466,243],[1476,244],[1477,245],[1464,246],[1465,247],[1468,248],[1467,249],[1485,250],[1486,251],[1478,252],[1487,253],[1488,254],[1481,255],[1489,256],[1490,257],[1480,258],[1479,82],[1497,259],[1498,260],[1492,261],[1499,262],[1500,263],[1493,264],[1491,82],[1494,262],[1496,265],[1495,266],[1482,253],[1484,267],[1483,268],[1508,269],[1509,270],[1503,271],[1510,272],[1511,273],[1501,274],[1512,275],[1513,276],[1504,277],[1514,278],[1515,279],[1502,14],[957,82],[1505,275],[1507,280],[1506,281],[1524,282],[1525,283],[1516,284],[1526,285],[1527,286],[1517,287],[1528,288],[1529,289],[1518,290],[1530,291],[1531,292],[1520,293],[1532,294],[1533,295],[1519,296],[959,297],[958,39],[1521,291],[1523,298],[1522,299],[1534,300],[1535,301],[1226,302],[1536,303],[1537,304],[1139,305],[1538,306],[1539,307],[1033,308],[1540,309],[1541,310],[1128,311],[1544,312],[1545,313],[1134,314],[1546,315],[1547,316],[1136,317],[1542,318],[1543,319],[1137,320],[1548,321],[1117,322],[1563,323],[1564,324],[1129,325],[1565,326],[1566,327],[1130,328],[1561,329],[1562,330],[1131,331],[1138,332],[1127,333],[1039,334],[1032,335],[1135,336],[1361,337],[1116,338],[1215,339],[1141,336],[1167,340],[1133,341],[1296,342],[1171,336],[1216,336],[971,343],[972,344],[973,345],[1567,346],[974,347],[861,348],[860,349],[806,350],[960,39],[961,39],[962,39],[1174,82],[804,39],[803,82],[967,351],[970,352],[805,39],[1568,353],[976,354],[1569,355],[978,356],[555,357],[975,82],[779,358],[802,359],[772,360],[1124,361],[1123,362],[1122,363],[1126,364],[1125,365],[1121,366],[1028,366],[1029,367],[1360,368],[1359,369],[1358,370],[1357,371],[1092,372],[1093,373],[1208,371],[1207,374],[1206,375],[1212,376],[1211,377],[1210,374],[1205,374],[1147,378],[1209,379],[1081,380],[1064,381],[1066,382],[1062,383],[1079,384],[1047,385],[1082,386],[1067,385],[1068,387],[1083,385],[1077,388],[1069,385],[1071,389],[1072,385],[1073,390],[1070,385],[1046,391],[1074,384],[1075,385],[1084,392],[1076,380],[1085,393],[1078,394],[1080,395],[1045,2],[1145,396],[1146,397],[1213,398],[1214,399],[1025,400],[1144,401],[1036,2],[1291,366],[1023,2],[1024,402],[1142,363],[1027,403],[1065,404],[1057,363],[1087,405],[1088,406],[1097,406],[1096,407],[1099,366],[1098,366],[1115,408],[1114,409],[1100,366],[1101,366],[1102,410],[1103,411],[1104,405],[1105,407],[1107,406],[1106,366],[1095,412],[1090,413],[1094,414],[1089,415],[1109,416],[1108,417],[1113,366],[1110,418],[1111,366],[1091,419],[1035,420],[1034,366],[1160,405],[1153,406],[1164,366],[1163,366],[1151,366],[1166,421],[1165,422],[1158,406],[1159,366],[1157,366],[1148,374],[1156,366],[1155,410],[1152,411],[1154,405],[1143,423],[1161,366],[1162,366],[1149,374],[1150,366],[1112,366],[1132,424],[1289,425],[1295,426],[1294,427],[1293,425],[1287,425],[1286,371],[1292,428],[1290,425],[1288,425],[1026,429],[1038,430],[1037,404],[1061,431],[1060,432],[1058,2],[1059,433],[1086,434],[1051,363],[1054,435],[1052,2],[1055,436],[1049,437],[1050,438],[1056,439],[1053,440],[1063,363],[1041,441],[1043,442],[1044,443],[1040,2],[1042,2],[1019,444],[397,2],[560,2],[562,2],[563,2],[565,445],[564,2],[566,446],[1013,447],[631,2],[776,448],[1015,449],[1021,450],[1020,451],[1011,452],[963,2],[964,453],[965,454],[966,455],[797,2],[794,2],[793,2],[788,456],[799,457],[784,458],[795,459],[787,460],[786,461],[796,2],[791,462],[798,2],[792,463],[785,2],[783,464],[782,465],[1550,465],[781,458],[801,466],[1441,467],[1442,467],[1444,468],[1443,467],[1436,467],[1437,467],[1439,469],[1438,467],[1414,2],[1416,2],[1415,2],[1418,470],[1417,2],[1381,471],[1379,472],[1382,2],[1429,473],[1383,467],[1419,474],[1428,475],[1420,2],[1423,476],[1421,2],[1424,2],[1426,2],[1422,476],[1425,2],[1427,2],[1380,477],[1455,478],[1440,467],[1435,479],[1445,480],[1451,481],[1452,482],[1454,483],[1453,484],[1433,479],[1434,485],[1430,486],[1432,487],[1431,488],[1446,467],[1450,489],[1447,467],[1448,490],[1449,467],[1384,2],[1385,2],[1388,2],[1386,2],[1387,2],[1390,2],[1391,491],[1392,2],[1393,2],[1389,2],[1394,2],[1395,2],[1396,2],[1397,2],[1398,492],[1399,2],[1413,493],[1400,2],[1401,2],[1402,2],[1403,2],[1404,2],[1405,2],[1406,2],[1409,2],[1407,2],[1408,2],[1410,467],[1411,467],[1412,494],[780,2],[634,495],[632,2],[1012,2],[155,496],[156,496],[157,497],[94,498],[158,499],[159,500],[160,501],[92,2],[161,502],[162,503],[163,504],[164,505],[165,506],[166,507],[167,507],[168,508],[169,509],[170,510],[171,511],[95,2],[93,2],[172,512],[173,513],[174,514],[214,515],[175,516],[176,517],[177,516],[178,518],[179,519],[180,520],[181,521],[182,521],[183,521],[184,522],[185,523],[186,524],[187,525],[188,526],[189,527],[190,527],[191,528],[192,2],[193,2],[194,529],[195,530],[196,529],[197,531],[198,532],[199,533],[200,534],[201,535],[202,536],[203,537],[204,538],[205,539],[206,540],[207,541],[208,542],[209,543],[210,544],[211,545],[96,516],[97,2],[98,546],[99,547],[100,2],[101,548],[102,2],[146,549],[147,550],[148,551],[149,551],[150,552],[151,2],[152,499],[153,553],[154,550],[212,554],[213,555],[218,556],[482,363],[219,557],[217,558],[484,559],[483,560],[800,363],[215,561],[480,2],[216,562],[83,2],[85,563],[479,363],[249,363],[778,564],[777,2],[663,565],[771,566],[659,2],[662,567],[672,568],[660,569],[661,570],[1559,571],[635,572],[641,573],[642,574],[640,2],[1551,2],[609,2],[618,575],[617,576],[637,575],[620,577],[622,578],[665,578],[621,579],[1549,2],[629,580],[628,2],[1558,581],[1553,582],[1556,583],[1557,584],[1552,2],[616,585],[610,586],[657,587],[612,588],[614,589],[656,2],[643,588],[613,2],[615,586],[611,2],[1002,2],[1004,590],[1003,2],[633,2],[988,591],[987,592],[986,593],[994,594],[995,595],[992,596],[993,597],[990,598],[991,599],[989,600],[1031,601],[1030,602],[968,2],[84,2],[577,2],[651,603],[653,604],[652,605],[650,606],[649,2],[1458,607],[1457,2],[1459,608],[843,609],[812,610],[822,610],[813,610],[823,610],[814,610],[815,610],[830,610],[829,610],[831,610],[832,610],[824,610],[816,610],[825,610],[817,610],[826,610],[818,610],[820,610],[828,611],[821,610],[827,611],[833,611],[819,610],[834,610],[839,610],[840,610],[835,610],[811,2],[841,2],[837,610],[836,610],[838,610],[842,610],[601,2],[603,612],[602,2],[1022,363],[810,613],[1118,614],[849,615],[848,616],[855,617],[857,618],[853,619],[852,620],[856,616],[850,621],[847,622],[858,623],[859,623],[851,624],[845,2],[846,625],[1120,626],[1119,627],[854,2],[977,363],[505,628],[510,1],[517,629],[500,630],[253,2],[261,631],[401,632],[404,633],[376,2],[389,634],[396,635],[278,2],[378,2],[259,2],[375,636],[421,637],[260,2],[251,638],[403,639],[405,640],[406,641],[477,642],[370,643],[323,644],[383,645],[384,646],[382,647],[381,2],[377,648],[402,649],[262,650],[447,2],[448,651],[289,652],[263,653],[290,652],[326,652],[229,652],[399,654],[398,2],[388,655],[495,2],[238,2],[516,656],[455,657],[456,658],[452,659],[534,2],[353,2],[457,660],[453,661],[539,662],[538,663],[533,2],[304,2],[356,664],[355,2],[532,665],[454,363],[309,666],[316,667],[318,668],[308,2],[313,669],[315,670],[317,671],[312,672],[310,2],[314,673],[535,2],[531,2],[537,674],[536,2],[307,675],[526,676],[529,677],[297,678],[296,679],[295,680],[542,363],[294,681],[283,2],[544,2],[545,363],[546,682],[221,2],[385,683],[386,684],[387,685],[225,2],[390,2],[245,686],[220,2],[469,363],[227,687],[468,688],[467,689],[458,2],[459,2],[466,2],[461,2],[464,690],[460,2],[462,691],[465,692],[463,691],[258,2],[255,2],[256,652],[410,2],[415,693],[416,694],[414,695],[412,696],[413,697],[408,2],[475,660],[250,660],[504,698],[511,699],[515,700],[344,701],[343,2],[338,2],[491,702],[499,703],[371,704],[372,705],[450,706],[360,2],[473,707],[348,363],[365,708],[476,709],[361,2],[364,710],[362,2],[474,711],[471,712],[470,2],[472,2],[368,2],[446,713],[233,714],[346,715],[350,716],[366,717],[369,718],[358,719],[351,720],[498,721],[424,722],[342,723],[230,724],[497,725],[226,726],[417,727],[409,2],[418,728],[435,729],[407,2],[434,730],[91,2],[429,731],[254,2],[449,732],[425,2],[239,2],[241,2],[380,2],[433,733],[257,2],[281,734],[367,735],[287,736],[347,2],[432,2],[411,2],[437,737],[438,738],[379,2],[440,739],[442,740],[441,741],[391,2],[431,724],[444,742],[341,743],[430,744],[436,745],[266,2],[270,2],[269,2],[268,2],[273,2],[267,2],[276,2],[275,2],[272,2],[271,2],[274,2],[277,746],[265,2],[333,747],[332,2],[337,748],[334,749],[336,750],[339,748],[335,749],[246,751],[325,752],[494,753],[492,2],[521,754],[523,755],[487,756],[522,757],[234,758],[231,758],[264,2],[248,759],[247,760],[243,761],[244,762],[252,763],[280,763],[291,763],[327,764],[292,764],[236,765],[235,2],[331,766],[330,767],[329,768],[328,769],[237,770],[478,771],[279,772],[486,773],[451,774],[481,775],[485,776],[374,777],[373,778],[354,779],[340,780],[322,781],[324,782],[321,783],[443,784],[345,2],[509,2],[242,785],[445,786],[493,787],[352,2],[282,788],[359,789],[357,790],[284,791],[419,792],[488,2],[285,793],[420,793],[507,2],[506,2],[508,2],[490,2],[489,2],[422,794],[349,2],[319,795],[240,796],[298,2],[224,797],[286,2],[513,363],[223,2],[525,798],[306,363],[519,660],[305,799],[502,800],[303,798],[228,2],[527,801],[301,363],[302,363],[293,2],[222,2],[300,802],[299,803],[288,804],[363,525],[423,525],[439,2],[427,805],[426,2],[311,675],[232,2],[320,363],[496,686],[503,806],[86,363],[89,807],[90,808],[87,363],[88,2],[400,547],[395,809],[394,2],[393,810],[392,2],[501,811],[512,812],[514,813],[518,814],[520,815],[524,816],[552,817],[528,817],[551,818],[530,819],[540,820],[541,821],[543,822],[547,823],[550,686],[549,2],[548,824],[669,548],[670,825],[844,826],[769,827],[673,2],[674,827],[768,828],[770,829],[597,830],[595,831],[596,832],[584,833],[585,831],[592,834],[583,835],[588,836],[598,2],[589,837],[594,838],[600,839],[599,840],[582,841],[590,842],[591,843],[586,844],[593,830],[587,845],[790,846],[789,2],[1018,847],[1017,848],[1460,849],[996,2],[997,850],[998,2],[999,851],[1048,2],[606,852],[568,853],[569,854],[572,855],[561,856],[571,857],[567,858],[559,2],[573,859],[574,860],[428,861],[581,2],[1000,862],[982,2],[1005,863],[984,864],[1007,865],[1001,866],[1010,867],[1008,868],[985,2],[983,869],[980,363],[1006,870],[1560,871],[981,363],[1009,872],[969,2],[638,2],[1554,2],[1555,873],[630,2],[81,2],[82,2],[13,2],[14,2],[16,2],[15,2],[2,2],[17,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[3,2],[25,2],[26,2],[4,2],[27,2],[31,2],[28,2],[29,2],[30,2],[32,2],[33,2],[34,2],[5,2],[35,2],[36,2],[37,2],[38,2],[6,2],[42,2],[39,2],[40,2],[41,2],[43,2],[7,2],[44,2],[49,2],[50,2],[45,2],[46,2],[47,2],[48,2],[8,2],[54,2],[51,2],[52,2],[53,2],[55,2],[9,2],[56,2],[57,2],[58,2],[60,2],[59,2],[61,2],[62,2],[10,2],[63,2],[64,2],[65,2],[11,2],[66,2],[67,2],[68,2],[69,2],[70,2],[1,2],[71,2],[72,2],[12,2],[76,2],[74,2],[79,2],[78,2],[73,2],[77,2],[75,2],[80,2],[1016,2],[121,874],[134,875],[118,876],[135,877],[144,878],[109,879],[110,880],[108,881],[143,882],[138,883],[142,884],[112,885],[131,886],[111,887],[141,888],[106,889],[107,883],[113,890],[114,2],[120,891],[117,890],[104,892],[145,893],[136,894],[124,895],[123,890],[125,896],[128,897],[122,898],[126,899],[139,882],[115,900],[116,901],[129,902],[105,877],[133,903],[132,890],[119,901],[127,904],[130,905],[137,2],[103,2],[140,906],[1014,907],[557,908],[608,909],[576,910],[558,908],[556,2],[575,911],[607,2],[605,2],[578,912],[604,913],[570,914],[580,2],[579,915],[664,916],[775,917],[658,918],[639,919],[627,920],[623,921],[655,2],[624,577],[647,922],[644,923],[667,924],[666,925],[625,926],[646,927],[619,2],[626,928],[774,929],[668,930],[654,931],[671,932],[648,933],[1572,934],[645,2],[636,2],[754,935],[757,936],[759,937],[766,938],[761,2],[762,2],[760,939],[763,940],[755,2],[756,2],[767,941],[758,942],[764,2],[765,943],[748,944],[752,945],[749,945],[745,944],[753,946],[750,947],[751,945],[746,948],[747,949],[741,950],[682,951],[684,952],[740,2],[683,953],[744,954],[743,955],[742,956],[675,2],[685,951],[686,2],[677,957],[681,958],[676,2],[678,959],[679,960],[680,2],[687,961],[688,961],[689,961],[690,961],[691,961],[692,961],[693,961],[694,961],[695,961],[696,961],[697,961],[698,961],[699,961],[700,961],[702,961],[701,961],[703,961],[704,961],[705,961],[706,961],[707,961],[739,962],[708,961],[709,961],[710,961],[711,961],[712,961],[713,961],[714,961],[715,961],[716,961],[717,961],[718,961],[719,961],[720,961],[722,961],[721,961],[723,961],[724,961],[725,961],[726,961],[727,961],[728,961],[729,961],[730,961],[731,961],[732,961],[733,961],[734,961],[735,961],[738,961],[736,961],[737,961]],"affectedFilesPendingEmit":[1571,773,1178,1179,1140,1180,1181,1168,1182,1183,1169,1184,1185,1170,1186,1187,1173,1188,1189,1172,808,807,1175,1177,1176,1197,1198,1190,1199,1200,1191,1201,1202,1192,1203,1204,1193,809,1194,1196,1195,1222,1223,1217,1224,1225,1218,1219,1221,1220,1233,1234,1227,1235,1236,1228,1237,1238,1229,862,1230,1232,1231,864,863,866,865,870,869,868,867,872,871,874,873,876,875,880,879,882,881,878,877,884,883,888,887,886,885,894,893,896,895,891,898,897,900,899,902,901,906,905,904,903,908,907,910,909,913,914,915,916,911,912,922,921,920,919,918,917,926,925,924,923,928,927,930,929,932,931,934,933,936,935,937,938,939,892,941,940,943,942,945,944,890,889,947,946,949,948,953,952,951,950,1245,1246,1240,1247,1248,1241,1249,1250,1239,954,1242,1244,1243,1264,1265,1257,1266,1267,1258,1268,1269,1260,1270,1271,1259,1261,1263,1262,1272,1273,1251,1274,1275,1252,1276,1277,1253,1254,1256,1255,1306,1307,1298,1308,1309,1297,1310,1302,1311,1312,1299,1313,1314,1300,1315,1316,1301,1303,1305,1304,1317,1318,1279,1319,1282,1320,1321,1280,1322,1323,1281,1278,1330,1331,1324,1332,1333,1325,1334,1335,1326,955,1327,1329,1328,1283,1285,1284,1342,1343,1336,1344,1345,1337,1346,1347,1338,956,1339,1341,1340,1353,1354,1348,1355,1356,1349,1350,1352,1351,979,1368,1369,1362,1370,1371,1364,1372,1373,1363,1365,1367,1366,1378,1375,1456,1374,1377,1376,1469,1470,1461,1471,1472,1462,1473,1474,1463,1475,1466,1476,1477,1464,1465,1468,1467,1485,1486,1478,1487,1488,1481,1489,1490,1480,1479,1497,1498,1492,1499,1500,1493,1491,1494,1496,1495,1482,1484,1483,1508,1509,1503,1510,1511,1501,1512,1513,1504,1514,1515,1502,957,1505,1507,1506,1524,1525,1516,1526,1527,1517,1528,1529,1518,1530,1531,1520,1532,1533,1519,959,958,1521,1523,1522,1534,1535,1226,1536,1537,1139,1538,1539,1033,1540,1541,1128,1544,1545,1134,1546,1547,1136,1542,1543,1137,1548,1117,1563,1564,1129,1565,1566,1130,1561,1562,1131,1138,1127,1039,1032,1135,1361,1116,1215,1141,1167,1133,1296,1171,1216,971,972,973,1567,974,861,860,806,960,961,962,1174,804,803,967,970,805,1568,976,1569,978,975,779,802],"version":"5.9.3"} \ No newline at end of file +{"fileNames":["../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2023.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.object.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.regexp.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2024.string.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.array.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.collection.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.intl.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.disposable.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.promise.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.decorators.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.iterator.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.float16.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.error.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/global.d.ts","../node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/css.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/macro.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/style.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/global.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/styled-jsx/types/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/get-page-files.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/compatibility/iterators.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/globals.typedarray.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/buffer.buffer.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/globals.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/abortcontroller.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/blob.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/console.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/crypto.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/domexception.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/encoding.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/events.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/utility.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/header.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/readable.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/fetch.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/formdata.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/connector.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/client-stats.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/errors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/dispatcher.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/global-dispatcher.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/global-origin.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/pool-stats.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/handlers.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/balanced-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/round-robin-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/h2c-client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-interceptor.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-call-history.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-client.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-pool.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/snapshot-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/mock-errors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/socks5-proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/env-http-proxy-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/retry-handler.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/retry-agent.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/api.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cache-interceptor.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/interceptors.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/util.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cookies.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/patch.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/websocket.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/eventsource.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/diagnostics-channel.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/content-type.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/cache.d.ts","../node_modules/.pnpm/undici-types@7.24.6/node_modules/undici-types/index.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/fetch.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/importmeta.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/messaging.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/navigator.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/performance.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/storage.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/streams.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/timers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/web-globals/url.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/assert.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/assert/strict.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/async_hooks.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/buffer.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/child_process.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/cluster.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/console.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/constants.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/crypto.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dgram.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/diagnostics_channel.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dns.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/dns/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/domain.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/events.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/fs.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/fs/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/http.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/http2.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/https.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector.generated.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/inspector/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/module.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/net.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/os.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path/posix.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/path/win32.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/perf_hooks.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/process.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/punycode.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/querystring.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/quic.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/readline.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/readline/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/repl.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/sea.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/sqlite.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/consumers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/stream/web.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/string_decoder.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/test.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/test/reporters.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/timers.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/timers/promises.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/tls.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/trace_events.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/tty.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/url.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/util.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/util/types.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/v8.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/vm.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/wasi.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/worker_threads.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/zlib.d.ts","../node_modules/.pnpm/@types+node@25.8.0/node_modules/@types/node/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/canary.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/experimental.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/index.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/canary.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/experimental.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/fallback.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/webpack/webpack.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/entry-constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/bundler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/load-custom-routes.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/body-streams.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/search-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/vary-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-kind.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/route-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/app-router-headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cache-control.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/app-router-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/use-cache-wrapper.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/constants.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render-result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/response-cache/index.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/userspace/pages/pages-dev-overlay-setup.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/static-paths/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/adapter/setup-node-env.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/instrumentation/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/setup-exception-listeners.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/experimental/ppr.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/page-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/require-hook.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-polyfill-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-baseline.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-file.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-exit.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/console-dim.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/random.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/date.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/node-environment.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/page-extensions-type.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/i18n-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/next-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/request.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/deep-readonly.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/mitt.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/with-router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/route-loader.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/page-loader.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/bloom-filter.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/readonly-url-search-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/flight-data-helpers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache-key.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/scheduler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache-map.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/vary-path.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/ppr-navigations.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/segment-cache/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/pages.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/render.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/suffix.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/rsc.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/next-data.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/builtin-request-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/pages/builtin/_error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/load-default-error-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/after-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-life.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/lazy-result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/create-error-handler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/action-revalidation-kind.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/work-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/http.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/hooks-server-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-status-code.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/cache-signal.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-samples.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/implicit-tags.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/staged-rendering.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-route.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/get-supported-browsers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/rendering-mode.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/cpu-profile.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/routes/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/export/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/coalesced-function.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/trace.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/trace/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/load-jsconfig.d.ts","../node_modules/.pnpm/@next+env@16.2.6/node_modules/@next/env/dist/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/use-cache-tracker-utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/plugins/telemetry-plugin/telemetry-plugin.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/telemetry/storage.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/build-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/generated-native.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/define-env.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/swc/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/parse-version-info.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/shared/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/dev-indicator-server-state.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/cache-indicator.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/parse-stack.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/server/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/shared/stack-frame.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/utils/get-error-by-type.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/container/runtime-error/render-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/dev-overlay/shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/debug-channel.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/node.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/async-callback-set.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","../node_modules/.pnpm/sharp@0.34.5/node_modules/sharp/lib/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/image-optimizer.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/lru-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/static-paths-worker.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/dev/next-dev-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/next.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/render-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/router-utils/router-server-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/route-module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/load-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/adapter.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/lib/app-dir-module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/app-render.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/error-boundary.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/layout-router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/render-from-template-context.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-page.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/client-segment.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/types/icons.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/metadata/metadata.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/lib/framework/boundary-components.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/rsc/taint.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/instant-validation/instant-validation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/next-devtools/userspace/app/segment-explorer-node.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/app-render/entry-base.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/templates/app-page.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/jsx-dev-runtime.d.ts","../node_modules/.pnpm/@types+react@19.2.14/node_modules/@types/react/compiler-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/client.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/static.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/route-modules/app-page/module.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/fallback-params.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/url-pattern.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/after/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/connection.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/exports/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request-meta.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/cli/next-test.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/size-limit.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/config-shared.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/base-http/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/api-utils/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/build/adapter/build-complete.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/utils.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_app.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/app.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/use-cache/cache-tag.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/cache.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_document.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/document.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/dynamic.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dynamic.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/pages/_error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/catch-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/api/error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/head.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/head.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/cookies.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/server/request/draft-mode.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/headers.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/get-img-props.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/image-component.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/shared/lib/image-external.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/link.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/link.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unrecognized-action-error.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/redirect.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/not-found.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/forbidden.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unauthorized.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unstable-rethrow.server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/unstable-rethrow.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.react-server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/components/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/navigation.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/router.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/client/script.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/script.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/dist/compiled/@vercel/og/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/server.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/global.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types/compiled.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/types.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/index.d.ts","../node_modules/.pnpm/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next/image-types/global.d.ts","./.next/types/routes.d.ts","./next-env.d.ts","./storybook.shims.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/hmrPayload.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/chunks/moduleRunnerTransport.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/customEvent.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/logging-C6h4g8dA.d.mts","../node_modules/.pnpm/@oxc-project+types@0.130.0/node_modules/@oxc-project/types/types.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/binding-zH1vcmbM.d.mts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/composable-filters.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/filter-vite-plugins.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/simple-filters.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/filter/index.d.ts","../node_modules/.pnpm/@rolldown+pluginutils@1.0.0/node_modules/@rolldown/pluginutils/dist/index.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/define-config-CeKzMIcs.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/index.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/parse-ast-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/rollupTypeCompat.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/constructors-DAnSz-_I.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/plugins-index.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/shared/transform-DgZ3paSD.d.mts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/utils-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/hot.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/module-runner.d.ts","../node_modules/.pnpm/esbuild@0.27.7/node_modules/esbuild/lib/main.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/esbuildOptions.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/metadata.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/terserOptions.d.ts","../node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/previous-map.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/input.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/css-syntax-error.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/declaration.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/root.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/warning.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/lazy-result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/no-work-result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/processor.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/result.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/document.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/rule.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/node.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/comment.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/container.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/at-rule.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/list.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/postcss.d.ts","../node_modules/.pnpm/postcss@8.5.14/node_modules/postcss/lib/postcss.d.mts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/ast.d.ts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/targets.d.ts","../node_modules/.pnpm/lightningcss@1.32.0/node_modules/lightningcss/node/index.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/lightningcssOptions.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/internal/cssPreprocessorOptions.d.ts","../node_modules/.pnpm/rolldown@1.0.1/node_modules/rolldown/dist/filter-index.d.mts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/types/importGlob.d.ts","../node_modules/.pnpm/vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0/node_modules/vite/dist/node/index.d.ts","../node_modules/.pnpm/@vitest+pretty-format@4.1.6/node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/display.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/types.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/helpers.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/timers.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/diff.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/tasks.d-DEYaIMIu.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/traces.d.D2T_R8rx.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/environment.d-DOJxxZV9.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/rawSnapshot.d-D_X3-62x.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/config.d.A1h_Y6Jt.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/environment.d.CrsxCzP1.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/rpc.d.B_8sPU0w.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/worker.d.ZpHpO4yb.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/browser.d.BcoexmFG.d.ts","../node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/optional-types.d.ts","../node_modules/.pnpm/@vitest+spy@4.1.6/node_modules/@vitest/spy/dist/index.d.ts","../node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.d.ts","../node_modules/.pnpm/@standard-schema+spec@1.1.0/node_modules/@standard-schema/spec/dist/index.d.ts","../node_modules/.pnpm/@types+deep-eql@4.0.2/node_modules/@types/deep-eql/index.d.ts","../node_modules/.pnpm/assertion-error@2.0.1/node_modules/assertion-error/index.d.ts","../node_modules/.pnpm/@types+chai@5.2.3/node_modules/@types/chai/index.d.ts","../node_modules/.pnpm/@vitest+expect@4.1.6/node_modules/@vitest/expect/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/optional-types.d.ts","../node_modules/.pnpm/@vitest+runner@4.1.6/node_modules/@vitest/runner/dist/utils.d.ts","../node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/benchmark.d.DAaHLpsq.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/types.d-BjI5eAwu.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/index.d-B41z0AuW.d.ts","../node_modules/.pnpm/@vitest+mocker@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitest/mocker/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/source-map.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/global.d.DVsSRdQ5.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/optional-runtime-types.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/suite.d.udJtyAgw.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/evaluatedModules.d.BxJ5omdx.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/runners.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","../node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/index.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/coverage.d.BZtK59WP.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/serialize.d.ts","../node_modules/.pnpm/@vitest+utils@4.1.6/node_modules/@vitest/utils/dist/error.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/browser.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/aria-role.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/jest-dom.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/matchers.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/context.d.ts","../node_modules/.pnpm/@vitest+browser-playwright@4.1.6_playwright@1.60.0_vite@8.0.13_@types+node@25.8.0_esbui_d3b4c98b933477f7e7f49b0aad7a0b49/node_modules/@vitest/browser-playwright/context.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/browser/context.d.ts","../node_modules/.pnpm/@vitest+snapshot@4.1.6/node_modules/@vitest/snapshot/dist/manager.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/reporters.d.CtLUhkkA.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/chunks/plugin.d.C0upjs_f.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/coverage.d.ts","../node_modules/.pnpm/obug@2.1.1/node_modules/obug/dist/core.d.ts","../node_modules/.pnpm/obug@2.1.1/node_modules/obug/dist/node.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/node.d.ts","../node_modules/.pnpm/@vitest+browser@4.1.6_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0__vitest@4.1.6/node_modules/@vitest/browser/dist/index.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/protocol.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/structs.d.ts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/standard-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ar.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/az.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/be.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/bg.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ca.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/cs.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/da.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/de.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/el.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/en.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/eo.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/es.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fa.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fi.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/fr-CA.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/he.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hu.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/hy.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/id.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/is.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/it.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ja.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ka.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/kh.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/km.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ko.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/lt.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/mk.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ms.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/nl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/no.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ota.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ps.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/pl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/pt.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ro.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ru.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/sl.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/sv.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ta.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/th.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/tr.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ua.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/uk.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/ur.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/uz.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/vi.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/zh-CN.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/zh-TW.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/yo.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/locales/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-generator.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/checks.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/compat.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/from-json-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/coerce.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/index.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/typeAliases.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/util.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/ZodError.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/locales/en.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/errors.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/parseUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/enumUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/errorUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/helpers/partialUtil.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/standard-schema.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/types.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/external.d.cts","../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v3/index.d.cts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/types/types.d.ts","../node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/index.d.ts","../node_modules/.pnpm/playwright@1.60.0/node_modules/playwright/index.d.ts","../node_modules/.pnpm/@vitest+browser-playwright@4.1.6_playwright@1.60.0_vite@8.0.13_@types+node@25.8.0_esbui_d3b4c98b933477f7e7f49b0aad7a0b49/node_modules/@vitest/browser-playwright/dist/index.d.ts","./vitest.shims.d.ts","./next.config.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/dist/config.d.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/config.d.ts","../node_modules/.pnpm/@storybook+addon-vitest@10.4.0_@vitest+browser-playwright@4.1.6_@vitest+browser@4.1.6_@_92cdec18fed2ca7a5a8c0a4dff19a221/node_modules/@storybook/addon-vitest/dist/vitest-plugin/index.d.ts","../node_modules/.pnpm/@vitejs+plugin-react@6.0.2_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitejs/plugin-react/types/optionalTypes.d.ts","../node_modules/.pnpm/@vitejs+plugin-react@6.0.2_vite@8.0.13_@types+node@25.8.0_esbuild@0.27.7_jiti@2.7.0_/node_modules/@vitejs/plugin-react/dist/index.d.ts","./vitest.config.ts","../node_modules/.pnpm/@types+aria-query@5.0.4/node_modules/@types/aria-query/index.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/matchers.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/jest.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/index.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/matches.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/wait-for.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/query-helpers.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/queries.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/get-queries-for-element.d.ts","../node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/build/types.d.ts","../node_modules/.pnpm/pretty-format@27.5.1/node_modules/pretty-format/build/index.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/screen.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/wait-for-element-to-be-removed.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/get-node-text.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/events.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/pretty-dom.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/role-helpers.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/config.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/suggestions.d.ts","../node_modules/.pnpm/@testing-library+dom@10.4.1/node_modules/@testing-library/dom/types/index.d.ts","../node_modules/.pnpm/@types+react-dom@19.2.3_@types+react@19.2.14/node_modules/@types/react-dom/test-utils/index.d.ts","../node_modules/.pnpm/@testing-library+react@16.3.2_@testing-library+dom@10.4.1_@types+react-dom@19.2.3_@type_01f2c7bab43719bd0469ec1c0554baf5/node_modules/@testing-library/react/types/index.d.ts","./vitest.setup.ts","./src/lib/types.ts","./src/lib/storybook-fixtures.ts","./src/lib/view-helpers.ts","./src/lib/dashboard-view.ts","./src/app/(home)/_components/shared.ts","./src/app/(home)/_components/shared.test.ts","./src/app/admin/health/_components/helpers.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/adapters.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/types.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/compact/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/flattened/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/general/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/general/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/compact/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/flattened/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/general/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/verify.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/decrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/produce.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/compact/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwe/flattened/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/compact/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/flattened/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jws/general/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/sign.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/encrypt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwk/thumbprint.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwk/embedded.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwks/local.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwks/remote.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/jwt/unsecured.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/export.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/import.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/decode_protected_header.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/decode_jwt.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/errors.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/generate_key_pair.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/key/generate_secret.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/base64url.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/util/runtime.d.ts","../node_modules/.pnpm/jose@4.15.9/node_modules/jose/dist/types/index.d.ts","../node_modules/.pnpm/openid-client@5.7.1/node_modules/openid-client/types/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/oauth-types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/oauth.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/email.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/lib/cookie.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/credentials.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/jwt/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/jwt/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/utils/logger.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/core/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/next/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/index.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/github.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/providers/google.d.ts","./src/lib/auth.ts","./src/lib/api.ts","./src/app/admin/sources/_components/helpers.ts","./src/app/api/auth/[...nextauth]/route.ts","./src/app/api/auth/[...nextauth]/route.test.ts","./src/app/api/content-skills/route.ts","./src/app/api/content-skills/route.test.ts","./src/app/api/entities/route.ts","./src/app/api/entities/route.test.ts","./src/app/api/entities/[id]/route.ts","./src/app/api/entities/[id]/route.test.ts","./src/app/api/entity-candidates/[id]/route.ts","./src/app/api/entity-candidates/[id]/route.test.ts","./src/app/api/feedback/route.ts","./src/app/api/feedback/route.test.ts","./src/app/api/invitations/[token]/accept/route.ts","./src/app/api/invitations/[token]/accept/route.test.ts","./src/app/api/messages/threads/route.ts","./src/app/api/messages/threads/route.test.ts","./src/app/api/messages/threads/[threadId]/messages/route.ts","./src/app/api/messages/threads/[threadId]/messages/route.test.ts","./src/app/api/messages/threads/[threadId]/read/route.ts","./src/app/api/messages/threads/[threadId]/read/route.test.ts","./src/app/api/notifications/route.ts","./src/app/api/notifications/route.test.ts","./src/app/api/profile/route.ts","./src/app/api/profile/route.test.ts","./src/app/api/profile/avatar/route.ts","./src/app/api/profile/avatar/route.test.ts","./src/app/api/projects/route.ts","./src/app/api/projects/route.test.ts","./src/app/api/projects/[id]/draft-action-helpers.ts","./src/app/api/projects/[id]/trend-action-helpers.ts","./src/app/api/projects/[id]/authority-settings/route.ts","./src/app/api/projects/[id]/authority-settings/route.test.ts","./src/app/api/projects/[id]/bluesky-credentials/route.ts","./src/app/api/projects/[id]/bluesky-credentials/route.test.ts","./src/app/api/projects/[id]/draft-items/[itemId]/route.ts","./src/app/api/projects/[id]/draft-items/[itemId]/route.test.ts","./src/app/api/projects/[id]/draft-original-pieces/[pieceId]/route.ts","./src/app/api/projects/[id]/draft-original-pieces/[pieceId]/route.test.ts","./src/app/api/projects/[id]/draft-sections/[sectionId]/route.ts","./src/app/api/projects/[id]/draft-sections/[sectionId]/route.test.ts","./src/app/api/projects/[id]/drafts/[draftId]/route.ts","./src/app/api/projects/[id]/drafts/[draftId]/route.test.ts","./src/app/api/projects/[id]/drafts/[draftId]/regenerate-section/route.ts","./src/app/api/projects/[id]/drafts/[draftId]/regenerate-section/route.test.ts","./src/app/api/projects/[id]/drafts/generate/route.ts","./src/app/api/projects/[id]/drafts/generate/route.test.ts","./src/app/api/projects/[id]/entity-candidate-bulk/route.ts","./src/app/api/projects/[id]/entity-candidate-bulk/route.test.ts","./src/app/api/projects/[id]/ideas/generate/route.ts","./src/app/api/projects/[id]/ideas/route.test.ts","./src/app/api/projects/[id]/ideas/[ideaId]/accept/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/dismiss/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/mark-written/route.ts","./src/app/api/projects/[id]/ideas/[ideaId]/route.test.ts","./src/app/api/projects/[id]/intake/route.ts","./src/app/api/projects/[id]/intake/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/route.ts","./src/app/api/projects/[id]/intake-allowlist/route.test.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.ts","./src/app/api/projects/[id]/intake-allowlist/[allowlistId]/route.test.ts","./src/app/api/projects/[id]/invitations/route.ts","./src/app/api/projects/[id]/invitations/route.test.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.ts","./src/app/api/projects/[id]/invitations/[invitationId]/revoke/route.test.ts","./src/app/api/projects/[id]/linkedin-oauth/start/route.ts","./src/app/api/projects/[id]/linkedin-oauth/start/route.test.ts","./src/app/api/projects/[id]/linkedin-source-configs/route.ts","./src/app/api/projects/[id]/linkedin-source-configs/route.test.ts","./src/app/api/projects/[id]/mastodon-credentials/route.ts","./src/app/api/projects/[id]/mastodon-credentials/route.test.ts","./src/app/api/projects/[id]/members/[membershipId]/route.ts","./src/app/api/projects/[id]/members/[membershipId]/route.test.ts","./src/app/api/projects/[id]/rotate-intake-token/route.ts","./src/app/api/projects/[id]/rotate-intake-token/route.test.ts","./src/app/api/projects/[id]/themes/[themeId]/accept/route.ts","./src/app/api/projects/[id]/themes/[themeId]/dismiss/route.ts","./src/app/api/projects/[id]/themes/[themeId]/route.test.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.ts","./src/app/api/projects/[id]/verify-bluesky-credentials/route.test.ts","./src/app/api/projects/[id]/verify-linkedin-credentials/route.ts","./src/app/api/projects/[id]/verify-linkedin-credentials/route.test.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.ts","./src/app/api/projects/[id]/verify-mastodon-credentials/route.test.ts","./src/app/api/review/[id]/route.ts","./src/app/api/review/[id]/route.test.ts","./src/app/api/skills/[skillName]/route.ts","./src/app/api/skills/[skillName]/route.test.ts","./src/app/api/source-configs/route.ts","./src/app/api/source-configs/route.test.ts","./src/app/api/source-configs/[id]/route.ts","./src/app/api/source-configs/[id]/route.test.ts","./src/app/content/[id]/_components/helpers.ts","./src/app/entities/candidates/_components/shared.ts","./src/app/ideas/_components/shared.ts","./src/app/themes/_components/shared.ts","./src/app/trends/_components/shared.ts","./src/app/trends/_components/shared.test.ts","./src/lib/messages.ts","./src/lib/notifications.ts","./src/lib/profile.ts","../node_modules/.pnpm/@tanstack+query-core@5.100.10/node_modules/@tanstack/query-core/build/modern/_tsup-dts-rollup.d.ts","../node_modules/.pnpm/@tanstack+query-core@5.100.10/node_modules/@tanstack/query-core/build/modern/index.d.ts","../node_modules/.pnpm/@tanstack+react-query@5.100.10_react@19.2.6/node_modules/@tanstack/react-query/build/modern/_tsup-dts-rollup.d.ts","../node_modules/.pnpm/@tanstack+react-query@5.100.10_react@19.2.6/node_modules/@tanstack/react-query/build/modern/index.d.ts","./src/lib/useRole.ts","../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/clsx.d.mts","../node_modules/.pnpm/tailwind-merge@3.6.0/node_modules/tailwind-merge/dist/types.d.ts","./src/lib/utils.ts","./src/lib/__tests__/api.test.ts","./src/lib/__tests__/auth.test.ts","./src/lib/__tests__/dashboard-view.test.ts","./src/lib/__tests__/view-helpers.test.ts","./test-support/server-only.ts","./src/providers/QueryProvider/index.tsx","../node_modules/.pnpm/next-themes@0.4.6_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/next-themes/dist/index.d.ts","./src/providers/ThemeProvider/index.tsx","./src/app/layout.tsx","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/router/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/theming/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/channels/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/preview-api/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/core-events/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/oxc-parser/index.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/namedTypes.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/kinds.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/builders.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/types.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/path.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/scope.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/node-path.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/path-visitor.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/gen/visitor.d.ts","../node_modules/.pnpm/ast-types@0.16.1/node_modules/ast-types/lib/main.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/options.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/parser.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/lib/printer.d.ts","../node_modules/.pnpm/recast@0.23.11/node_modules/recast/main.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/babel/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/csf-tools/index.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/environment.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/url.d.ts","../node_modules/.pnpm/@webcontainer+env@1.1.1/node_modules/@webcontainer/env/dist/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/common/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/telemetry/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/core-server/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/node-logger/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/types/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/csf/index.d.ts","../node_modules/.pnpm/@storybook+react@10.4.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react@19.2_9637a9e41fe50712e404e9d6bfecb53c/node_modules/@storybook/react/dist/index.d.ts","../node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","../node_modules/.pnpm/@rollup+pluginutils@5.3.0/node_modules/@rollup/pluginutils/types/index.d.ts","../node_modules/.pnpm/vite-plugin-storybook-nextjs@3.2.4_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_reac_a72e796d7340f857c3aad09f1cfbfdcd/node_modules/vite-plugin-storybook-nextjs/dist/index.d.ts","../node_modules/.pnpm/@storybook+builder-vite@10.4.0_esbuild@0.27.7_storybook@10.4.0_@emnapi+core@1.10.0_@emn_cdc6719e2a41f2fcaaeb8fc79968633c/node_modules/@storybook/builder-vite/dist/index.d.ts","../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/typescript.d.ts","../node_modules/.pnpm/react-docgen-typescript@2.4.0_typescript@5.9.3/node_modules/react-docgen-typescript/lib/parser.d.ts","../node_modules/.pnpm/react-docgen-typescript@2.4.0_typescript@5.9.3/node_modules/react-docgen-typescript/lib/index.d.ts","../node_modules/.pnpm/@joshwooding+vite-plugin-react-docgen-typescript@0.7.0_typescript@5.9.3_vite@8.0.13_@ty_12cc9b528b9fd9198b84fe3502042fbc/node_modules/@joshwooding/vite-plugin-react-docgen-typescript/dist/index.d.ts","../node_modules/.pnpm/@storybook+react-vite@10.4.0_@types+react-dom@19.2.3_@types+react@19.2.14__@types+react_8acbcb421469172b896e2c2ce69779dc/node_modules/@storybook/react-vite/dist/index.d.ts","../node_modules/.pnpm/@storybook+nextjs-vite@10.4.0_@babel+core@7.29.0_@types+react-dom@19.2.3_@types+react@1_0959b0caa1b7fc3f2aed0f76ce58a98c/node_modules/@storybook/nextjs-vite/dist/index.d.ts","../node_modules/.pnpm/lucide-react@1.16.0_react@19.2.6/node_modules/lucide-react/dist/lucide-react.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/reason-parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/reasons.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/createBaseUIEventDetails.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/types/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/types.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/button/Button.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/button/index.d.ts","../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/types.d.ts","../node_modules/.pnpm/class-variance-authority@0.7.1/node_modules/class-variance-authority/dist/index.d.ts","./src/components/ui/button.tsx","./src/components/elements/ThemeToggle/index.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/merge-props/mergeProps.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/merge-props/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/getStateAttributesProps.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/use-render/useRender.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/use-render/index.d.ts","./src/components/ui/badge.tsx","../node_modules/.pnpm/@floating-ui+utils@0.2.11/node_modules/@floating-ui/utils/dist/floating-ui.utils.d.mts","../node_modules/.pnpm/@floating-ui+core@1.7.5/node_modules/@floating-ui/core/dist/floating-ui.core.d.mts","../node_modules/.pnpm/@floating-ui+utils@0.2.11/node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.d.mts","../node_modules/.pnpm/@floating-ui+dom@1.7.6/node_modules/@floating-ui/dom/dist/floating-ui.dom.d.mts","../node_modules/.pnpm/@floating-ui+react-dom@2.1.8_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.d.mts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/utils/constants.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useInteractions.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingTreeStore.d.ts","../node_modules/.pnpm/reselect@5.1.1/node_modules/reselect/dist/reselect.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/createSelector.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/createSelectorMemoized.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/fastHooks.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/Store.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/useStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/ReactStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/StoreInspector.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/store/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/useTransitionStatus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/popupTriggerMap.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/store.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/popupStoreUtils.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/popups/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingRootStore.d.ts","../node_modules/.pnpm/@base-ui+utils@0.2.8_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/utils/esm/useEnhancedClickHandler.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingFocusManager.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/useRenderElement.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useClientPoint.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useDismiss.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFocus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverShared.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHover.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverFloatingInteraction.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useHoverReferenceInteraction.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useListNavigation.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useRole.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useTypeahead.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFloatingRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/safePolygon.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingTree.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/types.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/components/FloatingDelayGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useClick.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useFloating.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/hooks/useSyncedFloatingRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/floating-ui-react/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/utils/useAnchorPositioning.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/arrow/MenuArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/backdrop/MenuBackdrop.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/store/MenuStore.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/root/MenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menubar/MenubarContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/context-menu/root/ContextMenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/context-menu/root/ContextMenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/store/MenuHandle.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/root/MenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/checkbox-item/MenuCheckboxItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/checkbox-item-indicator/MenuCheckboxItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/group/MenuGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/group-label/MenuGroupLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/item/MenuItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/link-item/MenuLinkItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/popup/MenuPopup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/portal/MenuPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/positioner/MenuPositioner.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-group/MenuRadioGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-item/MenuRadioItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/radio-item-indicator/MenuRadioItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-root/MenuSubmenuRootContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-root/MenuSubmenuRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/trigger/MenuTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/viewport/MenuViewport.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/separator/Separator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/submenu-trigger/MenuSubmenuTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/menu/index.d.ts","./src/components/ui/dropdown-menu.tsx","./src/components/layout/NotificationMenu/index.tsx","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/client/_utils.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/react/types.d.ts","../node_modules/.pnpm/next-auth@4.24.14_next@16.2.6_@babel+core@7.29.0_react-dom@19.2.6_react@19.2.6__react@1_419c18c166c97e6709473f6e41924ffc/node_modules/next-auth/react/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/root/AvatarRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/image/useImageLoadingStatus.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/image/AvatarImage.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/fallback/AvatarFallback.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/avatar/index.d.ts","./src/components/ui/avatar.tsx","./src/components/elements/UserAvatar/index.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.tsx","./src/components/layout/UserMenu/index.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/separator/index.d.ts","./src/components/ui/separator.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.tsx","./src/components/ui/card.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.tsx","./src/components/layout/AppShell/index.tsx","./src/components/ui/alert.tsx","./src/components/elements/StatusBadge/index.tsx","./src/app/(home)/_components/ContentFeed/index.tsx","./src/components/ui/label.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/resolveValueLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/root/SelectRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/form-context/FormContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/form/Form.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/form/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/root/FieldRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/label/SelectLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/trigger/SelectTrigger.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/value/SelectValue.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/icon/SelectIcon.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/portal/SelectPortal.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/backdrop/SelectBackdrop.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/positioner/SelectPositioner.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/popup/SelectPopup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/list/SelectList.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item/SelectItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item-indicator/SelectItemIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/item-text/SelectItemText.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/arrow/SelectArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/scroll-down-arrow/SelectScrollDownArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/scroll-up-arrow/SelectScrollUpArrow.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/group/SelectGroup.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/group-label/SelectGroupLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/select/index.d.ts","./src/components/ui/select.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.tsx","./src/app/(home)/_components/DashboardOverview/index.tsx","./src/app/(home)/_components/DashboardSidebar/index.tsx","./src/components/ui/table.tsx","./src/app/(home)/_components/ReviewQueueTable/index.tsx","./src/app/(home)/_components/HomePageContent/index.tsx","./src/lib/storybook-docs.tsx","./src/app/(home)/page.stories.tsx","./src/app/(home)/page.tsx","./src/app/(home)/page.test.tsx","./src/app/(home)/_components/ContentFeed/index.stories.tsx","./src/app/(home)/_components/ContentFeed/index.test.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.stories.tsx","./src/app/(home)/_components/DashboardFilterToolbar/index.test.tsx","./src/app/(home)/_components/DashboardOverview/index.stories.tsx","./src/app/(home)/_components/DashboardOverview/index.test.tsx","./src/app/(home)/_components/DashboardSidebar/index.stories.tsx","./src/app/(home)/_components/DashboardSidebar/index.test.tsx","./src/app/(home)/_components/HomePageContent/index.stories.tsx","./src/app/(home)/_components/HomePageContent/index.test.tsx","./src/app/(home)/_components/ReviewQueueTable/index.stories.tsx","./src/app/(home)/_components/ReviewQueueTable/index.test.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.tsx","./src/app/admin/health/page.stories.tsx","./src/app/admin/health/page.tsx","./src/app/admin/health/page.test.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.stories.tsx","./src/app/admin/health/_components/SourceDiversityPanel/index.test.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.stories.tsx","./src/app/admin/health/_components/SourceHealthPanel/index.test.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.stories.tsx","./src/app/admin/health/_components/TopicCentroidPanel/index.test.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.stories.tsx","./src/app/admin/health/_components/TrendTaskRunsPanel/index.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/label/FieldLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/error/FieldError.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/description/FieldDescription.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/control/FieldControl.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/validity/FieldValidity.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/item/FieldItem.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/field/index.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/input/Input.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/input/index.d.ts","./src/components/ui/input.tsx","./src/components/ui/textarea.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.tsx","./src/app/admin/projects/new/page.stories.tsx","./src/app/admin/projects/new/page.tsx","./src/app/admin/projects/new/page.test.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.stories.tsx","./src/app/admin/projects/new/_components/NewProjectFormCard/index.test.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.stories.tsx","./src/app/admin/projects/new/_components/ProjectFlashNotice/index.test.tsx","./src/components/elements/CopyButton/index.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.tsx","./src/app/admin/sources/_components/SourceConfigList/index.tsx","./src/app/admin/sources/page.stories.tsx","./src/app/admin/sources/page.tsx","./src/app/admin/sources/page.test.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.stories.tsx","./src/app/admin/sources/_components/NewsletterIntakePanel/index.test.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.stories.tsx","./src/app/admin/sources/_components/ProviderSetupPanel/index.test.tsx","./src/app/admin/sources/_components/SourceConfigList/index.stories.tsx","./src/app/admin/sources/_components/SourceConfigList/index.test.tsx","./src/app/content/[id]/_components/SkillActionBar/index.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.tsx","./src/app/content/[id]/page.stories.tsx","./src/app/content/[id]/page.tsx","./src/app/content/[id]/page.test.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.stories.tsx","./src/app/content/[id]/_components/ContentDetailMainColumn/index.test.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.stories.tsx","./src/app/content/[id]/_components/ContentDetailSidebar/index.test.tsx","./src/app/content/[id]/_components/SkillActionBar/index.stories.tsx","./src/app/content/[id]/_components/SkillActionBar/index.test.tsx","./src/app/drafts/_components/DraftsList/index.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.tsx","./src/app/drafts/_components/DraftsToolbar/index.tsx","./src/app/drafts/page.stories.tsx","./src/app/drafts/page.tsx","./src/app/drafts/page.test.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.tsx","./src/app/drafts/[draftId]/page.stories.tsx","./src/app/drafts/[draftId]/page.tsx","./src/app/drafts/[draftId]/page.test.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftEditor/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftOverviewCards/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftRenderedOutput/index.test.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.stories.tsx","./src/app/drafts/[draftId]/_components/DraftViewSwitcher/index.test.tsx","./src/app/drafts/_components/DraftsList/index.stories.tsx","./src/app/drafts/_components/DraftsList/index.test.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.stories.tsx","./src/app/drafts/_components/DraftsOverviewCards/index.test.tsx","./src/app/drafts/_components/DraftsToolbar/index.stories.tsx","./src/app/drafts/_components/DraftsToolbar/index.test.tsx","./src/app/entities/_components/shared.tsx","./src/app/entities/_components/CreateEntityCard/index.tsx","./src/app/entities/_components/EntityCandidatesCard/index.tsx","./src/app/entities/_components/EntityCard/index.tsx","./src/app/entities/_components/EntitiesPageContent/index.tsx","./src/app/entities/page.stories.tsx","./src/app/entities/page.tsx","./src/app/entities/page.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/root/SliderRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/label/SliderLabel.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/value/SliderValue.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/control/SliderControl.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/track/SliderTrack.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/internals/labelable-provider/LabelableContext.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/thumb/SliderThumb.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/indicator/SliderIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/slider/index.d.ts","./src/components/ui/slider.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.tsx","./src/app/entities/[id]/_components/EntityDetailPageContent/index.tsx","./src/app/entities/[id]/page.stories.tsx","./src/app/entities/[id]/page.tsx","./src/app/entities/[id]/page.test.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.stories.tsx","./src/app/entities/[id]/_components/AuthorityHistoryPanel/index.test.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.stories.tsx","./src/app/entities/[id]/_components/AuthorityWeightControls/index.test.tsx","./src/app/entities/[id]/_components/EntityDetailPageContent/index.test.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.stories.tsx","./src/app/entities/[id]/_components/EntityMentionsPanel/index.test.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.stories.tsx","./src/app/entities/[id]/_components/EntityOverviewCard/index.test.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.stories.tsx","./src/app/entities/[id]/_components/EntitySidebar/index.test.tsx","./src/app/entities/_components/CreateEntityCard/index.stories.tsx","./src/app/entities/_components/CreateEntityCard/index.test.tsx","./src/app/entities/_components/EntitiesPageContent/index.test.tsx","./src/app/entities/_components/EntityCandidatesCard/index.stories.tsx","./src/app/entities/_components/EntityCandidatesCard/index.test.tsx","./src/app/entities/_components/EntityCard/index.stories.tsx","./src/app/entities/_components/EntityCard/index.test.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.tsx","./src/app/entities/candidates/page.stories.tsx","./src/app/entities/candidates/page.tsx","./src/app/entities/candidates/page.test.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.stories.tsx","./src/app/entities/candidates/_components/CandidateClusterCard/index.test.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.stories.tsx","./src/app/entities/candidates/_components/CandidateQueueOverview/index.test.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.stories.tsx","./src/app/entities/candidates/_components/ResolvedCandidateList/index.test.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.tsx","./src/app/ideas/page.stories.tsx","./src/app/ideas/page.tsx","./src/app/ideas/page.test.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.stories.tsx","./src/app/ideas/_components/IdeasQueueOverview/index.test.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.stories.tsx","./src/app/ideas/_components/IdeasToolbarCard/index.test.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.stories.tsx","./src/app/ideas/_components/OriginalContentIdeaCard/index.test.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.tsx","./src/app/invite/[token]/page.stories.tsx","./src/app/invite/[token]/page.tsx","./src/app/invite/[token]/page.test.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.stories.tsx","./src/app/invite/[token]/_components/InvitationDetailsCard/index.test.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.stories.tsx","./src/app/invite/[token]/_components/InvitePageContent/index.test.tsx","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/root/CheckboxRoot.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/indicator/CheckboxIndicator.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/index.parts.d.ts","../node_modules/.pnpm/@base-ui+react@1.4.1_@types+react@19.2.14_react-dom@19.2.6_react@19.2.6__react@19.2.6/node_modules/@base-ui/react/esm/checkbox/index.d.ts","./src/components/ui/checkbox.tsx","./src/app/login/_components/LoginForm/index.tsx","./src/app/login/_components/SocialAuthButtons/index.tsx","./src/app/login/_components/LoginPageContent/index.tsx","./src/app/login/page.stories.tsx","./src/app/login/page.tsx","./src/app/login/page.test.tsx","./src/app/login/_components/LoginForm/index.stories.tsx","./src/app/login/_components/LoginForm/index.test.tsx","./src/app/login/_components/LoginPageContent/index.stories.tsx","./src/app/login/_components/LoginPageContent/index.test.tsx","./src/app/login/_components/SocialAuthButtons/index.stories.tsx","./src/app/login/_components/SocialAuthButtons/index.test.tsx","./src/app/messages/_components/MessagesWorkspace/index.tsx","./src/app/messages/_components/MessagesPageContent/index.tsx","./src/app/messages/page.tsx","./src/app/messages/page.test.tsx","./src/app/messages/_components/MessagesPageContent/index.test.tsx","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/eventMap.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/types.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/dispatchEvent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/focus.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/input.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/click/isClickableInput.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/Blob.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/DataTransfer.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/FileList.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/dataTransfer/Clipboard.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/timeValue.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/isContentEditable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/isEditable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/maxLength.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/edit/setFiles.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/cursor.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/getActiveElement.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/getTabDestination.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/isFocusable.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/selection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/focus/selector.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/keyDef/readNextDescriptor.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/cloneEvent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/findClosest.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getDocumentFromNode.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getTreeDiff.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/getWindow.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isDescendantOrSelf.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isElementType.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isVisible.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/isDisabled.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/level.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/misc/wait.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/pointer/cssPointerEvents.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utils/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/UI.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/getValueOrTextContent.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/copySelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/trackValue.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/document/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/getInputRange.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/modifySelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/moveSelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelectionPerMouse.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/modifySelectionPerMouse.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/selectAll.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelectionRange.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/setSelection.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/updateSelectionOnFocus.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/selection/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/event/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/buttons.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/shared.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/pointer/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/system/keyboard.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/options.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/click.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/hover.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/tab.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/convenience/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/keyboard/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/copy.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/cut.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/paste.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/clipboard/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/pointer/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/clear.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/selectOptions.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/type.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/upload.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/utility/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/api.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/directApi.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/setup.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/setup/index.d.ts","../node_modules/.pnpm/@testing-library+user-event@14.6.1_@testing-library+dom@10.4.1/node_modules/@testing-library/user-event/dist/types/index.d.ts","./src/app/messages/_components/MessagesWorkspace/index.test.tsx","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/file.d.ts","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/file-selector.d.ts","../node_modules/.pnpm/file-selector@2.1.2/node_modules/file-selector/dist/index.d.ts","../node_modules/.pnpm/react-dropzone@15.0.0_react@19.2.6/node_modules/react-dropzone/typings/react-dropzone.d.ts","./src/app/profile/_components/AvatarDropzone/index.tsx","./src/app/profile/_components/AvatarPreview/index.tsx","./src/app/profile/_components/ProfileForm/index.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.tsx","./src/app/profile/page.stories.tsx","./src/app/profile/_components/ProfileSettingsPanel/index.tsx","./src/app/profile/page.tsx","./src/app/profile/page.test.tsx","./src/app/profile/_components/AvatarDropzone/index.stories.tsx","./src/app/profile/_components/AvatarDropzone/index.test.tsx","./src/app/profile/_components/AvatarPreview/index.stories.tsx","./src/app/profile/_components/AvatarPreview/index.test.tsx","./src/app/profile/_components/ProfileForm/index.stories.tsx","./src/app/profile/_components/ProfileForm/index.test.tsx","./src/app/profile/_components/ProfileSettingsPanel/index.test.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.stories.tsx","./src/app/profile/_components/ProfileSettingsWorkspace/index.test.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.tsx","./src/app/projects/[id]/members/_components/shared.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.tsx","./src/app/projects/[id]/members/page.stories.tsx","./src/app/projects/[id]/members/page.tsx","./src/app/projects/[id]/members/page.test.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.stories.tsx","./src/app/projects/[id]/members/_components/InvitationsCard/index.test.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.stories.tsx","./src/app/projects/[id]/members/_components/MembersPageContent/index.test.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.stories.tsx","./src/app/projects/[id]/members/_components/MembershipsCard/index.test.tsx","./src/app/projects/[id]/members/invite/_components/shared.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.tsx","./src/app/projects/[id]/members/invite/page.stories.tsx","./src/app/projects/[id]/members/invite/page.tsx","./src/app/projects/[id]/members/invite/page.test.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.stories.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberFormCard/index.test.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.stories.tsx","./src/app/projects/[id]/members/invite/_components/InviteMemberPageContent/index.test.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.tsx","./src/app/themes/_components/ThemesQueueOverview/index.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.tsx","./src/app/themes/_components/ThemesPageContent/index.tsx","./src/app/themes/page.stories.tsx","./src/app/themes/page.tsx","./src/app/themes/page.test.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.stories.tsx","./src/app/themes/_components/ThemeSuggestionCard/index.test.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.stories.tsx","./src/app/themes/_components/ThemesFilterToolbar/index.test.tsx","./src/app/themes/_components/ThemesPageContent/index.stories.tsx","./src/app/themes/_components/ThemesPageContent/index.test.tsx","./src/app/themes/_components/ThemesQueueOverview/index.stories.tsx","./src/app/themes/_components/ThemesQueueOverview/index.test.tsx","./src/app/trends/_components/TopicClusterCard/index.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.tsx","./src/app/trends/_components/TrendsQueueOverview/index.tsx","./src/app/trends/_components/TrendsPageContent/index.tsx","./src/app/trends/page.stories.tsx","./src/app/trends/page.tsx","./src/app/trends/page.test.tsx","./src/app/trends/_components/TopicClusterCard/index.stories.tsx","./src/app/trends/_components/TopicClusterCard/index.test.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.stories.tsx","./src/app/trends/_components/TrendClusterDetailPanel/index.test.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.stories.tsx","./src/app/trends/_components/TrendsFilterToolbar/index.test.tsx","./src/app/trends/_components/TrendsPageContent/index.stories.tsx","./src/app/trends/_components/TrendsPageContent/index.test.tsx","./src/app/trends/_components/TrendsQueueOverview/index.stories.tsx","./src/app/trends/_components/TrendsQueueOverview/index.test.tsx","./src/components/elements/CopyButton/index.stories.tsx","./src/components/elements/CopyButton/index.test.tsx","./src/components/elements/StatusBadge/index.stories.tsx","./src/components/elements/StatusBadge/index.test.tsx","./src/components/elements/ThemeToggle/index.stories.tsx","./src/components/elements/ThemeToggle/index.test.tsx","./src/components/elements/UserAvatar/index.stories.tsx","./src/components/elements/UserAvatar/index.test.tsx","./src/components/layout/AppShell/index.stories.tsx","./src/components/layout/AppShell/index.test.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.stories.tsx","./src/components/layout/AppShell/_components/AppShellHeader/index.test.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.stories.tsx","./src/components/layout/AppShell/_components/AppShellSidebar/index.test.tsx","./src/components/layout/NotificationMenu/index.test.tsx","../node_modules/.pnpm/@vitest+spy@3.2.4/node_modules/@vitest/spy/dist/index.d.ts","../node_modules/.pnpm/@testing-library+jest-dom@6.9.1/node_modules/@testing-library/jest-dom/types/matchers-standalone.d.ts","../node_modules/.pnpm/@vitest+pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/helpers.d.ts","../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/index-8b61d5bc.d.ts","../node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/index.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/types.d-BCElaP-c.d.ts","../node_modules/.pnpm/@vitest+utils@3.2.4/node_modules/@vitest/utils/dist/diff.d.ts","../node_modules/.pnpm/@vitest+expect@3.2.4/node_modules/@vitest/expect/dist/index.d.ts","../node_modules/.pnpm/storybook@10.4.0_@emnapi+core@1.10.0_@emnapi+runtime@1.10.0_@testing-library+dom@10.4.1_abb79ac70374afb8095762428ba092df/node_modules/storybook/dist/test/index.d.ts","./src/components/layout/UserMenu/index.stories.tsx","./src/components/layout/UserMenu/index.test.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.stories.tsx","./src/components/layout/UserMenu/_components/UserMenuContent/index.test.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.stories.tsx","./src/components/layout/UserMenu/_components/UserMenuTrigger/index.test.tsx","./src/lib/__tests__/useRole.test.tsx","./src/providers/QueryProvider/index.test.tsx","./src/providers/ThemeProvider/index.test.tsx","./.next/types/cache-life.d.ts","./.next/types/validator.ts","../node_modules/.pnpm/vitest@4.1.6_@types+node@25.8.0_@vitest+browser-playwright@4.1.6_@vitest+coverage-v8@4._78a1d11a0e872ad1e283f80fe3219983/node_modules/vitest/globals.d.ts"],"fileIdsList":[[94,158,166,170,173,175,176,177,189,506,507,508,509,548,555],[94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,249,547,548,550,553,555,863,865,867,869,871,873,875,877,879,881,883,885,887,889,893,895,897,899,901,903,905,907,909,911,913,914,915,917,919,921,923,925,927,929,931,933,935,937,938,940,942,944,946,948,950,952,979,1176,1195,1220,1231,1243,1255,1262,1284,1304,1328,1340,1351,1366,1376,1467,1483,1495,1506,1522],[94,158,166,170,173,175,176,177,189,548,551,552,553,555],[94,158,166,170,173,175,176,177,189,249,548,551,555],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1140,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1140],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,807,970,1032,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1168,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1168],[94,158,166,170,173,175,176,177,189,249,530,548,555,806,807,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1169,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1169],[94,158,166,170,173,175,176,177,189,249,548,555,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1170,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1170],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1173,1174],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,1173],[94,158,166,170,173,175,176,177,189,249,548,555,803,806,807,1137,1138,1140,1168,1169,1170,1172],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1172,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,1172],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1032,1138,1171],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,804,807],[94,158,166,170,173,175,176,177,189,249,548,555,803,806],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1176],[94,158,166,170,173,175,176,177,189,249,548,555,805,806,807,861,1137,1138,1173],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1190],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1190],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1191],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1191],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139,1171],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1192],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1192],[85,94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1135,1139,1171],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1193],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1193],[94,158,166,170,173,175,176,177,189,249,548,555,803],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1190,1191,1192,1193],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,809,1195],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,809,861,1137,1190,1191,1192,1193],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1217],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1217],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1135,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1218],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1218],[94,158,166,170,173,175,176,177,189,249,548,555,1138],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1217,1218],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1220],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1217,1218],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1227],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1227],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,862,970,1032,1135,1139,1141,1167,1215,1226],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1228],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1228],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,862,1032,1135,1139,1141,1167,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1229],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1229],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1032,1135,1139,1141,1167,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,803,861],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1227,1228,1229],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,862,1231],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,862,1137,1138,1227,1228,1229],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,863],[94,158,166,170,173,175,176,177,189,249,548,555,860],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,865],[94,158,166,170,173,175,176,177,189,249,547,548,555,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,869],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,867],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,871],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,873],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,875],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,879],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,881],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,877],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,883],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,887],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,885],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,893],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,895],[94,158,166,170,173,175,176,177,189,249,548,555],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,897],[94,158,166,170,173,175,176,177,189,249,547,548,555,861,891],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,899],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,901],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,905],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,903],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,907],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,909],[94,158,166,170,173,175,176,177,189,249,547,548,555,861,892],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,913,914,915],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,911],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,921],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,919],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,917],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,925],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,923],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,927],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,929],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,931],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,933],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,935],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,937,938],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,940],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,942],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,944],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,889],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,946],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,861,948],[94,158,166,170,173,175,176,177,189,249,547,548,555,803,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,952],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,861,950],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1174,1240],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1240],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1139,1239],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1174,1241],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1241],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,976,1021,1174,1239],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,976,1239],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,966,1032,1138],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1240,1241],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,954,1243],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,954,1137,1138,1240,1241],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1257],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1257],[85,94,158,166,170,173,175,176,177,189,249,530,540,548,555,803,970,1032,1135,1138,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1258],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1258],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1260],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1260],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135,1259],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1259],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1259],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1257,1258,1259,1260],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1262],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1257,1258,1259,1260],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1251],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1251],[94,158,166,170,173,175,176,177,189,249,548,555,803,1021,1174,1252],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1252],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1253],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1253],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135,1167],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1137,1138,1174,1251,1252,1253],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1255],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1251,1252,1253],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1298],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1298],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1039,1135,1297],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1297],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1297],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,1032,1135,1138,1141,1215,1296],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1302],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1298,1299,1300,1301],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1299],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1299],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1300],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1300],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1301],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1301],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,970,1032,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1302],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1304],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1302],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1279],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1279],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1135,1141,1215,1216,1278],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1282],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1279,1280,1281],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1280],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1280],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1135,1138,1139,1141,1167,1278],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1281],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1281],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1032,1135,1138,1139,1141,1215,1216,1278],[94,158,166,170,173,175,176,177,189,249,548,555,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,955,1021,1174,1324],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,955,1324],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,955,1032,1039,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1325],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1325],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1326],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1326],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,1039,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,955,1021,1137,1138,1174,1324,1325,1326],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1328],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,955,1137,1138,1324,1325,1326],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1282],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1284],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1282],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1336],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1336],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1337],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1337],[94,158,166,170,173,175,176,177,189,249,530,548,555,956,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1338],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1338],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,956,970,1032,1039,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1138,1174,1336,1337,1338],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1340],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,956,1137,1138,1336,1337,1338],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1348],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1348],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1032,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1349],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1349],[94,158,166,170,173,175,176,177,189,249,548,555,803,1135,1138,1348],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1351],[94,158,166,170,173,175,176,177,189,249,548,555,805,857,860,861,1349],[85,94,158,166,170,173,175,176,177,189,249,548,551,555,970,976,978],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1362],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1362],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,1032,1120,1138,1141,1215,1361],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1364],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1364],[94,158,166,170,173,175,176,177,189,249,530,548,555,1133,1135,1362,1363],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1363],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1363],[94,158,166,170,173,175,176,177,189,249,548,555,1032,1120],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1366],[94,158,166,170,173,175,176,177,189,249,548,555,1364],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1375],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1374],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,960,976,1374,1455],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,960,966,1032,1135,1138,1216],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1376],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1375],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1461],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1461],[85,94,158,166,170,173,175,176,177,189,249,548,555,1135,1138,1460],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1462],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1462],[94,158,166,170,173,175,176,177,189,249,548,555,803,1032,1128,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1463],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1463],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,1032,1135,1141,1215,1216],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,804,966,1466],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,962,966,1138,1464],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1464],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1464],[94,158,166,170,173,175,176,177,189,249,548,555,803,1138,1461,1462,1463],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1137,1174,1464],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1467],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1466],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1478],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1478],[94,158,166,170,173,175,176,177,189,249,548,555,803,805,970,1032,1039,1135,1138,1226],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1481],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1481],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,970,1032,1135,1137,1138,1478,1480],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1480],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1480],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,1032,1135,1141,1167,1479],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1492],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1492],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1135,1141,1167,1215,1491],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1493],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1493],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1492],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1495],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1493],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1483],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1481],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1503],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1503],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,957,970,1032,1135,1139,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1501],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1501],[94,158,166,170,173,175,176,177,189,249,530,548,555,957,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1504],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1504],[94,158,166,170,173,175,176,177,189,249,548,555,803,957,1137,1138,1501,1502,1503],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1502],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1502],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1506],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1504],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1516],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1516],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,970,1135,1139],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1517],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1517],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,805,958,970,1032,1135,1138,1139],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1518],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1518],[94,158,166,170,173,175,176,177,189,249,530,548,555,958,970,1032,1135,1141,1167],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1174,1520],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,804,1520],[94,158,166,170,173,175,176,177,189,249,548,555,803,1137,1138,1516,1517,1518,1519],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1519],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1519],[94,158,166,170,173,175,176,177,189,249,548,555,805,1135],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,804,958],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,805,1522],[94,158,166,170,173,175,176,177,189,249,548,555,805,861,1137,1138,1520],[85,94,158,166,170,173,175,176,177,189,249,548,555,1021,1174,1226],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1226],[85,94,158,166,170,173,175,176,177,189,249,548,555,1022,1032],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1139,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1139],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1031,1039],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1033,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1033],[85,94,158,166,170,173,175,176,177,189,249,548,555,977,1022,1032],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1128,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1128],[94,158,166,170,173,175,176,177,189,249,548,555,970,1127],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1134,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1134],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1022,1032,1033,1117,1131,1133],[94,158,166,170,173,175,176,177,189,249,548,555,803,804,1021,1136,1174],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,960,966,976,1136],[94,158,166,170,173,175,176,177,189,249,530,548,555,803,960,966,970,1039,1135],[94,158,166,170,173,175,176,177,189,249,548,555,804,1021,1135,1137,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,1137],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,861,1134,1136],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,961,976,1117,1455],[85,94,158,166,170,173,175,176,177,189,249,540,548,555,803,961,966,1022,1039,1116],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1116,1129,1174,1560],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1116,1129],[94,158,166,170,173,175,176,177,189,249,530,548,555,970,1032,1116,1120,1128],[94,158,166,170,173,175,176,177,189,249,548,555,1021,1116,1130,1174],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,1116,1130],[94,158,166,170,173,175,176,177,189,249,548,555,1116,1128],[94,158,166,170,173,175,176,177,189,249,548,555,803,962,966,1021,1131,1174,1560],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,976,1131],[94,158,166,170,173,175,176,177,189,249,548,555,962,966,1116,1129,1130],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1031],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1126],[94,158,166,170,173,175,176,177,189,249,548,555,970,1031,1035,1038],[94,158,166,170,173,175,176,177,189,249,548,555,970,1029,1031],[85,94,158,166,170,173,175,176,177,189,249,548,555,970],[94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1360],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1115],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1214],[85,94,158,166,170,173,175,176,177,189,249,548,555,970,1022,1166],[94,158,166,170,173,175,176,177,189,249,548,555,970,1132],[94,158,166,170,173,175,176,177,189,249,548,555,970,1295],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,861],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,860],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,806],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,803,966,967],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,803,805],[85,94,158,166,170,173,175,176,177,189,249,548,555,803,857,860],[94,158,166,170,173,175,176,177,189,249,548,555,850,857,858,859],[94,158,166,170,173,175,176,177,189,249,548,555,803,805],[94,158,166,170,173,175,176,177,189,249,548,555,803,966],[94,158,166,170,173,175,176,177,189,249,548,555,968,969],[94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,966,976],[85,94,158,166,170,173,175,176,177,189,249,548,555,966],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801,978],[85,94,158,166,170,173,175,176,177,189,249,548,555,977],[94,158,166,170,173,175,176,177,189,548],[94,158,166,170,173,175,176,177,181,189,206,249,548,555,771,775,776,778],[85,94,158,166,170,173,175,176,177,189,249,548,555,654,661,801],[94,158,166,170,173,175,176,177,189,548,555,771],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1121],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1121,1122],[85,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,548,555,1121,1123,1124,1125],[94,158,166,170,173,175,176,177,189,548,555,1121,1123,1124],[85,94,158,166,170,173,175,176,177,189,548,555,1027],[94,158,166,170,173,175,176,177,189,548,555,1028],[94,158,166,170,173,175,176,177,189,548,555,1357,1358,1359],[94,158,166,170,173,175,176,177,189,548,555,1357,1358],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1357],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1027,1147],[94,158,166,170,173,175,176,177,189,249,548,555,1026,1095,1115],[85,94,158,166,170,173,175,176,177,189,548,555,1092],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1147],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1147],[94,158,166,170,173,175,176,177,189,548,555,1147,1205,1206,1207,1208,1209,1210,1211],[94,158,166,170,173,175,176,177,189,548,555,1147,1205,1206,1207,1208,1209,1210],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1146],[85,94,158,166,170,173,175,176,177,189,548,555,1057,1147],[85,94,158,166,170,173,175,176,177,189,548,555,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1047,1063,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1065],[85,94,158,166,170,173,175,176,177,189,548,555,1025,1056,1057,1061,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1047,1080],[94,158,166,170,173,175,176,177,189,548,555,1080],[94,158,166,170,173,175,176,177,189,548,555,1024,1080],[94,158,166,170,173,175,176,177,189,548,555,1047,1080],[94,158,166,170,173,175,176,177,189,548,555,1025,1062,1080],[94,158,166,170,173,175,176,177,189,548,555,1070,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1047,1070,1080],[85,94,158,166,170,173,175,176,177,189,548,555,1045,1080],[94,158,166,170,173,175,176,177,189,548,555,1026,1056,1061,1062],[94,158,166,170,173,175,176,177,189,548,555,1044,1046,1047,1064,1066,1067,1068,1069,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084],[94,158,166,170,173,175,176,177,189,548,555,1070],[85,94,158,166,170,173,175,176,177,189,548,555,1025,1044,1046,1047,1062,1064,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1081,1085],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1027,1144],[94,158,166,170,173,175,176,177,189,548,555,1145],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1212],[94,158,166,170,173,175,176,177,189,548,555,1213],[94,158,166,170,173,175,176,177,189,548,555,1024],[85,94,158,166,170,173,175,176,177,189,548,555,1145,1147],[94,158,166,170,173,175,176,177,189,548,555,1023],[85,94,158,166,170,173,175,176,177,189,548,555,1026],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1036],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1086],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1095],[94,158,166,170,173,175,176,177,189,548,555,1087,1088,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1109,1110,1111,1113,1114],[94,158,166,170,173,175,176,177,189,548,555,1087,1088,1094,1095,1096,1097,1098,1099,1100,1101,1102,1103,1104,1105,1106,1107,1109,1110,1111,1112,1113],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1057,1063,1086],[85,94,158,166,170,173,175,176,177,189,548,555,1085],[85,94,158,166,170,173,175,176,177,189,249,548,555,1024,1025,1061,1089,1090,1091,1093,1094],[85,94,158,166,170,173,175,176,177,189,548,555,1089,1095],[94,158,166,170,173,175,176,177,189,548,555,1089],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1047,1056,1057,1061,1062,1063,1085,1095,1115],[94,158,166,170,173,175,176,177,189,249,548,555,1095,1108],[85,94,158,166,170,173,175,176,177,189,548,555,1089],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1094],[85,94,158,166,170,173,175,176,177,189,548,555,1095],[94,158,166,170,173,175,176,177,189,548,555,1034],[94,158,166,170,173,175,176,177,189,548,555,1143,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164,1165],[94,158,166,170,173,175,176,177,189,548,555,1112,1143,1148,1149,1150,1151,1152,1153,1154,1155,1156,1157,1158,1159,1160,1161,1162,1163,1164],[85,94,158,166,170,173,175,176,177,189,548,555,1024,1025,1142],[94,158,166,170,173,175,176,177,189,548,555,1112],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1286],[94,158,166,170,173,175,176,177,189,548,555,1286,1287,1288,1289,1290,1292,1293,1294],[94,158,166,170,173,175,176,177,189,548,555,1286,1287,1288,1289,1290,1292,1293],[85,94,158,166,170,173,175,176,177,189,548,555,1027,1286,1291],[85,94,158,166,170,173,175,176,177,189,548,555,1025],[94,158,166,170,173,175,176,177,189,548,555,1027,1037],[94,158,166,170,173,175,176,177,189,548,555,1058,1059,1060],[85,94,158,166,170,173,175,176,177,189,548,555,1056,1057,1059],[94,158,166,170,173,175,176,177,189,548,555,1027,1057,1058,1062,1085],[85,94,158,166,170,173,175,176,177,189,548,555,1040,1085],[94,158,166,170,173,175,176,177,189,548,555,1052],[85,94,158,166,170,173,175,176,177,189,249,548,555,1052],[94,158,166,170,173,175,176,177,189,548,555,1048],[94,158,166,170,173,175,176,177,189,548,555,1049],[94,158,166,170,173,175,176,177,189,548,555,1049,1050,1052,1053,1054,1055],[94,158,166,170,173,175,176,177,189,548,555,1051,1052],[94,158,166,170,173,175,176,177,189,548,555,1040],[94,158,166,170,173,175,176,177,189,548,555,1041,1042],[85,94,158,166,170,173,175,176,177,189,548,555,1043],[94,158,166,170,173,175,176,177,189,548,555,608,774,1016,1018],[94,158,166,170,173,175,176,177,189,548,555,562,563,564],[94,158,166,170,173,175,176,177,189,548,555,565],[94,158,166,170,173,175,176,177,189,548,555,1012],[94,158,166,170,173,175,176,177,189,548,555,775],[94,158,166,170,173,175,176,177,189,548,555,608,774,1009],[94,158,166,170,173,175,176,177,189,541,548,555,1009,1010,1011,1014,1015,1020,1560],[94,158,166,170,173,175,176,177,189,548,555,1009,1011,1015,1019],[85,94,158,166,170,173,175,176,177,189,219,482,548,555,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,963],[85,94,158,166,170,173,175,176,177,189,249,548,555,964],[94,158,166,170,173,175,176,177,189,548,555,965],[94,158,166,170,173,175,176,177,189,548,555,787],[94,158,166,170,173,175,176,177,189,548,555,784,785,786,787,788,791,792,793,794,795,796,797,798],[94,158,166,170,173,175,176,177,189,548,555,780],[94,158,166,170,173,175,176,177,189,548,555,790],[94,158,166,170,173,175,176,177,189,548,555,784,785,786],[94,158,166,170,173,175,176,177,189,548,555,784,785],[94,158,166,170,173,175,176,177,189,548,555,787,788,790],[94,158,166,170,173,175,176,177,189,548,555,785],[94,158,166,170,173,175,176,177,189,548,555,782],[94,158,166,170,173,175,176,177,189,548,555,781],[85,94,158,166,170,173,175,176,177,189,219,482,548,555,799,800],[94,158,166,170,173,175,176,177,189,548,555,1454],[94,158,166,170,173,175,176,177,189,548,555,1441,1442,1443],[94,158,166,170,173,175,176,177,189,548,555,1436,1437,1438],[94,158,166,170,173,175,176,177,189,548,555,1414,1415,1416,1417],[94,158,166,170,173,175,176,177,189,548,555,1380,1454],[94,158,166,170,173,175,176,177,189,548,555,1380],[94,158,166,170,173,175,176,177,189,548,555,1380,1381,1382,1383,1428],[94,158,166,170,173,175,176,177,189,548,555,1418],[94,158,166,170,173,175,176,177,189,548,555,1413,1419,1420,1421,1422,1423,1424,1425,1426,1427],[94,158,166,170,173,175,176,177,189,548,555,1428],[94,158,166,170,173,175,176,177,189,548,555,1379],[94,158,166,170,173,175,176,177,189,548,555,1432,1434,1435,1453,1454],[94,158,166,170,173,175,176,177,189,548,555,1432,1434],[94,158,166,170,173,175,176,177,189,548,555,1429,1432,1454],[94,158,166,170,173,175,176,177,189,548,555,1439,1440,1444,1445,1450],[94,158,166,170,173,175,176,177,189,548,555,1433,1435,1445,1453],[94,158,166,170,173,175,176,177,189,548,555,1452,1453],[94,158,166,170,173,175,176,177,189,548,555,1429,1433,1435,1451,1452],[94,158,166,170,173,175,176,177,189,548,555,1433,1454],[94,158,166,170,173,175,176,177,189,548,555,1431],[94,158,166,170,173,175,176,177,189,548,555,1431,1433,1454],[94,158,166,170,173,175,176,177,189,548,555,1429,1430],[94,158,166,170,173,175,176,177,189,548,555,1446,1447,1448,1449],[94,158,166,170,173,175,176,177,189,548,555,1435,1454],[94,158,166,170,173,175,176,177,189,548,555,1390],[94,158,166,170,173,175,176,177,189,548,555,1384,1391],[94,158,166,170,173,175,176,177,189,548,555,1384,1385,1386,1387,1388,1389,1390,1391,1392,1393,1394,1395,1396,1397,1398,1399,1400,1401,1402,1403,1404,1405,1406,1407,1408,1409,1410,1411,1412],[94,158,166,170,173,175,176,177,189,548,555,1410,1454],[94,158,166,170,173,175,176,177,189,548,555,632,633],[94,155,156,158,166,170,173,175,176,177,189,548,555],[94,157,158,166,170,173,175,176,177,189,548,555],[158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,197,548,555],[94,158,159,164,166,169,170,173,175,176,177,179,189,194,206,548,555],[94,158,159,160,166,169,170,173,175,176,177,189,548,555],[94,158,161,166,170,173,175,176,177,189,207,548,555],[94,158,162,163,166,170,173,175,176,177,180,189,548,555],[94,158,163,166,170,173,175,176,177,189,194,203,548,555],[94,158,164,166,169,170,173,175,176,177,179,189,548,555],[94,157,158,165,166,170,173,175,176,177,189,548,555],[94,158,166,167,170,173,175,176,177,189,548,555],[94,158,166,168,169,170,173,175,176,177,189,548,555],[94,157,158,166,169,170,173,175,176,177,189,548,555],[94,158,166,169,170,171,173,175,176,177,189,194,206,548,555],[94,158,166,169,170,171,173,175,176,177,189,194,197,548,555],[94,145,158,166,169,170,172,173,175,176,177,179,189,194,206,548,555],[94,158,166,169,170,172,173,175,176,177,179,189,194,203,206,548,555],[94,158,166,170,172,173,174,175,176,177,189,194,203,206,548,555],[92,93,94,95,96,97,98,99,100,101,102,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,548,555],[94,158,166,169,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,177,189,548,555],[94,158,166,170,173,175,176,177,178,189,206,548,555],[94,158,166,169,170,173,175,176,177,179,189,194,548,555],[94,158,166,170,173,175,176,177,180,189,548,555],[94,158,166,170,173,175,176,177,181,189,548,555],[94,158,166,169,170,173,175,176,177,184,189,548,555],[94,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,548,555],[94,158,166,170,173,175,176,177,186,189,548,555],[94,158,166,170,173,175,176,177,187,189,548,555],[94,158,163,166,170,173,175,176,177,179,189,197,548,555],[94,158,166,169,170,173,175,176,177,189,190,548,555],[94,158,166,170,173,175,176,177,189,191,207,210,548,555],[94,158,166,169,170,173,175,176,177,189,194,196,197,548,555],[94,158,166,170,173,175,176,177,189,195,197,548,555],[94,158,166,170,173,175,176,177,189,197,207,548,555],[94,158,166,170,173,175,176,177,189,198,548,555],[94,155,158,166,170,173,175,176,177,189,194,200,206,548,555],[94,158,166,170,173,175,176,177,189,194,199,548,555],[94,158,166,169,170,173,175,176,177,189,201,202,548,555],[94,158,166,170,173,175,176,177,189,201,202,548,555],[94,158,163,166,170,173,175,176,177,179,189,194,203,548,555],[94,158,166,170,173,175,176,177,189,204,548,555],[94,158,166,170,173,175,176,177,179,189,205,548,555],[94,158,166,170,172,173,175,176,177,187,189,206,548,555],[94,158,166,170,173,175,176,177,189,207,208,548,555],[94,158,163,166,170,173,175,176,177,189,208,548,555],[94,158,166,170,173,175,176,177,189,194,209,548,555],[94,158,166,170,173,175,176,177,178,189,210,548,555],[94,158,166,170,173,175,176,177,189,211,548,555],[94,158,161,166,170,173,175,176,177,189,548,555],[94,158,163,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,207,548,555],[94,145,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,189,212,548,555],[94,158,166,170,173,175,176,177,184,189,548,555],[94,158,166,170,173,175,176,177,189,202,548,555],[94,145,158,166,169,170,171,173,175,176,177,184,189,194,197,206,209,210,212,548,555],[94,158,166,170,173,175,176,177,189,194,213,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,217,219,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,217,218,482,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,216,218,219,501,546,548,555],[85,94,158,166,170,173,175,176,177,189,219,482,483,548,555],[85,94,158,166,170,173,175,176,177,189,219,482,548,555],[85,89,94,158,166,170,173,175,176,177,189,216,217,218,219,501,546,548,555],[85,89,94,158,166,170,173,175,176,177,189,215,217,218,219,501,546,548,555],[83,84,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,548,555,608,774,777],[94,158,166,170,173,175,176,177,189,548,555,662],[94,158,166,170,173,175,176,177,189,548,555,664,671,672,770,771],[94,158,166,170,173,175,176,177,189,548,555,654,658,659,661],[94,158,166,170,173,175,176,177,189,548,555,662,671,672,771],[94,158,166,170,173,175,176,177,189,548,555,659,662],[94,158,166,170,173,175,176,177,189,548,555,654,660,661,662],[94,158,166,170,173,175,176,177,189,548,555,1549,1555,1556,1558],[94,158,166,170,173,175,176,177,189,548,555,610,616,629,630,631,634],[94,158,166,170,173,175,176,177,189,548,555,640],[94,158,166,170,173,175,176,177,189,548,555,640,641],[94,158,166,170,173,175,176,177,189,548,555,614,616,617],[94,158,166,170,173,175,176,177,189,548,555,614,616],[94,158,166,170,173,175,176,177,189,548,555,614],[94,158,166,170,173,175,176,177,189,548,555,609,614,620,621],[94,158,166,170,173,175,176,177,189,548,555,609,614,620],[94,158,166,170,173,175,176,177,189,548,555,628],[94,158,166,170,173,175,176,177,189,548,555,1551,1557],[94,158,166,170,173,175,176,177,189,548,555,1552],[94,158,166,170,173,175,176,177,189,548,555,1551,1552,1553,1555],[94,158,166,170,173,175,176,177,189,548,555,1551],[94,158,166,170,173,175,176,177,189,548,555,609,615],[94,158,166,170,173,175,176,177,189,548,555,609],[94,158,166,170,173,175,176,177,189,548,555,609,611,615,656],[94,158,166,170,173,175,176,177,189,548,555,611],[94,158,166,170,173,175,176,177,189,548,555,609,610,611,612,613],[94,158,166,170,173,175,176,177,189,548,555,1002,1003],[94,158,166,170,173,175,176,177,189,548,555,986,987],[94,158,166,170,173,175,176,177,189,548,555,986],[94,158,166,170,173,175,176,177,189,548,555,987,989],[94,158,166,170,173,175,176,177,189,548,555,986,992,993],[94,158,166,170,173,175,176,177,189,548,555,986,988,989,990,992,993,994],[94,158,166,170,173,175,176,177,189,548,555,989,990,991],[94,158,166,170,173,175,176,177,189,548,555,989,992,994],[94,158,166,170,173,175,176,177,189,548,555,989],[94,158,166,170,173,175,176,177,189,548,555,989,992],[94,158,166,170,173,175,176,177,189,548,555,986,988],[94,158,166,170,173,175,176,177,189,548,555,968,1030],[94,158,166,170,173,175,176,177,189,548,555,968],[94,158,166,170,173,175,176,177,189,548,555,649,650],[94,158,166,170,173,175,176,177,189,548,555,649,650,651,652],[94,158,166,170,173,175,176,177,189,548,555,649,651],[94,158,166,170,173,175,176,177,189,548,555,649],[94,158,166,170,173,175,176,177,189,548,555,1457],[94,158,166,170,173,175,176,177,189,548,555,1457,1458],[94,158,166,170,173,175,176,177,189,548,555,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842],[94,158,166,170,173,175,176,177,189,548,555,811],[94,158,166,170,173,175,176,177,189,548,555,811,821],[94,158,166,170,173,175,176,177,189,548,555,601,602],[94,158,166,170,173,175,176,177,189,548,555,857],[94,158,166,170,172,173,175,176,177,189,214,548,555,857],[94,158,166,170,173,175,176,177,189,548,555,848,855],[94,158,166,170,173,175,176,177,189,547,548,551,555,855,857],[94,158,166,170,173,175,176,177,189,548,555,810,844,851,853,854],[94,158,166,170,173,175,176,177,189,548,555,849,855,856],[94,158,166,170,173,175,176,177,189,547,548,551,555,852,857],[94,158,166,170,173,175,176,177,189,214,548,555,857],[94,158,166,170,173,175,176,177,189,548,555,849,851,857],[94,158,166,170,173,175,176,177,189,548,555,851,855,857],[94,158,166,170,173,175,176,177,189,548,555,851],[94,158,166,170,173,175,176,177,189,548,555,846,847,850],[94,158,166,170,173,175,176,177,189,548,555,843,844,845,851,857],[85,94,158,166,170,173,175,176,177,189,548,555,851,857,1118,1119],[85,94,158,166,170,173,175,176,177,189,548,555,851,857],[94,158,166,170,173,175,176,177,189,504,548,555],[94,158,166,170,173,175,176,177,189,452,515,516,548,555],[94,158,166,170,173,175,176,177,189,224,225,227,239,263,378,389,497,548,555],[94,158,166,170,173,175,176,177,189,227,258,259,260,262,497,548,555],[94,158,166,170,173,175,176,177,189,227,395,397,399,400,402,497,499,548,555],[94,158,166,170,173,175,176,177,189,227,261,298,497,548,555],[94,158,166,170,173,175,176,177,189,225,227,238,239,245,251,256,377,378,379,388,497,499,548,555],[94,158,166,170,173,175,176,177,189,497,548,555],[94,158,166,170,173,175,176,177,189,234,240,259,279,374,548,555],[94,158,166,170,173,175,176,177,189,227,548,555],[94,158,166,170,173,175,176,177,189,220,234,240,548,555],[94,158,166,170,173,175,176,177,189,406,548,555],[94,158,166,170,173,175,176,177,189,403,404,406,548,555],[94,158,166,170,173,175,176,177,189,403,405,497,548,555],[94,158,166,170,172,173,175,176,177,189,279,476,494,548,555],[94,158,166,170,172,173,175,176,177,189,350,353,369,374,494,548,555],[94,158,166,170,172,173,175,176,177,189,322,494,548,555],[94,158,166,170,173,175,176,177,189,382,548,555],[94,158,166,170,173,175,176,177,189,381,382,383,548,555],[94,158,166,170,173,175,176,177,189,381,548,555],[91,94,158,166,170,172,173,175,176,177,189,220,227,239,245,251,257,259,263,264,277,278,345,375,376,389,497,501,548,555],[94,158,166,170,173,175,176,177,189,224,227,261,298,395,396,401,497,548,549,555],[94,158,166,170,173,175,176,177,189,261,548,549,555],[94,158,166,170,173,175,176,177,189,224,278,447,497,548,549,555],[94,158,166,170,173,175,176,177,189,548,549,555],[94,158,166,170,173,175,176,177,189,227,261,262,548,549,555],[94,158,166,170,173,175,176,177,189,398,548,549,555],[94,158,166,170,173,175,176,177,189,264,377,380,387,548,555],[85,94,158,166,170,173,175,176,177,189,452,548,555],[94,158,166,170,173,175,176,177,187,189,234,249,548,555],[94,158,166,170,173,175,176,177,189,234,249,548,555],[85,94,158,166,170,173,175,176,177,189,319,548,555],[85,94,158,166,170,173,175,176,177,189,249,548,555],[85,94,158,166,170,173,175,176,177,189,240,249,452,548,555],[94,158,166,170,173,175,176,177,189,234,305,319,320,531,538,548,555],[94,158,166,170,173,175,176,177,189,304,532,533,534,535,537,548,555],[94,158,166,170,173,175,176,177,189,355,548,555],[94,158,166,170,173,175,176,177,189,355,356,548,555],[94,158,166,170,173,175,176,177,189,238,240,307,308,548,555],[94,158,166,170,173,175,176,177,189,240,314,315,548,555],[94,158,166,170,173,175,176,177,189,240,309,317,548,555],[94,158,166,170,173,175,176,177,189,314,548,555],[94,158,166,170,173,175,176,177,189,232,240,307,308,309,310,311,312,313,314,317,548,555],[94,158,166,170,173,175,176,177,189,240,307,314,315,316,318,548,555],[94,158,166,170,173,175,176,177,189,240,308,310,311,548,555],[94,158,166,170,173,175,176,177,189,308,310,313,315,548,555],[94,158,166,170,173,175,176,177,189,536,548,555],[94,158,166,170,173,175,176,177,189,240,548,555],[85,94,158,166,170,173,175,176,177,189,228,525,548,555],[85,94,158,166,170,173,175,176,177,189,206,548,555],[85,94,158,166,170,173,175,176,177,189,261,296,548,555],[85,94,158,166,170,173,175,176,177,189,261,389,548,555],[94,158,166,170,173,175,176,177,189,294,299,548,555],[85,94,158,166,170,173,175,176,177,189,295,503,548,555],[85,89,94,158,166,170,172,173,175,176,177,189,215,216,217,218,219,501,545,548,555],[94,158,166,170,172,173,175,176,177,189,240,548,555],[94,158,166,170,172,173,175,176,177,189,239,244,325,342,384,385,389,444,446,497,498,548,555],[94,158,166,170,173,175,176,177,189,277,386,548,555],[94,158,166,170,173,175,176,177,189,501,548,555],[94,158,166,170,173,175,176,177,189,226,548,555],[85,94,158,166,170,173,175,176,177,189,231,234,449,465,467,548,555],[94,158,166,170,173,175,176,177,187,189,234,449,464,465,466,548,555],[94,158,166,170,173,175,176,177,189,458,459,460,461,462,463,548,555],[94,158,166,170,173,175,176,177,189,460,548,555],[94,158,166,170,173,175,176,177,189,464,548,555],[94,158,166,170,173,175,176,177,189,249,413,414,416,548,555],[85,94,158,166,170,173,175,176,177,189,240,407,408,409,410,415,548,555],[94,158,166,170,173,175,176,177,189,413,415,548,555],[94,158,166,170,173,175,176,177,189,411,548,555],[94,158,166,170,173,175,176,177,189,412,548,555],[85,94,158,166,170,173,175,176,177,189,249,295,503,548,555],[85,94,158,166,170,173,175,176,177,189,249,502,503,548,555],[85,94,158,166,170,173,175,176,177,189,249,503,548,555],[94,158,166,170,173,175,176,177,189,342,343,548,555],[94,158,166,170,173,175,176,177,189,343,548,555],[94,158,166,170,172,173,175,176,177,189,498,503,548,555],[94,158,166,170,173,175,176,177,189,372,548,555],[94,157,158,166,170,173,175,176,177,189,371,548,555],[94,158,166,170,173,175,176,177,189,234,240,246,248,350,363,367,369,446,449,486,487,494,498,548,555],[94,158,166,170,173,175,176,177,189,240,289,311,548,555],[94,158,166,170,173,175,176,177,189,350,361,364,369,548,555],[85,94,158,166,170,173,175,176,177,189,231,234,350,353,369,372,406,453,454,455,456,457,468,469,470,471,472,473,474,475,548,549,555],[94,158,166,170,173,175,176,177,189,231,234,259,350,357,358,359,362,363,548,555],[94,158,166,170,173,175,176,177,189,194,240,259,361,368,449,450,494,548,555],[94,158,166,170,173,175,176,177,189,365,548,555],[94,158,166,170,172,173,175,176,177,187,189,228,240,244,254,286,287,290,342,345,410,444,445,486,497,498,499,501,548,549,555],[94,158,166,170,173,175,176,177,189,231,232,234,548,555],[94,158,166,170,173,175,176,177,189,350,548,555],[94,157,158,166,170,173,175,176,177,189,259,286,287,344,345,346,347,348,349,498,548,555],[94,158,166,170,173,175,176,177,189,369,548,555],[94,157,158,166,170,173,175,176,177,189,233,234,244,248,284,350,357,358,359,360,361,364,365,366,367,368,487,548,555],[94,158,166,170,172,173,175,176,177,189,284,285,357,498,499,548,555],[94,158,166,170,173,175,176,177,189,259,287,342,345,350,446,498,548,555],[94,158,166,170,172,173,175,176,177,189,497,499,548,555],[94,158,166,170,172,173,175,176,177,189,194,494,498,499,548,555],[94,158,166,170,172,173,175,176,177,187,189,220,234,239,246,248,251,254,261,281,286,287,288,289,290,325,326,328,331,333,336,337,338,339,341,389,444,446,494,497,498,499,548,555],[94,158,166,170,172,173,175,176,177,189,194,548,555],[94,158,166,170,173,175,176,177,189,227,228,229,257,494,495,496,501,503,548,549,555],[94,158,166,170,173,175,176,177,189,224,225,497,548,555],[94,158,166,170,173,175,176,177,189,418,548,555],[94,158,166,170,172,173,175,176,177,189,194,206,236,402,406,407,408,409,410,416,417,548,549,555],[94,158,166,170,173,175,176,177,187,189,206,220,234,236,248,251,287,326,331,341,342,395,422,423,424,430,433,434,444,446,494,497,548,555],[94,158,166,170,173,175,176,177,189,251,257,264,277,287,345,497,548,555],[94,158,166,170,172,173,175,176,177,189,206,228,239,248,287,428,494,497,548,555],[94,158,166,170,173,175,176,177,189,448,548,555],[94,158,166,170,172,173,175,176,177,189,418,431,432,441,548,555],[94,158,166,170,173,175,176,177,189,494,497,548,555],[94,158,166,170,173,175,176,177,189,347,487,548,555],[94,158,166,170,173,175,176,177,189,248,286,389,503,548,555],[94,158,166,170,172,173,175,176,177,187,189,226,331,391,395,424,430,433,436,494,548,555],[94,158,166,170,172,173,175,176,177,189,264,277,395,437,548,555],[94,158,166,170,173,175,176,177,189,227,288,389,439,497,499,548,555],[94,158,166,170,172,173,175,176,177,189,206,410,497,548,555],[94,158,166,170,172,173,175,176,177,189,261,288,389,390,391,400,418,438,440,497,548,555],[91,94,158,166,170,172,173,175,176,177,189,286,443,501,503,548,555],[94,158,166,170,173,175,176,177,189,340,444,548,555],[94,158,166,170,172,173,175,176,177,187,189,234,237,239,240,246,248,254,263,264,277,287,290,326,328,338,341,342,389,422,423,424,425,427,429,444,446,494,503,548,555],[94,158,166,170,172,173,175,176,177,189,194,264,430,435,441,494,548,555],[94,158,166,170,173,175,176,177,189,267,268,269,270,271,272,273,274,275,276,548,555],[94,158,166,170,173,175,176,177,189,281,332,548,555],[94,158,166,170,173,175,176,177,189,334,548,555],[94,158,166,170,173,175,176,177,189,332,548,555],[94,158,166,170,173,175,176,177,189,334,335,548,555],[94,158,166,170,172,173,175,176,177,189,238,239,240,244,245,498,548,555],[94,158,166,170,172,173,175,176,177,187,189,226,228,246,250,286,289,290,324,444,494,499,501,503,548,555],[94,158,166,170,172,173,175,176,177,187,189,206,230,237,238,248,250,287,442,487,493,498,548,555],[94,158,166,170,173,175,176,177,189,357,548,555],[94,158,166,170,173,175,176,177,189,358,548,555],[94,158,166,170,173,175,176,177,189,240,251,486,548,555],[94,158,166,170,173,175,176,177,189,359,548,555],[94,158,166,170,173,175,176,177,189,233,548,555],[94,158,166,170,173,175,176,177,189,235,247,548,555],[94,158,166,170,172,173,175,176,177,189,235,239,246,548,555],[94,158,166,170,173,175,176,177,189,242,247,548,555],[94,158,166,170,173,175,176,177,189,243,548,555],[94,158,166,170,173,175,176,177,189,235,236,548,555],[94,158,166,170,173,175,176,177,189,235,291,548,555],[94,158,166,170,173,175,176,177,189,235,548,555],[94,158,166,170,173,175,176,177,189,237,281,330,548,555],[94,158,166,170,173,175,176,177,189,329,548,555],[94,158,166,170,173,175,176,177,189,234,236,237,548,555],[94,158,166,170,173,175,176,177,189,237,327,548,555],[94,158,166,170,173,175,176,177,189,234,236,548,555],[94,158,166,170,173,175,176,177,189,286,389,548,555],[94,158,166,170,173,175,176,177,189,486,548,555],[94,158,166,170,172,173,175,176,177,189,206,246,248,252,286,389,443,446,449,450,451,477,478,481,485,487,494,498,548,555],[94,158,166,170,173,175,176,177,189,300,303,305,306,319,320,548,555],[85,94,158,166,170,173,175,176,177,189,217,219,249,479,480,548,555],[85,94,158,166,170,173,175,176,177,189,217,219,249,479,480,484,548,555],[94,158,166,170,173,175,176,177,189,373,548,555],[94,158,166,170,173,175,176,177,189,259,280,285,286,350,351,352,353,354,356,369,370,372,375,443,446,497,499,548,555],[94,158,166,170,173,175,176,177,189,319,548,555],[94,158,166,170,172,173,175,176,177,189,324,494,548,555],[94,158,166,170,173,175,176,177,189,324,548,555],[94,158,166,170,172,173,175,176,177,189,246,292,321,323,325,443,494,501,503,548,555],[94,158,166,170,173,175,176,177,189,300,301,302,303,305,306,319,320,502,548,555],[91,94,158,166,170,172,173,175,176,177,187,189,206,235,236,248,254,286,287,290,389,441,442,444,494,497,498,501,548,555],[94,158,166,170,173,175,176,177,189,231,234,241,548,555],[94,158,166,170,173,175,176,177,189,285,287,419,422,548,555],[94,158,166,170,173,175,176,177,189,285,420,488,489,490,491,492,548,555],[94,158,166,170,172,173,175,176,177,189,281,497,548,555],[94,158,166,170,172,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,284,369,548,555],[94,158,166,170,173,175,176,177,189,283,548,555],[94,158,166,170,173,175,176,177,189,285,338,548,555],[94,158,166,170,173,175,176,177,189,282,284,497,548,555],[94,158,166,170,172,173,175,176,177,189,230,285,419,420,421,494,497,498,548,555],[85,94,158,166,170,173,175,176,177,189,234,240,318,548,555],[85,94,158,166,170,173,175,176,177,189,232,548,555],[94,158,166,170,173,175,176,177,189,222,223,548,555],[85,94,158,166,170,173,175,176,177,189,228,548,555],[85,94,158,166,170,173,175,176,177,189,234,304,548,555],[85,91,94,158,166,170,173,175,176,177,189,286,290,501,503,548,555],[94,158,166,170,173,175,176,177,189,228,525,526,548,555],[85,94,158,166,170,173,175,176,177,189,299,548,555],[85,94,158,166,170,173,175,176,177,187,189,206,226,293,295,297,298,503,548,555],[94,158,166,170,173,175,176,177,189,234,261,498,548,555],[94,158,166,170,173,175,176,177,189,234,426,548,555],[85,94,158,166,170,172,173,175,176,177,187,189,224,226,299,397,501,502,548,555],[85,94,158,166,170,173,175,176,177,189,215,216,217,218,219,501,546,548,555],[85,86,87,88,89,94,158,166,170,173,175,176,177,189,548,555],[94,158,166,170,173,175,176,177,189,392,393,394,548,555],[94,158,166,170,173,175,176,177,189,392,548,555],[85,89,94,158,166,170,172,173,174,175,176,177,187,189,214,215,216,217,218,219,220,226,254,259,436,464,499,500,503,546,548,555],[94,158,166,170,173,175,176,177,189,511,548,555],[94,158,166,170,173,175,176,177,189,513,548,555],[94,158,166,170,173,175,176,177,189,517,548,555],[94,158,166,170,173,175,176,177,189,519,548,555],[94,158,166,170,173,175,176,177,189,521,522,523,548,555],[94,158,166,170,173,175,176,177,189,527,548,555],[90,94,158,166,170,173,175,176,177,189,505,510,512,514,518,520,524,528,530,540,541,543,547,548,549,550,555],[94,158,166,170,173,175,176,177,189,529,548,555],[94,158,166,170,173,175,176,177,189,539,548,555],[94,158,166,170,173,175,176,177,189,295,548,555],[94,158,166,170,173,175,176,177,189,542,548,555],[94,157,158,166,170,173,175,176,177,189,285,419,420,422,488,489,491,492,544,546,548,555],[94,158,166,170,173,175,176,177,189,214,555],[94,158,166,170,173,175,176,177,189,548,555,669],[94,158,163,166,170,172,173,174,175,176,177,189,206,207,214,548,555,843],[94,158,166,170,173,175,176,177,189,548,555,768],[94,158,159,166,170,173,175,176,177,189,194,548,555,673,674,754,767],[94,158,166,170,173,175,176,177,189,548,555,769],[94,158,166,170,173,175,176,177,189,548,555,596],[94,158,166,170,173,175,176,177,189,548,555,594,596],[94,158,166,170,173,175,176,177,189,548,555,585,593,594,595,597,599],[94,158,166,170,173,175,176,177,189,548,555,583],[94,158,166,170,173,175,176,177,189,548,555,586,591,596,599],[94,158,166,170,173,175,176,177,189,548,555,582,599],[94,158,166,170,173,175,176,177,189,548,555,586,587,590,591,592,599],[94,158,166,170,173,175,176,177,189,548,555,586,587,588,590,591,599],[94,158,166,170,173,175,176,177,189,548,555,583,584,585,586,587,591,592,593,595,596,597,599],[94,158,166,170,173,175,176,177,189,548,555,599],[94,158,166,170,173,175,176,177,189,548,555,581,583,584,585,586,587,588,590,591,592,593,594,595,596,597,598],[94,158,166,170,173,175,176,177,189,548,555,581,599],[94,158,166,170,173,175,176,177,189,548,555,586,588,589,591,592,599],[94,158,166,170,173,175,176,177,189,548,555,590,599],[94,158,166,170,173,175,176,177,189,548,555,591,592,596,599],[94,158,166,170,173,175,176,177,189,548,555,584,594],[94,158,166,170,173,175,176,177,189,548,555,789],[94,158,166,170,173,175,176,177,189,548,555,1017],[94,158,166,170,173,175,176,177,189,548,555,1016],[85,94,158,166,170,173,175,176,177,189,548,555,1459],[94,158,166,170,173,175,176,177,189,548,555,996],[94,158,166,170,173,175,176,177,189,548,555,995,996,997,998],[94,158,166,170,173,175,176,177,189,548,555,567],[94,158,166,170,173,175,176,177,189,548,555,559,561,567],[94,158,166,170,173,175,176,177,189,548,555,560,561],[94,158,166,170,173,175,176,177,189,548,555,561,567,571],[94,158,166,170,173,175,176,177,189,548,555,560],[94,158,166,170,173,175,176,177,189,548,555,561,567],[94,158,166,170,173,175,176,177,189,548,555,559,560,561,566],[94,158,166,170,173,175,176,177,189,548,555,559,561],[94,158,166,170,173,175,176,177,189,548,555,560,561,573],[94,158,166,170,173,175,176,177,189,194,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,999],[94,158,159,166,170,173,175,176,177,180,189,194,197,548,555,1000,1001,1004,1009],[94,158,166,170,173,175,176,177,189,548,555,983],[94,158,166,170,171,173,175,176,177,189,548,555,982,984,985,1005,1006,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,1000,1009,1010,1560],[85,94,158,166,170,173,175,176,177,189,548,555,1009,1010,1560],[94,158,159,166,169,170,173,175,176,177,180,189,194,197,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,982,984,1009,1010,1560],[94,158,163,166,170,173,175,176,177,189,548,555,1009],[94,158,166,170,173,175,176,177,189,548,555,1010,1452,1453,1455,1549,1550,1559,1560],[85,94,158,166,170,172,173,175,176,177,179,189,548,555,980,981,982,984,985,1001,1005,1007,1008,1009,1010,1560],[94,158,166,170,173,175,176,177,189,548,555,1554],[94,109,112,115,116,158,166,170,173,175,176,177,189,206,548,555],[94,112,158,166,170,173,175,176,177,189,194,206,548,555],[94,112,116,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,189,194,548,555],[94,106,158,166,170,173,175,176,177,189,548,555],[94,110,158,166,170,173,175,176,177,189,548,555],[94,108,109,112,158,166,170,173,175,176,177,189,206,548,555],[94,158,166,170,173,175,176,177,179,189,203,548,555],[94,158,166,170,173,175,176,177,189,214,548,555],[94,106,158,166,170,173,175,176,177,189,214,548,555],[94,108,112,158,166,170,173,175,176,177,179,189,206,548,555],[94,103,104,105,107,111,158,166,169,170,173,175,176,177,189,194,206,548,555],[94,112,121,129,158,166,170,173,175,176,177,189,548,555],[94,104,110,158,166,170,173,175,176,177,189,548,555],[94,112,139,140,158,166,170,173,175,176,177,189,548,555],[94,104,107,112,158,166,170,173,175,176,177,189,197,206,214,548,555],[94,112,158,166,170,173,175,176,177,189,548,555],[94,108,112,158,166,170,173,175,176,177,189,206,548,555],[94,103,158,166,170,173,175,176,177,189,548,555],[94,106,107,108,110,111,112,113,114,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,140,141,142,143,144,158,166,170,173,175,176,177,189,548,555],[94,112,132,135,158,166,170,173,175,176,177,189,548,555],[94,112,121,122,123,158,166,170,173,175,176,177,189,548,555],[94,110,112,122,124,158,166,170,173,175,176,177,189,548,555],[94,111,158,166,170,173,175,176,177,189,548,555],[94,104,106,112,158,166,170,173,175,176,177,189,548,555],[94,112,116,122,124,158,166,170,173,175,176,177,189,548,555],[94,116,158,166,170,173,175,176,177,189,548,555],[94,110,112,115,158,166,170,173,175,176,177,189,206,548,555],[94,104,108,112,121,158,166,170,173,175,176,177,189,548,555],[94,112,132,158,166,170,173,175,176,177,189,548,555],[94,124,158,166,170,173,175,176,177,189,548,555],[94,104,108,112,116,158,166,170,173,175,176,177,189,548,555],[94,106,112,139,158,166,170,173,175,176,177,189,197,212,214,548,555],[94,158,166,170,173,175,176,177,189,548,555,608,774,1013],[94,158,166,170,173,175,176,177,189,548,555,556],[94,158,166,169,170,172,173,174,175,176,177,179,189,194,203,206,213,214,548,555,556,557,558,568,569,570,572,574,576,578,579,580,600,604,605,606,607,608],[94,158,166,170,173,175,176,177,189,548,555,556,557,558,575],[94,158,166,170,173,175,176,177,189,548,555,558],[94,158,166,170,173,175,176,177,189,548,555,577],[94,158,166,170,173,175,176,177,189,548,555,603],[94,158,166,170,173,175,176,177,189,548,555,568,579,608],[94,158,166,170,173,175,176,177,189,548,555,568,608],[94,158,166,170,173,175,176,177,189,548,555,658,663],[94,158,166,170,173,175,176,177,189,548,555,635,644,774],[94,158,166,170,173,175,176,177,189,548,555,609,610,612,613,614,616,618,619,622,623,629,643,644,655,657],[94,158,166,170,173,175,176,177,189,548,555,618,637,638,644],[94,158,166,170,173,175,176,177,189,548,555,618,619,626,644],[94,158,166,170,173,175,176,177,189,548,555,609,616,618,619,622,644],[94,158,166,170,173,175,176,177,189,548,555,576],[94,158,166,170,173,175,176,177,189,548,555,609,618,619,622,635,639,644],[94,158,166,170,173,175,176,177,189,548,555,608,666,774],[94,158,161,166,170,173,175,176,177,189,194,548,555,608,609,614,616,618,619,622,623,626,627,635,636,639,642,643,644,655,664,665,771,774],[94,158,166,170,173,175,176,177,189,548,555,576,618,619,622,644],[94,158,166,170,173,175,176,177,189,548,555,618,637,638,639,644],[94,158,166,170,173,175,176,177,189,548,555,576,618,623,624,625,644],[94,158,161,166,170,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,667,771,774],[94,158,161,166,170,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,771,774],[94,158,166,170,173,175,176,177,189,548,555,576,609,614,616,618,619,622,623,624,625,626,627,629,635,637,638,639,642,644,645,646,647,648,653],[94,158,161,166,170,172,173,175,176,177,189,194,548,555,576,608,609,614,616,618,619,622,623,624,625,626,627,635,636,637,638,639,642,643,644,655,664,665,666,667,668,670,771,774],[94,158,166,170,173,175,176,177,189,548,555,609,616,618,619,622,623,637,638,639,644,646],[94,158,166,170,173,175,176,177,189,548,555,654,661],[94,158,166,170,173,175,176,177,189,548,555,753],[94,158,166,170,173,175,176,177,189,548,555,755,756,767],[94,158,166,170,173,175,176,177,189,548,555,757,758],[94,158,166,170,173,175,176,177,189,548,555,755,756,757,759,760,765],[94,158,166,170,173,175,176,177,189,548,555,756,757],[94,158,166,170,173,175,176,177,189,548,555,765],[94,158,166,170,173,175,176,177,189,548,555,766],[94,158,166,170,173,175,176,177,189,548,555,757],[94,158,166,170,173,175,176,177,189,548,555,755,756,757,760,761,762,763,764],[94,158,166,170,173,175,176,177,189,548,555,744],[94,158,166,170,173,175,176,177,189,548,555,744,747],[94,158,166,170,173,175,176,177,189,548,555,679,739,742,744,745,746,747,748,749,750,751,752],[94,158,166,170,173,175,176,177,189,548,555,675,677,747],[94,158,166,170,173,175,176,177,189,548,555,744,745],[94,158,166,170,173,175,176,177,189,548,555,676,744,746],[94,158,166,170,173,175,176,177,189,548,555,677,679,681,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,679,681,683,684],[94,158,166,170,173,175,176,177,189,548,555,679,681,683],[94,158,166,170,173,175,176,177,189,548,555,676,679,681,682,684],[94,158,166,170,173,175,176,177,189,548,555,675,677,678,679,680,681,682,683,684,685,686,739,740,741,742,743],[94,158,166,170,173,175,176,177,189,548,555,675,677,678,681],[94,158,166,170,173,175,176,177,189,548,555,677,678,681],[94,158,166,170,173,175,176,177,189,548,555,681,684],[94,158,166,170,173,175,176,177,189,548,555,675,676,678,679,680,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,675,676,677,681,744],[94,158,166,170,173,175,176,177,189,548,555,681,682,683,684],[94,158,166,170,173,175,176,177,189,548,555,683],[94,158,166,170,173,175,176,177,189,548,555,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"27bdc30a0e32783366a5abeda841bc22757c1797de8681bbe81fbc735eeb1c10","impliedFormat":1},{"version":"8fd575e12870e9944c7e1d62e1f5a73fcf23dd8d3a321f2a2c74c20d022283fe","impliedFormat":1},{"version":"2ab096661c711e4a81cc464fa1e6feb929a54f5340b46b0a07ac6bbf857471f0","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"df83c2a6c73228b625b0beb6669c7ee2a09c914637e2d35170723ad49c0f5cd4","affectsGlobalScope":true,"impliedFormat":1},{"version":"436aaf437562f276ec2ddbee2f2cdedac7664c1e4c1d2c36839ddd582eeb3d0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e3c06ea092138bf9fa5e874a1fdbc9d54805d074bee1de31b99a11e2fec239d","affectsGlobalScope":true,"impliedFormat":1},{"version":"87dc0f382502f5bbce5129bdc0aea21e19a3abbc19259e0b43ae038a9fc4e326","affectsGlobalScope":true,"impliedFormat":1},{"version":"b1cb28af0c891c8c96b2d6b7be76bd394fddcfdb4709a20ba05a7c1605eea0f9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2fef54945a13095fdb9b84f705f2b5994597640c46afeb2ce78352fab4cb3279","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac77cb3e8c6d3565793eb90a8373ee8033146315a3dbead3bde8db5eaf5e5ec6","affectsGlobalScope":true,"impliedFormat":1},{"version":"56e4ed5aab5f5920980066a9409bfaf53e6d21d3f8d020c17e4de584d29600ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ece9f17b3866cc077099c73f4983bddbcb1dc7ddb943227f1ec070f529dedd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a6282c8827e4b9a95f4bf4f5c205673ada31b982f50572d27103df8ceb8013c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1c9319a09485199c1f7b0498f2988d6d2249793ef67edda49d1e584746be9032","affectsGlobalScope":true,"impliedFormat":1},{"version":"e3a2a0cee0f03ffdde24d89660eba2685bfbdeae955a6c67e8c4c9fd28928eeb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811c71eee4aa0ac5f7adf713323a5c41b0cf6c4e17367a34fbce379e12bbf0a4","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"60037901da1a425516449b9a20073aa03386cce92f7a1fd902d7602be3a7c2e9","affectsGlobalScope":true,"impliedFormat":1},{"version":"d4b1d2c51d058fc21ec2629fff7a76249dec2e36e12960ea056e3ef89174080f","affectsGlobalScope":true,"impliedFormat":1},{"version":"22adec94ef7047a6c9d1af3cb96be87a335908bf9ef386ae9fd50eeb37f44c47","affectsGlobalScope":true,"impliedFormat":1},{"version":"196cb558a13d4533a5163286f30b0509ce0210e4b316c56c38d4c0fd2fb38405","affectsGlobalScope":true,"impliedFormat":1},{"version":"73f78680d4c08509933daf80947902f6ff41b6230f94dd002ae372620adb0f60","affectsGlobalScope":true,"impliedFormat":1},{"version":"c5239f5c01bcfa9cd32f37c496cf19c61d69d37e48be9de612b541aac915805b","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"7e29f41b158de217f94cb9676bf9cbd0cd9b5a46e1985141ed36e075c52bf6ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"dc0a7f107690ee5cd8afc8dbf05c4df78085471ce16bdd9881642ec738bc81fe","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"21da358700a3893281ce0c517a7a30cbd46be020d9f0c3f2834d0a8ad1f5fc75","impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"0ccdaa19852d25ecd84eec365c3bfa16e7859cadecf6e9ca6d0dbbbee439743f","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc2110f7decca6bfb9392e30421cfa1436479e4a6756e8fec6cbc22625d4f881","affectsGlobalScope":true,"impliedFormat":1},{"version":"096116f8fedc1765d5bd6ef360c257b4a9048e5415054b3bf3c41b07f8951b0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e5e01375c9e124a83b52ee4b3244ed1a4d214a6cfb54ac73e164a823a4a7860a","affectsGlobalScope":true,"impliedFormat":1},{"version":"f90ae2bbce1505e67f2f6502392e318f5714bae82d2d969185c4a6cecc8af2fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"4b58e207b93a8f1c88bbf2a95ddc686ac83962b13830fe8ad3f404ffc7051fb4","affectsGlobalScope":true,"impliedFormat":1},{"version":"1fefabcb2b06736a66d2904074d56268753654805e829989a46a0161cd8412c5","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"c18a99f01eb788d849ad032b31cafd49de0b19e083fe775370834c5675d7df8e","affectsGlobalScope":true,"impliedFormat":1},{"version":"5247874c2a23b9a62d178ae84f2db6a1d54e6c9a2e7e057e178cc5eea13757fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"156a859e21ef3244d13afeeba4e49760a6afa035c149dda52f0c45ea8903b338","impliedFormat":1},{"version":"10ec5e82144dfac6f04fa5d1d6c11763b3e4dbbac6d99101427219ab3e2ae887","impliedFormat":1},{"version":"615754924717c0b1e293e083b83503c0a872717ad5aa60ed7f1a699eb1b4ea5c","impliedFormat":1},{"version":"14e9acf826baba0ef4b5665704084896e7bcc06f65a9ab13af7e93d27d6b7069","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"21adf13435b9b748529c8cedf80f884e5130b9684188120a686cd2b26a2059c7","impliedFormat":1},{"version":"eec76bf6b9346f3f95fa402621b889489e96930e72295b0369022f332e9b4a6a","impliedFormat":1},{"version":"0ecd58f413f9bc3b7d4383eae31b0c8fc576985cd7404d6f99f8c643543ade74","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"9c32412007b5662fd34a8eb04292fb5314ec370d7016d1c2fb8aa193c807fe22","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"4d327f7d72ad0918275cea3eee49a6a8dc8114ae1d5b7f3f5d0774de75f7439a","impliedFormat":1},{"version":"6ebe8ebb8659aaa9d1acbf3710d7dae3e923e97610238b9511c25dc39023a166","impliedFormat":1},{"version":"e85d7f8068f6a26710bff0cc8c0fc5e47f71089c3780fbede05857331d2ddec9","impliedFormat":1},{"version":"7befaf0e76b5671be1d47b77fcc65f2b0aad91cc26529df1904f4a7c46d216e9","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"d33ce35e3f9cfcc1d94eca415bdd3bde94d5b153ffdd33e6c4455c029986c630","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"8aee8b6d4f9f62cf3776cda1305fb18763e2aade7e13cea5bbe699112df85214","impliedFormat":1},{"version":"98498b101803bb3dde9f76a56e65c14b75db1cc8bec5f4db72be541570f74fc5","impliedFormat":1},{"version":"4dc59f6e1dbf3d5f66660fceabe6c174d3261b37b696ae1854f0dbaf255fc753","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"436d7b4543b340b0f3eef4310d524242e41369b9652aa9c70428767c4dcac455","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"12950411eeab8563b349cb7959543d92d8d02c289ed893d78499a19becb5a8cc","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"114f493b30f364255290472111b5a4791d5902c308645670cd0401429cbc6930","impliedFormat":1},{"version":"c3f5289820990ab66b70c7fb5b63cb674001009ff84b13de40619619a9c8175f","affectsGlobalScope":true,"impliedFormat":1},{"version":"b3275d55fac10b799c9546804126239baf020d220136163f763b55a74e50e750","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa68a0a3b7cb32c00e39ee3cd31f8f15b80cac97dce51b6ee7fc14a1e8deb30b","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"6c36e755bced82df7fb6ce8169265d0a7bb046ab4e2cb6d0da0cb72b22033e89","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7a93de4ff8a63bafe62ba86b89af1df0ccb5e40bb85b0c67d6bbcfdcf96bf3d4","affectsGlobalScope":true,"impliedFormat":1},{"version":"90e85f9bc549dfe2b5749b45fe734144e96cd5d04b38eae244028794e142a77e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0a5deeb610b2a50a6350bd23df6490036a1773a8a71d70f2f9549ab009e67ee","affectsGlobalScope":true,"impliedFormat":1},{"version":"d2ae155afe8a01cc0ae612d99117cf8ef16692ba7c4366590156fdec1bcf2d8c","impliedFormat":1},{"version":"3f5e5d9be35913db9fea42a63f3df0b7e3c8703b97670a2125587b4dbbd56d7c","impliedFormat":1},{"version":"8caeb65fdc3bfe0d13f86f67324fcb2d858ed1c55f1f0cce892eb1acfb9f3239","impliedFormat":1},{"version":"57c23df0b5f7a8e26363a3849b0bc7763f6b241207157c8e40089d1df4116f35","affectsGlobalScope":true,"impliedFormat":1},{"version":"3b8bc0c17b54081b0878673989216229e575d67a10874e84566a21025a2461ee","impliedFormat":1},{"version":"5b0db5a58b73498792a29bfebc333438e61906fef75da898b410e24e52229e6f","impliedFormat":1},{"version":"dbe055b2b29a7bab2c1ca8f259436306adb43f469dca7e639a02cd3695d3f621","impliedFormat":1},{"version":"1678b04557dca52feab73cc67610918a7f5e25bfdba3e7fa081acd625d93106d","impliedFormat":1},{"version":"e3905f6902f0b69e5eefc230daa69fdd4ab707a973ec2d086d65af1b3ea47ef0","impliedFormat":1},{"version":"2ea729503db9793f2691162fec3dd1118cab62e96d025f8eeb376d43ec293395","impliedFormat":1},{"version":"9ec87fea42b92894b0f209931a880789d43c3397d09dd99c631ae40a2f7071d1","impliedFormat":1},{"version":"c68e88cdfadfb6c8ba5fc38e58a3a166b0beae77b1f05b7d921150a32a5ffb8d","impliedFormat":1},{"version":"2bc7aa4fba46df0bd495425a7c8201437a7d465f83854fac859df2d67f664df3","impliedFormat":1},{"version":"41d17e1ad9a002feb11c8cdd2777e5bbc0cdb1e3f595d237e4dded0b6949983b","impliedFormat":1},{"version":"1fede9296beac11ce8e6b425396a1791f64341f2be85deebb6286faf6e16306e","affectsGlobalScope":true,"impliedFormat":1},{"version":"80219a97fd3ce5f91af5c355a264844027a899b3d9a3cd41cf6f3bc5947edc95","impliedFormat":1},{"version":"89444c76f16bf7994e230d98e1bc3f01d654de04ef02f60430d9a98d5b450a8b","impliedFormat":1},{"version":"0eb5d0cbf09de5d34542b977fd6a933bb2e0817bffe8e1a541b2f1ad1b9af1ff","impliedFormat":1},{"version":"fac3e88881b35d3a757ed891ac912b2674792c25e2a1a74e1f5fbc72d19a9792","impliedFormat":1},{"version":"2c2bdaa1d8ead9f68628d6d9d250e46ee8e81aa4898b4769a36956ae15e060fe","impliedFormat":1},{"version":"c32c840c62d8bd7aeb3147aa6754cd2d922b990a6b6634530cb2ebdce5adc8e9","impliedFormat":1},{"version":"e1c1a0b4d1ead0de9eca52203aeb1f771f21e6238d6fcd15aa56ac2a02f1b7bf","impliedFormat":1},{"version":"82b91e4e42e6c41bc7fc1b6c2dc5eba6a2ba98375eb1f210e6ff6bba2d54177e","impliedFormat":1},{"version":"6fe28249ac0c7bc19a79aa9264baf00efbd080e868dbe1d3052033ad1c64f206","affectsGlobalScope":true,"impliedFormat":1},{"version":"cbed824fec91efefc7bbdcb8b43d1a531fdbebd0e2ef19481501ff365a93cb70","impliedFormat":1},{"version":"d0716593b3f2b0451bcf0c24cfa86dec2235c325c89f201934248b7c742715fc","impliedFormat":1},{"version":"ec501101c2a96133a6c695f934c8f6642149cc728571b29cbb7b770984c1088e","impliedFormat":1},{"version":"b214ebcf76c51b115453f69729ee8aa7b7f8eccdae2a922b568a45c2d7ff52f7","impliedFormat":1},{"version":"429c9cdfa7d126255779efd7e6d9057ced2d69c81859bbab32073bad52e9ba76","impliedFormat":1},{"version":"2991bca2cc0f0628a278df2a2ccdb8d6cbcb700f3761abbed62bba137d5b1790","impliedFormat":1},{"version":"c51b3c3f6c5aa5b121124f4b593996826aab90667f95de88c1ff13c1736e11ba","affectsGlobalScope":true,"impliedFormat":1},{"version":"230763250f20449fa7b3c9273e1967adb0023dc890d4be1553faca658ee65971","impliedFormat":1},{"version":"c3e9078b60cb329d1221f5878e88cecfa3e74460550e605a58fcfb41a66029ff","impliedFormat":1},{"version":"a74edb3bab7394a9dbde529d60632be590def2f5f01024dbd85441587fbfbbe0","impliedFormat":1},{"version":"0ea59f7d3e51440baa64f429253759b106cfcbaf51e474cae606e02265b37cf8","impliedFormat":1},{"version":"bc18a1991ba681f03e13285fa1d7b99b03b67ee671b7bc936254467177543890","impliedFormat":1},{"version":"00049ccc87f3f37726db03c01ca68fe74fd9c0109b68c29eb9923ebec2c76b13","impliedFormat":1},{"version":"fa94bbf532b7af8f394b95fa310980d6e20bd2d4c871c6a6cb9f70f03750a44b","impliedFormat":1},{"version":"7fde0e1be5c8be204ffbf428abfcf01da2eb0f130e1bc3f539eb7275f4fd1f58","impliedFormat":1},{"version":"e284328553df5f425a5d33d36a0c3fa66b46af9d097cad6f4d2e8696dfdeb0f1","affectsGlobalScope":true,"impliedFormat":1},{"version":"7fa2214bb0d64701bc6f9ce8cde2fd2ff8c571e0b23065fa04a8a5a6beb91511","impliedFormat":1},{"version":"f1c93e046fb3d9b7f8249629f4b63dc068dd839b824dd0aa39a5e68476dc9420","impliedFormat":1},{"version":"016b29bf4926b80255a108c53a1451717350059da04fcae64d1075f5e93bbb39","impliedFormat":1},{"version":"841983e39bd4cbb463be385e92fda11057cab368bf27100a801c492f1d86cbaa","impliedFormat":1},{"version":"3856f7d31d0c47ec0dded3ec552519a3cd6639c1ad7be279dd1b31abffd8cc85","impliedFormat":1},{"version":"e16b319e5aca1031168de823c4946ff8e29629c4c8cc0ec0fcfe2a8ab2155043","impliedFormat":1},{"version":"e4156ddb25aa0e3b5303d372f26957b36778f0f6bbd4326359269873295e3058","affectsGlobalScope":true,"impliedFormat":1},{"version":"cc1b433a84cae05ddc5672d4823170af78606ad21ecef60dbc4570190cbf1357","impliedFormat":1},{"version":"9d3821bc75c59577e52643324cec92fc2145642e8d17cf7ee07a3181f21d985d","impliedFormat":1},{"version":"7f78cfb2b343838612c192cb251746e3a7c62ac7675726a47e130d9b213f6580","impliedFormat":1},{"version":"201db9cf1687fab1adf5282fcba861f382b32303dc4f67c89d59655e78a25461","impliedFormat":1},{"version":"2c3c5c0f54055e87640f5d233716fd889f3034fc7911d603b642369b0dbeb2a7","impliedFormat":1},{"version":"0a20eaf2e4b1e3c1e1f87f7bccb0c936375b23b022baeea750519b7c9bc6ce83","impliedFormat":1},{"version":"b484ec11ba00e3a2235562a41898d55372ccabe607986c6fa4f4aba72093749f","impliedFormat":1},{"version":"a16b91b27bd6b706c687c88cbc8a7d4ee98e5ed6043026d6b84bda923c0aed67","impliedFormat":1},{"version":"4ebf086fa2e5ef2b65133a944cb3f8ab518a22087727dfbfc802a3654c396f2f","impliedFormat":1},{"version":"99ab6d0d660ce4d21efb52288a39fd35bb3f556980ec5463b1ae8f304a3bbc85","impliedFormat":1},{"version":"6eeded8c7e352be6e0efb83f4935ec752513c4d22043b52522b90849a49a3a11","impliedFormat":1},{"version":"6c1ad90050ffbb151cacc68e2d06ea1a26a945659391e32651f5d42b86fd7f2c","impliedFormat":1},{"version":"55cdbeebe76a1fa18bbd7e7bf73350a2173926bd3085bb050cf5a5397025ee4e","impliedFormat":1},{"version":"2beff543f6e9a9701df88daeee3cdd70a34b4a1c11cb4c734472195a5cb2af54","impliedFormat":1},{"version":"2e07abf27aa06353d46f4448c0bbac73431f6065eef7113128a5cd804d0c384d","impliedFormat":1},{"version":"be1cc4d94ea60cbe567bc29ed479d42587bf1e6cba490f123d329976b0fe4ee5","impliedFormat":1},{"version":"42bc0e1a903408137c3df2b06dfd7e402cdab5bbfa5fcfb871b22ebfdb30bd0b","impliedFormat":1},{"version":"9894dafe342b976d251aac58e616ac6df8db91fb9d98934ff9dd103e9e82578f","impliedFormat":1},{"version":"413df52d4ea14472c2fa5bee62f7a40abd1eb49be0b9722ee01ee4e52e63beb2","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"446a50749b24d14deac6f8843e057a6355dd6437d1fac4f9e5ce4a5071f34bff","impliedFormat":1},{"version":"182e9fcbe08ac7c012e0a6e2b5798b4352470be29a64fdc114d23c2bab7d5106","impliedFormat":1},{"version":"2f4e6b4d39426a1b85ecf4bdeb9dddbf4d9b3397d95d8555d46f925c9519ec7d","impliedFormat":1},{"version":"78a2869ad0cbf3f9045dda08c0d4562b7e1b2bfe07b19e0db072f5c3c56e9584","impliedFormat":1},{"version":"89d5d28d4f57e000b836ac273079be1b75710e28ce14750d081fb420d37e2ca5","impliedFormat":1},{"version":"fd4e24ccff3966390600d7f5d6aa1fed5a512e92ada735ea5fbc933d313ad3d3","impliedFormat":1},{"version":"b7cddfe1aa6b86b5fad3c9ccb30d05b3ccb165aebbf112f48d2d8a5f69dd98b1","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"ad0d1d75d129b1c80f911be438d6b61bfa8703930a8ff2be2f0e1f8a91841c64","impliedFormat":1},{"version":"bd2c7ada3dee03653d3f601011d30072194bc3970cd93208f9588fbdc0c69347","impliedFormat":1},{"version":"e480da45d32313e7174b265674da504f075f59ef326852f0c5a5d863b438ae85","impliedFormat":1},{"version":"ad54850f61fcf5d014e11be80d2f46fea9265cfa7e77456da876f7833ef81769","impliedFormat":1},{"version":"6f7c9e8bd2b5b6a080b07080065f94900bd3c7e5ebbd3047bc33fcce2fab1dd8","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"da5950ee2a90721df6f3fba45f5d05308f7e4c35835392215dd2cd404505e2de","impliedFormat":1},{"version":"ce75b1aebb33d510ff28af960a9221410a3eaf7f18fc5f21f9404075fba77256","impliedFormat":1},{"version":"f42d5fed19610d485c646a0c430e768115567d078c7fc855c57b0c578b3d6cd3","impliedFormat":1},{"version":"ee8df1cb8d0faaca4013a1b442e99130769ce06f438d18d510fed95890067563","impliedFormat":1},{"version":"d5630f2ad9b4541e5ce891648121022f9412ecdca1820baa1f0104f70fd7eff7","impliedFormat":1},{"version":"4d15375ab13497104bc8fe56fdef2b5fd6853f29255737d23a33fa306ff7fd69","impliedFormat":1},{"version":"2cd3fc1d0d6a1e85baffd2d4f50f5efb192b5446eef567e97c94765402f0aad4","impliedFormat":1},{"version":"e4cbf2f1e89ecccaddd2c045e600ae41b732295953fb06247c7dcbc2d281ed30","impliedFormat":1},{"version":"6dcedaef57dff0d79a05ab0ab602cde74db803d1e765468bf91263786a383e1b","impliedFormat":1},{"version":"8c1697d90c394a6fd955b98eae01238eff628e129b987a68aea10f898a48e7da","impliedFormat":1},{"version":"7580e62139cb2b44a0270c8d01abcbfcba2819a02514a527342447fa69b34ef1","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"f374cb24e93e7798c4d9e83ff872fa52d2cdb36306392b840a6ddf46cb925cb6","impliedFormat":1},{"version":"d10d63718e1646c2279e3b33831f82c60e31f622b2b7020f1196409ca4c09242","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"148679c6d0f449210a96e7d2e562d589e56fcde87f843a92808b3ff103f1a774","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"02436d7e9ead85e09a2f8e27d5f47d9464bced31738dec138ca735390815c9f0","impliedFormat":1},{"version":"f8d5ff8eafd37499f2b6a98659dd9b45a321de186b8db6b6142faed0fea3de77","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"a22dd55aa4d39906252000ab8e8a1b83b195eef7f4274eb51e457c1f11cf6580","impliedFormat":1},{"version":"540cc83ab772a2c6bc509fe1354f314825b5dba3669efdfbe4693ecd3048e34f","impliedFormat":1},{"version":"121b0696021ab885c570bbeb331be8ad82c6efe2f3b93a6e63874901bebc13e3","impliedFormat":1},{"version":"612d9da66bb046a9c1e2e8d026245ded881fc4b9f98cbfae714415d57ee0ae0b","impliedFormat":1},{"version":"32c2ad9494dad5d11b0564a619fee18f388db6c1e9e2cd3c360b3122549691eb","impliedFormat":1},{"version":"6c301d40aec56a74ec7bd7324e31a728dadf9bfba3e96def02938d3d973534ec","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"aa14cee20aa0db79f8df101fc027d929aec10feb5b8a8da3b9af3895d05b7ba2","impliedFormat":1},{"version":"493c700ac3bd317177b2eb913805c87fe60d4e8af4fb39c41f04ba81fae7e170","impliedFormat":1},{"version":"aeb554d876c6b8c818da2e118d8b11e1e559adbe6bf606cc9a611c1b6c09f670","impliedFormat":1},{"version":"acf5a2ac47b59ca07afa9abbd2b31d001bf7448b041927befae2ea5b1951d9f9","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"d71291eff1e19d8762a908ba947e891af44749f3a2cbc5bd2ec4b72f72ea795f","impliedFormat":1},{"version":"c0480e03db4b816dff2682b347c95f2177699525c54e7e6f6aa8ded890b76be7","impliedFormat":1},{"version":"25a5f6fd3a2243c859eddc99ab5fba11d970af2fe7a5df9c32b7668f76f97b01","impliedFormat":1},{"version":"8d207e1f9d2c30d6f77dfa693f3827c3fbf0d89240297e10bdfe1041d433df68","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"d682336018141807fb602709e2d95a192828fcb8d5ba06dda3833a8ea98f69e3","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"4fbd3116e00ed3a6410499924b6403cc9367fdca303e34838129b328058ede40","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"8c70ddc0c22d85e56011d49fddfaae3405eb53d47b59327b9dd589e82df672e7","impliedFormat":1},{"version":"2f9c89cbb29d362290531b48880a4024f258c6033aaeb7e59fbc62db26819650","impliedFormat":1},{"version":"a365c4d3bed3be4e4e20793c999c51f5cd7e6792322f14650949d827fbcd170f","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"273782b8454e78f6a8b30d2cfbf6860499c930595095fcc1689637115f0eddda","affectsGlobalScope":true,"impliedFormat":1},{"version":"3fbdd025f9d4d820414417eeb4107ffa0078d454a033b506e22d3a23bc3d9c41","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a8f8e6ab2fa07b45251f403548b78eaf2022f3c2254df3dc186cb2671fe4996d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"9f9bb6755a8ce32d656ffa4763a8144aa4f274d6b69b59d7c32811031467216e","impliedFormat":1},{"version":"5c32bdfbd2d65e8fffbb9fbda04d7165e9181b08dad61154961852366deb7540","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"0c05e9842ec4f8b7bfebfd3ca61604bb8c914ba8da9b5337c4f25da427a005f2","impliedFormat":1},{"version":"faed7a5153215dbd6ebe76dfdcc0af0cfe760f7362bed43284be544308b114cf","impliedFormat":1},{"version":"7029e566b8df176f703fb59fd437a38670c7a0e02c58b2d66dfb5b2e2b2defdb","impliedFormat":1},{"version":"7f2aa4d4989a82530aaac3f72b3dceca90e9c25bee0b1a327e8a08a1262435ad","impliedFormat":1},{"version":"d96b39301d0ded3f1a27b47759676a33a02f6f5049bfcbde81e533fd10f50dcb","impliedFormat":1},{"version":"e9f147ecca73d9346a4c073432843c159ccbe50bdcb678a78f6da10eae2cecf4","impliedFormat":1},{"version":"de061f7d72bd65c06fc1419f841dfdcb29a8e22fe6fa527d1e6eb20b897d4de0","impliedFormat":1},{"version":"663beafc2446079574570cba86e9b15f986f908ddb1b01274509970126fee945","impliedFormat":1},{"version":"a3102887d5058bf4cb5b37fa6964c09e9527c42053b3b5c642b89878620748de","impliedFormat":1},{"version":"0aaaa1727edd29673d85c9b26d7ca4d54e5407a48586903c51b48b7f7d196f61","impliedFormat":1},{"version":"d35bca0b261bff02635758c48e8ab99c61c420d0dfabbcf467e847171d876b7d","impliedFormat":1},{"version":"3bc12c40d90c342ff88a3d876996c555ed5cbee5fe8c3308a240b321f401ee46","impliedFormat":1},{"version":"ba130768aae855a5477e9e148e5c879548e6e7ccbcc56fd1934c8a18ea5b7569","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d38530db0601215d6d767f280e3a3c54b2a83b709e8d9001acb6f61c67e965fc","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"b499af2054a037a162b3b72cd886f48bbf32a3502c865c6e29fac7d2ab3ce0b5","impliedFormat":1},{"version":"b83cb14474fa60c5f3ec660146b97d122f0735627f80d82dd03e8caa39b4388c","impliedFormat":1},{"version":"48773ca557b0319c2ee62ae249cf52a81709e8be139920d6479a66274de7c4ed","impliedFormat":1},{"version":"7274fbffbd7c9589d8d0ffba68157237afd5cecff1e99881ea3399127e60572f","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"8b4327413e5af38cd8cb97c59f48c3c866015d5d642f28518e3a891c469f240e","impliedFormat":1},{"version":"4cceef18d7f088e797a463e90b7a9dad10c6bc667724b7686e3e740ae00122be","impliedFormat":1},{"version":"7ee86fbb3754388e004de0ef9e6505485ddfb3be7640783d6d015711c03d302d","impliedFormat":1},{"version":"cc1954b539604b1e562319119ac7e888172208b32ca873f9a357a92c826bd046","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"bb8f2dbc03533abca2066ce4655c119bff353dd4514375beb93c08590c03e023","impliedFormat":1},{"version":"706dd95827e7ebaabda91d5db2b755233e0952d98570e9c032b0f066a15c1177","affectsGlobalScope":true,"impliedFormat":1},{"version":"0b103e9abfe82d14c0ad06a55d9f91d6747154ef7cacc73cf27ecad2bfb3afcf","impliedFormat":1},{"version":"cd9304972e6d616197fb44fce00540a904f38b54306a1951b5dbeaf3c01ab5bd","impliedFormat":1},{"version":"77438e2c397a3db78407621cfc57241a305b310ddea2c185f1d555248297f587","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"a6805fcafed712aea7759f8bc731014f9d22738c1d6ef9d43b8091d1d48346d5","impliedFormat":1},{"version":"c49469a5349b3cc1965710b5b0f98ed6c028686aa8450bcb3796728873eb923e","impliedFormat":1},{"version":"4a889f2c763edb4d55cb624257272ac10d04a1cad2ed2948b10ed4a7fda2a428","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"d88ea80a6447d7391f52352ec97e56b52ebec934a4a4af6e2464cfd8b39c3ba8","impliedFormat":1},{"version":"142617b3cdf902b69c6464c9fbd942b60ab3e733ca18c032b19e0f7e2adbefe8","impliedFormat":1},{"version":"0b603555f1881f87256ffd6344d3e3ed6d466c2e701eabf381f28be8c2125892","impliedFormat":1},{"version":"897e4f7662488e3ecc79e743bdd3b78f13bdb69a97851afa5b440c4211e32ea9","impliedFormat":1},{"version":"e2e1c6d3b2d93add5200bd7bc1a8cccb4e446836b2111ece45db8683a2c765de","impliedFormat":1},{"version":"251b03d5cd243854ce870d9a9a39f491faf69898c5d6b5eee28cc7649c57417b","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"2c4de79f406d137390608e8c0a44fba2ff8e00bacfcae7c9d1781fef10e9440d","impliedFormat":1},{"version":"07ba23a10465791be5d22deaf5ef7de7658774ddff53721e5ea17fedea1bc721","impliedFormat":1},{"version":"dca8c645c5afeb03b1ecedbf16323f33e7d0afaa6256c8e047e6e38087a97f53","impliedFormat":1},{"version":"775f181bd4a533d6f8b5e55ec1d9f1624559720ae8a70e9432258da26b38d27c","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"7715134a0cf07dd41a9da2895d708625a3a303a0385e355ecaaf0b8bfaef2550","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"622694a8522b46f6310c2a9b5d2530dde1e2854cb5829354e6d1ff8f371cf469","impliedFormat":1},{"version":"cd8ce8d68567f62dd580b3c3c37777ac3f5b81944c7417f5ea83030eab533385","impliedFormat":1},{"version":"e5c939d896565dcac0f6fbdbada11284e7728ef26a069561c09aa5aa4a788393","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9e6c0ff3f8186fccd05752cf75fc94e147c02645087ac6de5cc16403323d870","impliedFormat":1},{"version":"49af4b52f0d4d2304c5f2c6fe5fab3e153e0acc38830d0202821b877c097dd02","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"e68b8e5a1df7c1be2bc105141456ecba70215806e1c28bfbc5c12bfce4be6e68","impliedFormat":1},{"version":"511c8f02329808d47d00b859c532ae9115590048b17325a946c74dac48428650","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"b5f9e66625783eefcbe3d2da074b2e7ba2066d61ce3fc6ef4f22805ad946cab4","impliedFormat":1},{"version":"e37115962d284b9f7a37c2bdd2add50f88365dde41f5e0ff591ffc48a8ec7575","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"f89488602bec98a142072fae7ea5ba99431a569ff580c64b7be39896474799d8","impliedFormat":1},{"version":"bbbc47961f39a57df103cf4ca3bb8f8732b4b6678a18225a0aa76d59c466956c","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"2ffb043dc5163458e473b7010859f86e01dc4edffcae0a93d885d028b426a546","impliedFormat":1},{"version":"c8f004e6036aa1c764ad4ec543cf89a5c1893a9535c80ef3f2b653e370de45e6","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"b064c36f35de7387d71c599bfcf28875849a1dbc733e82bd26cae3d1cd060521","impliedFormat":1},{"version":"05c7280d72f3ed26f346cbe7cbbbb002fb7f15739197cbbee6ab3fd1a6cb9347","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"04b7b2e0832dfd3c31e81df3975e8d8fda28e7ff999b0aa2932608a8f6661d5c","impliedFormat":1},{"version":"ca2d34c6ed5cbd3070b8b6f32f42ae54adcc6499c1e4b99f0a5798b3f27cc653","impliedFormat":1},{"version":"9ec68995e66dd6b9dac834bf5ae85fde802714ea2e82151a5d1d53ef01b463ef","impliedFormat":1},{"version":"5c4d626b4902f2ef8a1cc146d761d276cef988016dc674e3b98fbad70e64bc9f","impliedFormat":1},{"version":"fdfaa0aad899524962e2955287b5b991ffe3be50f64e02eb60c933ca44644a94","impliedFormat":1},{"version":"53c972a0f9bc3a4ec70fff7314123ea8cfcf75b3703046f767d2dc1eea87b2fb","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"7d73b24e7bf31dfb8a931ca6c4245f6bb0814dfae17e4b60c9e194a631fe5f7b","impliedFormat":1},{"version":"d130c5f73768de51402351d5dc7d1b36eaec980ca697846e53156e4ea9911476","impliedFormat":1},{"version":"413586add0cfe7369b64979d4ec2ed56c3f771c0667fbde1bf1f10063ede0b08","impliedFormat":1},{"version":"06472528e998d152375ad3bd8ebcb69ff4694fd8d2effaf60a9d9f25a37a097a","impliedFormat":1},{"version":"7303b45138d2511035056a5901a1490ebdcbf055cbb1276f8629c5121cbe733e","impliedFormat":1},{"version":"27f874cd5327507eeff699a74567f60c1215b94509f4308633a7b01922471ed2","impliedFormat":1},{"version":"a401617604fa1f6ce437b81689563dfdc377069e4c58465dbd8d16069aede0a5","impliedFormat":1},{"version":"2c6cf04bc525caf6546e859e8ef10bfb9573837ec0bc5ec7b53a7b1b8ca72781","impliedFormat":1},{"version":"8695dec09ad439b0ceef3776ea68a232e381135b516878f0901ed2ea114fd0fe","impliedFormat":1},{"version":"304b44b1e97dd4c94697c3313df89a578dca4930a104454c99863f1784a54357","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"87cc05fe13108f02e12da7e3efd8e360fef78d96a0c9e11408ea1b1b9fb3e03d","impliedFormat":1},{"version":"1abbf67c218d23c2ce76887caac2df6c7dab3d97ba2b65348432b876f510002a","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"c06ef3b2569b1c1ad99fcd7fe5fba8d466e2619da5375dfa940a94e0feea899b","impliedFormat":1},{"version":"f7d628893c9fa52ba3ab01bcb5e79191636c4331ee5667ecc6373cbccff8ae12","impliedFormat":1},{"version":"1d879125d1ec570bf04bc1f362fdbe0cb538315c7ac4bcfcdf0c1e9670846aa6","impliedFormat":1},{"version":"dad97c99382889e9c7d1a9d8275500ff71235130fae9f8916fdbf3641d56e592","impliedFormat":1},{"version":"a6dba407fc287f1e25454e75028c91bbc00675f2d1c4e8b3edcc36c08611a486","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"e91f7b1344577a02f051b9b471f33044fef8334a76dc9e1de003d17595a5219b","impliedFormat":1},{"version":"c0723195c85e19656d6b5b9fdb81d3f3403c1ae4679e722c6ea058c516b38d12","impliedFormat":1},{"version":"b55eb9f72166093b5460d34b34f5d8699c968de3bc3fc696e40f2c93f2ebf650","impliedFormat":1},{"version":"71d9eb4c4e99456b78ae182fb20a5dfc20eb1667f091dbb9335b3c017dd1c783","impliedFormat":1},{"version":"cfa846a7b7847a1d973605fbb8c91f47f3a0f0643c18ac05c47077ebc72e71c7","impliedFormat":1},{"version":"1594da19968752a22b2ac48c2d0e60575700e745c577a8a4a676b841238ad5bb","impliedFormat":1},{"version":"e0cee12109e0a10a4c3d6769fcc7644b7c1ea7f52365bea51728f5af29f8a137","impliedFormat":1},{"version":"7d4254b4c6c67a29d5e7f65e67d72540480ac2cfb041ca484847f5ae70480b62","impliedFormat":1},{"version":"3536968defef8a75514f547ead5e2e9c1e984820290ec9b00c5fdfb6ef786535","impliedFormat":1},{"version":"d83773870080c30a230e322ce13a9c6f3398e8dacea4ea8a83e26370f3bac23e","impliedFormat":1},{"version":"dcfeaf98d66314fec29a9076c4290e45d0b196a65827becc19138e9c7b855f37","impliedFormat":1},{"version":"6849fe9210fe4946d5f085bfed36758f33dc6ae15a751338d178dd4daa017c46","impliedFormat":1},{"version":"888cda0fa66d7f74e985a3f7b1af1f64b8ff03eb3d5e80d051c3cbdeb7f32ab7","impliedFormat":1},{"version":"60681e13f3545be5e9477acb752b741eae6eaf4cc01658a25ec05bff8b82a2ef","impliedFormat":1},{"version":"ffae4e1e06aa848a1e4bcef162cd1c48e5909b26223515981310af9c036bdfc7","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"34e16eb7c31768a11a08aebcfb3d70d7b8f0b016197e98d8419e566ceae6d6c8","impliedFormat":1},{"version":"f94ec1f7e4b709d26960306c9082a7a1b728a6e13089346aa48ba57c74cbf47e","impliedFormat":1},{"version":"9a11cb4033405e96c247cd5aa29790212aaffdd127869e8a5219103f0b389fd5","impliedFormat":1},{"version":"01479d9d5a5dda16d529b91811375187f61a06e74be294a35ecce77e0b9e8d6c","impliedFormat":1},{"version":"aff5213585cb72e94054dfe17250ff315f3569b3919d1ef1ad235f37c4ee894e","impliedFormat":1},{"version":"fb2ea35e1be6388d722d7725e2b49c697d34d9c890c3b96758faaeb86d35cef8","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"1a4dc28334a926d90ba6a2d811ba0ff6c22775fcc13679521f034c124269fd40","impliedFormat":1},{"version":"f05315ff85714f0b87cc0b54bcd3dde2716e5a6b99aedcc19cad02bf2403e08c","impliedFormat":1},{"version":"5fad3b31fc17a5bc58095118a8b160f5260964787c52e7eb51e3d4fcf5d4a6f0","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"456006a6975b26c0a1785feddae165f6d307e2d601ffde27e21fc4a790e448a4","impliedFormat":1},{"version":"c857e0aae3f5f444abd791ec81206020fbcc1223e187316677e026d1c1d6fe08","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"1fe0d18b111e1145a7e7601855bccd4ca20f24e3b9a5aba6bb1fa9d1a7059170","impliedFormat":1},{"version":"5632c3c26d420c063eebe64c45b1248b9492a67bf44f1d0c57e9dc8f6cf449bb","impliedFormat":1},{"version":"0df5aa619ab12993a39ea6dae062ee46eadbb4d738916460e636ada52bced75b","impliedFormat":1},{"version":"8fca3039857709484e5893c05c1f9126ab7451fa6c29e19bb8c2411a2e937345","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"10ab7be91f87ebe8916b62cf28af2e45b5601fc7b0e311adf838f912c6b31dd8","impliedFormat":1},{"version":"bc636fbc08e0979ceb7eb0731a33000283d77a33b62e1f71ee65be50394e40ba","impliedFormat":1},{"version":"7e0b7f91c5ab6e33f511efc640d36e6f933510b11be24f98836a20a2dc914c2d","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"75bbd3be047d539988a0ff0b56384ef7a6a25f3b676ad96bee547d44c31622a7","impliedFormat":1},{"version":"42960001a776b089ade681ab5cfddc936e0afb0615133ec1841f3dee89d3e1bf","impliedFormat":1},{"version":"0aedb02516baf3e66b2c1db9fef50666d6ed257edac0f866ea32f1aa05aa474f","impliedFormat":1},{"version":"da47712b394d944328245482603bc6f416d3949b67c9392279caab595076b510","affectsGlobalScope":true,"impliedFormat":1},{"version":"37d0071d8f0a06dc55c2c5e0ec3391affd4fd107c53410bf358196ec0bf3923f","impliedFormat":1},{"version":"b213dad76ca37fd552274c9499056e1c0d9c1bd38a55bb7f68b22ba6b84c3ad7","impliedFormat":1},{"version":"56ccb49443bfb72e5952f7012f0de1a8679f9f75fc93a5c1ac0bafb28725fc5f","impliedFormat":1},{"version":"20fa37b636fdcc1746ea0738f733d0aed17890d1cd7cb1b2f37010222c23f13e","impliedFormat":1},{"version":"d90b9f1520366d713a73bd30c5a9eb0040d0fb6076aff370796bc776fd705943","impliedFormat":1},{"version":"bc03c3c352f689e38c0ddd50c39b1e65d59273991bfc8858a9e3c0ebb79c023b","impliedFormat":1},{"version":"19df3488557c2fc9b4d8f0bac0fd20fb59aa19dec67c81f93813951a81a867f8","affectsGlobalScope":true,"impliedFormat":1},{"version":"b25350193e103ae90423c5418ddb0ad1168dc9c393c9295ef34980b990030617","affectsGlobalScope":true,"impliedFormat":1},{"version":"bef86adb77316505c6b471da1d9b8c9e428867c2566270e8894d4d773a1c4dc2","impliedFormat":1},{"version":"5a49adaef698b7ad7e6127949fa1b0bbd3d46b7cbd11c54e392a4dcdd51f5190","impliedFormat":1},{"version":"6ee598cdfdd0fa52039dca135b3dfff7b49035dc13292143e0a93843e3861967","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"2489bf04d77dc025ba67f49f1a56eb24b9db477d5ff88123d887e163ed1776aa","impliedFormat":1},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"0b77b819b5417775fccb20c678293cf614c054a5b1a65421a5b933a9124ba998","impliedFormat":1},{"version":"eb5acb58487367e502d994b57e2c58255d8241f481ea8efa8e79af23af3f41c2","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"b1f1d57fde8247599731b24a733395c880a6561ec0c882efaaf20d7df968c5af","impliedFormat":1},{"version":"6715dc4eb59c8ea9abe2b78c235ed331dc710a06fe56798868dbc4d40cd1b707","impliedFormat":1},{"version":"35e6379c3f7cb27b111ad4c1aa69538fd8e788ab737b8ff7596a1b40e96f4f90","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"5a3ea721d03a361ccbdd7390ccd75f6e84cbca3a3f01f4b331ecc9af31890c49","impliedFormat":1},{"version":"e7dfaee4af38d45b1cab8a1ee0b3bc1f85ddcf64545ed391d675d78ae6526274","affectsGlobalScope":true,"impliedFormat":1},{"version":"e8daa443eaf9a27fd382cc1f8ebe30330c0f4d89511cfb469166874806751d35","impliedFormat":1},{"version":"af48e58339188d5737b608d41411a9c054685413d8ae88b8c1d0d9bfabdf6e7e","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"1de8c302fd35220d8f29dea378a4ae45199dc8ff83ca9923aca1400f2b28848a","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"332248ee37cca52903572e66c11bef755ccc6e235835e63d3c3e60ddda3e9b93","impliedFormat":1},{"version":"94e8cc88ae2ef3d920bb3bdc369f48436db123aa2dc07f683309ad8c9968a1e1","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"320f4091e33548b554d2214ce5fc31c96631b513dffa806e2e3a60766c8c49d9","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"d90d5f524de38889d1e1dbc2aeef00060d779f8688c02766ddb9ca195e4a713d","impliedFormat":1},{"version":"07ed3ddab975995eea41b22f3010506fb9f5fb301d04820b07d7a1aee5477d7c","impliedFormat":1},{"version":"969d8b0965849f4bae7cab0ba90bd1e1220e95999c2c6f01117fa7500901c017","impliedFormat":1},{"version":"6ec840ee5e2bc103f557fe38b1d585ee250540468713d7634ee066de372bf332","impliedFormat":1},{"version":"b0309e1eda99a9e76f87c18992d9c3689b0938266242835dd4611f2b69efe456","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"6ceb10ca57943be87ff9debe978f4ab73593c0c85ee802c051a93fc96aaf7a20","impliedFormat":1},{"version":"1de3ffe0cc28a9fe2ac761ece075826836b5a02f340b412510a59ba1d41a505a","impliedFormat":1},{"version":"e46d6cc08d243d8d0d83986f609d830991f00450fb234f5b2f861648c42dc0d8","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"ff863d17c6c659440f7c5c536e4db7762d8c2565547b2608f36b798a743606ca","impliedFormat":1},{"version":"5412ad0043cd60d1f1406fc12cb4fb987e9a734decbdd4db6f6acf71791e36fe","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"e297c0a524edee7677939122f90027bfbe5f2698939d9a85728e5044b39c7124","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"bc9ee0192f056b3d5527bcd78dc3f9e527a9ba2bdc0a2c296fbc9027147df4b2","impliedFormat":1},{"version":"b62381cae176db34f003cc6172ee8f3e0122014889d66391aa73698105cf4934","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"30d8da250766efa99490fc02801047c2c6d72dd0da1bba6581c7e80d1d8842a4","impliedFormat":1},{"version":"03566202f5553bd2d9de22dfab0c61aa163cabb64f0223c08431fb3fc8f70280","impliedFormat":1},{"version":"41eb514d9ce0a6e87957f08a4b7af70d93f87637f37dee706e2d92a6601c25a9","impliedFormat":1},{"version":"e7765aa8bcb74a38b3230d212b4547686eb9796621ffb4367a104451c3f9614f","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"7bd01f0f28cd3aeb2046274d85208e245965f6f2948edf4f7b2057bcf9f22ccc","impliedFormat":99},{"version":"d2f2cf2b8cc92bea913cda4a076e0f790b23a21e84f989d12f0116a7fe3906e0","impliedFormat":99},{"version":"6de125ea94866c736c6d58d68eb15272cf7d1020a5b459fea1c660027eca9a90","affectsGlobalScope":true,"impliedFormat":1},{"version":"f5b20bc288ee49989c95b20847fc93b96bf61cc0845598897a6a53a967dd7d07","affectsGlobalScope":true,"impliedFormat":1},{"version":"064ac1c2ac4b2867c2ceaa74bbdce0cb6a4c16e7c31a6497097159c18f74aa7c","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"d3b315763d91265d6b0e7e7fa93cfdb8a80ce7cdd2d9f55ba0f37a22db00bdb8","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},{"version":"b8fd21e843bf2d078a96fdd1df42f22aa48b246ca368f78f856ef91ba372c830","affectsGlobalScope":true},"7b550dda9686c16f36a17bf9051d5dbf31e98555b30d114ac49fc49a1e712651","68937fe95088ffe97ac986edf4dcfb88cfe71df2e67db91ba03ee165779dedd7",{"version":"3b89216a7e38a454985ad17bb2ff85792837dc812f2a89fa5f60ad0a2e216fa7","impliedFormat":99},{"version":"16fe60bb544cfedfd2b5bb2f7d0b3957be7978706d57d9f06edc9c0c8dbdba23","impliedFormat":99},{"version":"de4a612aa8f1704af486f701e21993c786ba7d8cfed55fffa6684026aea2346e","impliedFormat":99},{"version":"c73fdf42528325dd17940937ed787b15ae3445c6a2dae1a2b74bc4d87d337ca2","impliedFormat":99},{"version":"e8e17dfef3cfa9f0847ac93dd535a9896af7fb57c1a1b164484bb1b0ee4a25d8","impliedFormat":99},{"version":"51d2ffea2d1ee4a81c775938588c1e16620281adb60cbc26579a2fc6baa10bd2","impliedFormat":99},{"version":"148debd12783ded0a60d115daeacd8136f77757ae89a05c4e18de6dd77646fd2","impliedFormat":99},{"version":"0088b02dca63c47b273a140d0a3944bdc6dc2eb765fff0ca98e3c3a2786b3a5a","impliedFormat":99},{"version":"a651d06b780fa354231f19b040cbcde484bede3218885752b4f9e9a8f72d3b5f","impliedFormat":99},{"version":"06e26f75bed4c8389a8a63f0e6d6a9068038873dc95d8d1338e8c370a0ae8bc3","impliedFormat":99},{"version":"a2155e2675fd1af52b0b70779371c28611cdd1076b29d0f68bf93b983e5ddce0","impliedFormat":99},{"version":"4a61989c1daaed8bd7a2c7d87f9fae8d98f1205b3de3b3131478f972d997380b","impliedFormat":99},{"version":"8543104cedbc17231ef94fe02530b612c466d436be7201a8a94f65694cb7875d","impliedFormat":99},{"version":"c074e054c9db79055d37d7d70131e9a3234b8186773b3edb617c13f80bcf8774","impliedFormat":99},{"version":"7d3e062a778b8f5ea4f0cac7e925e31f88e6739812ebc5f827474324a4048f14","impliedFormat":99},{"version":"aa26f7bd84a3ea2b59408071accf147ba8cb0d5991aabf1b9ba22d66ce36086e","impliedFormat":99},{"version":"5c3c3eb0a2b0ac00106e768c4920c56f2629545e9376649eed9671057469586a","impliedFormat":99},{"version":"5a9834c603c65aee5cba0c1d6b3c7aee85cdc7862832a23165c6aa4139c165f2","impliedFormat":99},{"version":"a40c35037b336bca6ffdd1b3e1384b1c1ac867a3f1588f70d1d6d6f821f49385","impliedFormat":99},{"version":"4e003c868b0d8f8ad200b96cbc653e18e513fa23e1c19c4fe3cc25d4394efc47","impliedFormat":99},{"version":"8887e70871f697fa42ad7cdf32168db60ec2d6760c173c97973e35377fe5d83b","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"e0864480ea083087d705f9405bd6bf59b795e8474c3447f0d6413b2bce535a09","impliedFormat":99},{"version":"e67cbea16f1994af89efd700542dbf3828a46a52b29e4d67e801bd7869dc103c","impliedFormat":99},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":99},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"3fd8a5aefd8c3feb3936ca66f5aa89dff7bf6e6537b4158dbd0f6e0d65ed3b9e","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"41c41c6e90133bb2a14f7561f29944771886e5535945b2b372e2f6ed6987746e","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":99},{"version":"a0e59ab6a42cc19a9d9fe4bd8eaacb9de9a12e25d4bf5027bc721df36f1a842e","impliedFormat":99},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":99},{"version":"be836d4f889a447168231c052f4fd5f3dc34538fc158d293120106a13dd2c8cb","impliedFormat":99},{"version":"3a582c6e8906f5b094ccf0de6cc6f4f8a54b05a34f52517aba5c9c7f704f6b28","impliedFormat":99},{"version":"0528f6d21f7a02d4092895090d2dd86104bd5a3e79eced96d5a1a7dd90943d17","impliedFormat":99},{"version":"b5ce343886d23392be9c8280e9f24a87f1d7d3667f6672c2fe4aa61fa4ece7d4","impliedFormat":99},{"version":"72ce5b734c05da85c85a6f6dc05823b051d6aa41acaedeeb1d17c72f3b4efa72","impliedFormat":99},{"version":"b0857bb28fd5236ace84280f79a25093f919fd0eff13e47cc26ea03de60a7294","impliedFormat":99},{"version":"5e43e0824f10cd8c48e7a8c5c673638488925a12c31f0f9e0957965c290eb14c","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"49ab4f1d153a252779958fc87b700743d32b5ffa42addd70ae23ad3f429daa5c","impliedFormat":99},{"version":"53cf4076f42b29b8d411259d168d51b3a0274c42c8814e5b44dfa8803a35d4fc","impliedFormat":99},{"version":"a39461ee1f27cf3e6cfd63d21045713d26d521da55ea4d8efccb705f689e6dbb","impliedFormat":99},{"version":"61bb64660ee150f3ab618340e15cca0a81664801bede7c966ca0eca3a952fe63","impliedFormat":99},{"version":"42a12f2faa483c9b48195ed794d22698162274e755f6e07219c2351c4f08d732","impliedFormat":99},{"version":"ec0c42bb0f465e4993f2bc68a6ce9df9a2dcbc7b83e21748f82f1b69561938e3","impliedFormat":99},{"version":"f50ff37a9cbbe74475f426474d9827083c7c2c138a954d28f1690df338f69291","impliedFormat":99},{"version":"61fd6c17235d530c40f543dd7c40afab091d91c1ef890baeed30db6d82b04b28","impliedFormat":99},{"version":"bcbd3becd08b4515225880abea0dbfbbf0d1181ce3af8f18f72f61edbe4febfb","impliedFormat":99},{"version":"091767bc841f937654ed597d49e023ed59850355e746ae1a6f20ab31076ee1fb","impliedFormat":99},{"version":"19c6d6135af59693698d384050b45a8a049493500add442f58e4bd7c8a255ab6","impliedFormat":99},{"version":"6a0dba12d55314638a8c51108b20fe2f68f1364a619d098918bda91c22dec154","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"4ead13a482c539b77394b2a97e3b877b809eac596390371cea490286f53b996a","impliedFormat":99},{"version":"06db2f8ba1d1dfacf04529cb731081ab23f133f29c7608ebdfbcab356996827c","impliedFormat":99},{"version":"bdd14f07b4eca0b4b5203b85b8dbc4d084c749fa590bee5ea613e1641dcd3b29","impliedFormat":99},{"version":"427fe2004642504828c1476d0af4270e6ad4db6de78c0b5da3e4c5ca95052a99","impliedFormat":1},{"version":"2eeffcee5c1661ddca53353929558037b8cf305ffb86a803512982f99bcab50d","impliedFormat":99},{"version":"9afb4cb864d297e4092a79ee2871b5d3143ea14153f62ef0bb04ede25f432030","affectsGlobalScope":true,"impliedFormat":99},{"version":"5c935b7fc4ddc1410ea1cd7cd4e35ed106a6e4920dd27a9480a40fd224359dc3","affectsGlobalScope":true,"impliedFormat":99},{"version":"8124828a11be7db984fcdab052fd4ff756b18edcfa8d71118b55388176210923","impliedFormat":99},{"version":"2b39c6cf59088713babbfc3e20ee85f1375d40e66953156fa658346b8346f24f","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"6987dfb4b0c4e02112cc4e548e7a77b3d9ddfeffa8c8a2db13ceac361a4567d9","impliedFormat":99},{"version":"5e2ba3d18d78aebbde1f34bde356e41e9c76eeaeaeee56a37036596a9eff4211","impliedFormat":99},{"version":"8280ae8ccc0493b32d1742d585357ab9f0a508ea050af25a5a20d64010d0a5cf","impliedFormat":99},{"version":"7adfd9f9056ecd4ae6c65fde2a98654960c662714c73f048478959d04c09e144","impliedFormat":99},{"version":"437b7613a30a2fcde463f7b707c6d5567a8823fbc51de50b8641bf5b1d126fad","impliedFormat":99},{"version":"4ffba3c5848b4fe62ee59b754fd5f256ad9656a0db6d37b9a2a8cb40dfc7ac21","impliedFormat":99},{"version":"c76c02846ba7d40b9b3488f0e8d75d02cbdee2f0bc5fcd55dd3bd2e1457646ea","impliedFormat":99},{"version":"32b35cf0dc3a1b1a7118b61c34ce2ad1a29695851679f9ec34e0776f2ece2a69","impliedFormat":99},{"version":"b413fbc6658fe2774f8bf9a15cf4c53e586fc38a2d5256b3b9647da242c14389","impliedFormat":99},{"version":"59e5e964b84fdb2378e9455e4e59405030e4ed2b4c6f891ce395f17796af3cbb","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"90ba95a763101bb61b8a799731a2ed60b5016b8135c1a2d5186862d4b534d4a1","impliedFormat":99},{"version":"63ea959e28c110923f495576e614fb8b36c09b6828b467b2c7cd7f03b03ccf9f","impliedFormat":99},{"version":"1601a95dbb33059fc3d12638ed2a9aecff899e339c5c0f3a0b28768866d385b4","impliedFormat":99},{"version":"56fc978580577d30f4c2cdb5b1eb9217b66ed66537dd27141256f426e4b8dd68","impliedFormat":99},{"version":"2c5413050a2580becf9d82dd7e3006b95623e96f145356bf73230cd635352f70","impliedFormat":99},{"version":"7cd92bc6808967f175063fce65f8d1d60a07d752b79ed20aa2eba9a2b8ed0820","impliedFormat":99},{"version":"59de9e07114b1da6421bf15359a951f1b762cb19c3a6abfc2e671a5ac2599dff","impliedFormat":99},{"version":"84e977ab810a75dc103b38cad1e019d3ab8f4ec9042b8f09f117b7d18c4076f0","impliedFormat":99},{"version":"9b98988fc27cbf7f08598871f275999540bcc267a8a47453866a198f93601959","impliedFormat":99},{"version":"2b1795d67cf9aa20167c72d795d4db6badea9cea646d9181869b5ca08c778479","impliedFormat":99},{"version":"860bedc71ead192ea4a0ea5ef4686e65724d14b391ebd1a6671a7044e6bd8e15","impliedFormat":99},{"version":"7c0a845bee4a084cbb8654709f48e5f13e2f6d45e5e2dde7c57cadf79fd9e3d5","impliedFormat":99},{"version":"a9c8214ef27f5762c807802fd468b50fafae5c909d4376b67b3bfccaf8f99e13","impliedFormat":99},{"version":"94c8f4fa20a287054630c9521a9c5c7e8f74e806bcc8444503b6dc378853dd09","impliedFormat":99},{"version":"bf5b6e99ad75e9caaad2a66b4cc05e8ab73c7ec0ba58525eb6be7baa49be0cd2","impliedFormat":99},{"version":"632711a108accd4533a327ac3b60d0518fa536dfdc2d9e146222c16aa348506e","impliedFormat":99},{"version":"81b5530dfc39e1946dd771dd5526b84710e11d6a371812b1dc2e8eef9e12aa61","impliedFormat":99},{"version":"5d0aaba829cea9511506bc2bcda7255ae1764366b8a7809cbd6dc220531f360f","impliedFormat":99},{"version":"cd71a7eb14e09b7962e8955c6b0376be99044d814e7d60d7d1e9d095716b89f3","impliedFormat":99},{"version":"681cf9768031b874fafdd81a20e8a20ca6c92a5cbf12e86532a69182e315b065","impliedFormat":1},{"version":"32727845ab5bd8a9ef3e4844c567c09f6d418fcf0f90d381c00652a6f23e7f6e","impliedFormat":1},{"version":"c1a2e05eb6d7ca8d7e4a7f4c93ccf0c2857e842a64c98eaee4d85841ee9855e6","impliedFormat":1},{"version":"835fb2909ce458740fb4a49fc61709896c6864f5ce3db7f0a88f06c720d74d02","impliedFormat":1},{"version":"6e5857f38aa297a859cab4ec891408659218a5a2610cd317b6dcbef9979459cc","impliedFormat":1},{"version":"ead8e39c2e11891f286b06ae2aa71f208b1802661fcdb2425cffa4f494a68854","impliedFormat":1},{"version":"40ba6c32eb732a09e4446ade5cb6ad0c147f186f9c9dc6878b90b4418ad9f6ea","impliedFormat":1},{"version":"fdd814741843f85c98281522c58f5a646590ba9019fad2efaa95987655e0611b","impliedFormat":1},{"version":"c78aff4fb58b28b8f642d5095fc7eeb79f00e652a67caa19693af1adabb833c9","impliedFormat":1},{"version":"f80a08ced8818dc99359c0acd5b3f12762e1ce53758007759b0d4e503cbf4a5e","impliedFormat":1},{"version":"37935fa7564bcc6e0bc845b766a24391098d26f7c8245d6e8ab37bc016816e94","impliedFormat":1},{"version":"68add36d9632bc096d7245d24d6b0b8ad5f125183016102a3dad4c9c2438ccb0","impliedFormat":1},{"version":"3a819c2928ee06bbcc84e2797fd3558ae2ebb7e0ed8d87f71732fb2e2acc87b4","impliedFormat":1},{"version":"0f8a263f4c8595c8a07de52e3f3927640c44386c1aa2984de9eae50d75e613b2","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"e0bfe601a9fdf6defe94ed62dc60ac71597566001a1f86e705c95e431a9c816d","impliedFormat":1},{"version":"346fffde7c32da87c2196eb7494422449dc2ca82d3b4e6bf55be1d1a33ffc2b0","impliedFormat":1},{"version":"add0ce7b77ba5b308492fa68f77f24d1ed1d9148534bdf05ac17c30763fc1a79","impliedFormat":1},{"version":"8b5875e4958528042103fdd775e106a7f76bafc29709f0690df9a7d2241d52a7","impliedFormat":1},{"version":"2f67911e4bf4e0717dc2ded248ce2d5e4398d945ee13889a6852c1233ea41508","impliedFormat":1},{"version":"d8430c275b0f59417ea8e173cfb888a4477b430ec35b595bf734f3ec7a7d729f","impliedFormat":1},{"version":"69364df1c776372d7df1fb46a6cb3a6bf7f55e700f533a104e3f9d70a32bec18","impliedFormat":1},{"version":"6042774c61ece4ba77b3bf375f15942eb054675b7957882a00c22c0e4fe5865c","impliedFormat":1},{"version":"5a3bd57ed7a9d9afef74c75f77fce79ba3c786401af9810cdf45907c4e93f30e","impliedFormat":1},{"version":"aef26cf95593c8ace1c62c4724f9afac77bdfa756fb8a00613cd152117cb2f43","impliedFormat":1},{"version":"30db853bb2e60170ba11e39ab48bacecb32d06d4def89eedf17e58ebab762a65","impliedFormat":1},{"version":"e27451b24234dfed45f6cf22112a04955183a99c42a2691fb4936d63cfe42761","impliedFormat":1},{"version":"2316301dd223d31962d917999acf8e543e0119c5d24ec984c9f22cb23247160c","impliedFormat":1},{"version":"58d65a2803c3b6629b0e18c8bf1bc883a686fcf0333230dd0151ab6e85b74307","impliedFormat":1},{"version":"e818471014c77c103330aee11f00a7a00b37b35500b53ea6f337aefacd6174c9","impliedFormat":1},{"version":"268fd6d9f2e807a39a6c5aa654b00f949feb63d3faa7dd0f9bba7dde9172159c","impliedFormat":1},{"version":"29f823cbe0166e10e7176a94afe609a24b9e5af3858628c541ff8ce1727023cd","impliedFormat":1},{"version":"d3cfde44f8089768ebb08098c96d01ca260b88bccf238d55eee93f1c620ff5a5","impliedFormat":1},{"version":"293eadad9dead44c6fd1db6de552663c33f215c55a1bfa2802a1bceed88ff0ec","impliedFormat":1},{"version":"36eb5babc665b890786550d4a8cb20ef7105673a6d5551fbdd7012877bb26942","impliedFormat":1},{"version":"fec412ded391a7239ef58f455278154b62939370309c1fed322293d98c8796a6","impliedFormat":1},{"version":"e3498cf5e428e6c6b9e97bd88736f26d6cf147dedbfa5a8ad3ed8e05e059af8a","impliedFormat":1},{"version":"dba3f34531fd9b1b6e072928b6f885aa4d28dd6789cbd0e93563d43f4b62da53","impliedFormat":1},{"version":"f672c876c1a04a223cf2023b3d91e8a52bb1544c576b81bf64a8fec82be9969c","impliedFormat":1},{"version":"e4b03ddcf8563b1c0aee782a185286ed85a255ce8a30df8453aade2188bbc904","impliedFormat":1},{"version":"2329d90062487e1eaca87b5e06abcbbeeecf80a82f65f949fd332cfcf824b87b","impliedFormat":1},{"version":"25b3f581e12ede11e5739f57a86e8668fbc0124f6649506def306cad2c59d262","impliedFormat":1},{"version":"93c3e73824ad57f98fd23b39335dbdae2db0bd98199b0dc0b9ccc60bf3c5134a","impliedFormat":1},{"version":"a9ebb67d6bbead6044b43714b50dcb77b8f7541ffe803046fdec1714c1eba206","impliedFormat":1},{"version":"833e92c058d033cde3f29a6c7603f517001d1ddd8020bc94d2067a3bc69b2a8e","impliedFormat":1},{"version":"e5b810845dedef5aa0d682efa61be20dcb2e28c27a64ff5cc4cfc122f3f3980f","impliedFormat":1},{"version":"7a8ec10b0834eb7183e4bfcd929838ac77583828e343211bb73676d1e47f6f01","impliedFormat":1},{"version":"b07f64ff6ec710998a62b07377fbda0ab4c313ba1f0055bfe9faa22cffedd47c","impliedFormat":1},{"version":"72ef861333c7870a29fc0e248129c4afc0741c0716a74c3da409d05e41ff1322","impliedFormat":99},"49641a5eaa0ab31c12d312d53b724213afccbbbcc8e43a1119479d5e71858e61",{"version":"168aa44771e55cda628eeb5fbb6f626f6f263d827967b119e8c06abd7f1362ec","signature":"435a1e418e8338be3f39614b96b81a9aa2700bc8c27bc6b98f064ff9ce17c363"},{"version":"174e5bf2d8c80c676eb42959eaac341892bd946f18b68d2f5efe99a38b152fb3","impliedFormat":99},{"version":"7bbff6783e96c691a41a7cf12dd5486b8166a01b0c57d071dbcfca55c9525ec4","impliedFormat":99},{"version":"54460343eae2288e42a041a096da60709dc8150684cb79679770b68a46a33601","impliedFormat":99},{"version":"2faebfa830ae4cfbfb58e48b0ec20a2a63882d776f0ca36ec7155d45cf1b7f2d","impliedFormat":99},{"version":"dc6e3ce502651ecc523590588213bfb986c04b60f5265d37f76662b824425903","impliedFormat":99},{"version":"10a7c14e787e6b77f7079b80a8013f8349b37d115673f59bb7d5c17616df4e09","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"ae77d81a5541a8abb938a0efedf9ac4bea36fb3a24cc28cfa11c598863aba571","impliedFormat":1},{"version":"f329dfad7970297cbf07ddc8fce2ad4a24e2a3855917c661922ef86eb24dd1f1","impliedFormat":1},{"version":"841784cfa9046a2b3e453d638ea5c3e53680eb8225a45db1c13813f6ea4095e5","affectsGlobalScope":true,"impliedFormat":1},{"version":"646ef1cff0ec3cf8e96adb1848357788f244b217345944c2be2942a62764b771","impliedFormat":1},{"version":"3cfb7c0c642b19fb75132154040bb7cd840f0002f9955b14154e69611b9b3f81","impliedFormat":1},{"version":"8387ec1601cf6b8948672537cf8d430431ba0d87b1f9537b4597c1ab8d3ade5b","impliedFormat":1},{"version":"d16f1c460b1ca9158e030fdf3641e1de11135e0c7169d3e8cf17cc4cc35d5e64","impliedFormat":1},{"version":"a934063af84f8117b8ce51851c1af2b76efe960aa4c7b48d0343a1b15c01aedf","impliedFormat":1},{"version":"e3c5ad476eb2fca8505aee5bdfdf9bf11760df5d0f9545db23f12a5c4d72a718","impliedFormat":1},{"version":"462bccdf75fcafc1ae8c30400c9425e1a4681db5d605d1a0edb4f990a54d8094","impliedFormat":1},{"version":"5923d8facbac6ecf7c84739a5c701a57af94a6f6648d6229a6c768cf28f0f8cb","impliedFormat":1},{"version":"d0570ce419fb38287e7b39c910b468becb5b2278cf33b1000a3d3e82a46ecae2","impliedFormat":1},{"version":"3aca7f4260dad9dcc0a0333654cb3cde6664d34a553ec06c953bce11151764d7","impliedFormat":1},{"version":"a0a6f0095f25f08a7129bc4d7cb8438039ec422dc341218d274e1e5131115988","impliedFormat":1},{"version":"b58f396fe4cfe5a0e4d594996bc8c1bfe25496fbc66cf169d41ac3c139418c77","impliedFormat":1},{"version":"45785e608b3d380c79e21957a6d1467e1206ac0281644e43e8ed6498808ace72","impliedFormat":1},{"version":"bece27602416508ba946868ad34d09997911016dbd6893fb884633017f74e2c5","impliedFormat":1},{"version":"2a90177ebaef25de89351de964c2c601ab54d6e3a157cba60d9cd3eaf5a5ee1a","impliedFormat":1},{"version":"82200e963d3c767976a5a9f41ecf8c65eca14a6b33dcbe00214fcbe959698c46","impliedFormat":1},{"version":"b4966c503c08bbd9e834037a8ab60e5f53c5fd1092e8873c4a1c344806acdab2","impliedFormat":1},{"version":"3d3208d0f061e4836dd5f144425781c172987c430f7eaee483fadaa3c5780f9f","impliedFormat":1},{"version":"34a8a5b4c21e7a6d07d3b6bce72371da300ec1aed58961067e13f1f4dc849712","impliedFormat":1},{"version":"ad763fa0c24ede2b818eb6598c12dd581451f94688fb9ed963beba20d513a7ec","signature":"90ec9100c29e008c3d9194acd818e2cfa6dc6e177154bc8e10c5959aa35619ed"},{"version":"779a08ff2ae8855cf826d32062335e0d09d86ca4ee4b288b2cec6dd8d5ec338c","signature":"2ce3ed9a3d1ebf72e4a45464c2407c21c315fc9fa491ef2b02f3608b07450a4a"},{"version":"32ffc97bd9635972ee63e77eff2ef2839aeb2e842822a492136d822f70681655","signature":"83eb632f831cb93e76e3e2cc7d8f4917d3893481ce5a15751bd0685330881ad9"},{"version":"a756b102aea002f51675c9febd4c26527ef3086bc672951b654830fdbef92373","signature":"21aacd1a7a2350329abc64f2a4578e9603ea963d797fafb8672981192c61c18b"},{"version":"7c8545a7c4ec6978ebb4af07475d76007eb084c4f7f45aeba7ee817eeb4316e1","signature":"37eeb4730a8634d70c51bd1933939e66cba31f30600246487f3568cee7742b9c"},{"version":"243d22ed8f1d2e7fa0d99e4f25a87b791ebf16769fb48de846c9ac34ed07903e","signature":"c7ebc7a24a51984ec8231424b974dbbacc456090c53301dd1d8502ed95083a1b"},{"version":"62fa0410d02bd18aa492a8c465834a3bb8691e9478205041e297c08f30d57f46","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"464f35f5b7856d95d7277dc3472e8edc4c8a232b12601b667da28c44f98e0294","signature":"27c3eb3de9a7dd7e04b74bf3e630702bd7946ac349ae0ae66174d26591b25ead"},{"version":"b7ca2f47522d4ea41e65ff92c4c6dd9c4c8260da7c456a7631a9c88dc056b4d0","impliedFormat":1},{"version":"4f01e4d0959f9125b89e5737eb1ca2bfa69fd6b7d6126eba22feb8b505b00cde","impliedFormat":1},{"version":"4363a1adb9c77f2ed1ca383a41fbab1afadd35d485c018b2f84e834edde6a2c7","impliedFormat":1},{"version":"1d6458533adb99938d041a93e73c51d6c00e65f84724e9585e3cc8940b25523f","impliedFormat":1},{"version":"b0878fbd194bdc4d49fc9c42bfeeb25650842fe1412c88e283dc80854b019768","impliedFormat":1},{"version":"a892ea0b88d9d19281e99d61baba3155200acced679b8af290f86f695b589b16","impliedFormat":1},{"version":"03b42e83b3bcdf5973d28641d72b81979e3ce200318e4b46feb8347a1828cd5d","impliedFormat":1},{"version":"8a3d57426cd8fb0d59f6ca86f62e05dde8bfd769de3ba45a1a4b2265d84bac5a","impliedFormat":1},{"version":"afc6e1f323b476fdf274e61dab70f26550a1be2353e061ab34e6eed180d349b6","impliedFormat":1},{"version":"7c14483430d839976481fe42e26207f5092f797e1a4190823086f02cd09c113c","impliedFormat":1},{"version":"828a3bea78921789cbd015e968b5b09b671f19b1c14c4bbf3490b58fbf7d6841","impliedFormat":1},{"version":"69759c42e48938a714ee2f002fe5679a7ab56f0b5f29d571e4c31a5398d038fe","impliedFormat":1},{"version":"6e5e666fa6adeb60774b576084eeff65181a40443166f0a46ae9ba0829300fcb","impliedFormat":1},{"version":"1a4d43bdc0f2e240395fd204e597349411c1141dd08f5114c37d6268c3c9d577","impliedFormat":1},{"version":"874e58f8d945c7ac25599128a40ec9615aa67546e91ca12cbf12f97f6baf54ff","impliedFormat":1},{"version":"da2627da8d01662eb137ccd84af7ffa8c94cf2b2547d4970f17802324e54defc","impliedFormat":1},{"version":"07af06b740c01ed0473ebdd3f2911c8e4f5ebf4094291d31db7c1ab24ff559aa","impliedFormat":1},{"version":"ba1450574b1962fcf595fc53362b4d684c76603da5f45b44bc4c7eeed5de045b","impliedFormat":1},{"version":"b7903668ee9558d758c64c15d66a89ed328fee5ac629b2077415f0b6ca2f41bc","impliedFormat":1},{"version":"c7628425ee3076c4530b4074f7d48f012577a59f5ddade39cea236d6405c36ba","impliedFormat":1},{"version":"28c8aff998cc623ab0864a26e2eb1a31da8eb04e59f31fa80f02ec78eb225bcd","impliedFormat":1},{"version":"78d542989bdf7b6ba5410d5a884c0ab5ec54aa9ce46916d34267f885fcf65270","impliedFormat":1},{"version":"4d95060af2775a3a86db5ab47ca7a0ed146d1f6f13e71d96f7ac3b321718a832","impliedFormat":1},{"version":"6708cd298541a89c2abf66cceffc6c661f8ee31c013f98ddb58d2ec4407d0876","impliedFormat":1},{"version":"2e90928c29c445563409d89a834662c2ba6a660204fb3d4dc181914e77f8e29d","impliedFormat":1},{"version":"84be1b8b8011c2aab613901b83309d017d57f6e1c2450dfda11f7b107953286a","impliedFormat":1},{"version":"d7af890ef486b4734d206a66b215ebc09f6743b7fb2f3c79f2fb8716d1912d27","impliedFormat":1},{"version":"7e82c1d070c866eaf448ac7f820403d4e1b86112de582901178906317efc35ad","impliedFormat":1},{"version":"c5c4f547338457f4e8e2bec09f661af14ee6e157c7dc711ccca321ab476dbc6d","impliedFormat":1},{"version":"223e233cb645b44fa058320425293e68c5c00744920fc31f55f7df37b32f11ad","impliedFormat":1},{"version":"1394fe4da1ab8ab3ea2f2b0fcbfd7ccbb8f65f5581f98d10b037c91194141b03","impliedFormat":1},{"version":"086d9e59a579981bdf4f3bfa6e8e893570e5005f7219292bf7d90c153066cdfc","impliedFormat":1},{"version":"1ea59d0d71022de8ea1c98a3f88d452ad5701c7f85e74ddaa0b3b9a34ed0e81c","impliedFormat":1},{"version":"cd66a32437a555f7eb63490509a038d1122467f77fe7a114986186d156363215","impliedFormat":1},{"version":"f53d243499acfacc46e882bbf0bf1ae93ecea350e6c22066a062520b94055e47","impliedFormat":1},{"version":"65522e30a02d2720811b11b658c976bff99b553436d99bafd80944acba5b33b4","impliedFormat":1},{"version":"76b3244ec0b2f5b09b4ebf0c7419260813820f128d2b592b07ea59622038e45c","impliedFormat":1},{"version":"66eb7e876b49beff61e33f746f87b6e586382b49f3de21d54d41313aadb27ee6","impliedFormat":1},{"version":"69e8dc4b276b4d431f5517cd6507f209669691c9fb2f97933e7dbd5619fd07b7","impliedFormat":1},{"version":"361a647c06cec2e7437fa5d7cdf07a0dcce3247d93fbf3b6de1dc75139ff5700","impliedFormat":1},{"version":"fe5726291be816d0c89213057cd0c411bb9e39e315ed7e1987adc873f0e26856","impliedFormat":1},{"version":"1b76990de23762eb038e8d80b3f9c810974a7ed2335caa97262c5b752760f11a","impliedFormat":1},{"version":"5e050e05fe99cd06f2d4ad70e73aa4a72961d0df99525e9cad4a78fa588f387b","impliedFormat":1},{"version":"4ff327e8b16da9d54347b548f85675e35a1dc1076f2c22b2858e276771010dd2","impliedFormat":1},{"version":"f767787945b5c51c0c488f50b3b3aeb2804dfd2ddafcb61125d8d8857c339f5a","impliedFormat":1},{"version":"14ab21a9aeff5710d1d1262459a6d49fb42bed835aa0f4cfc36b75aa36faddcd","impliedFormat":1},{"version":"ba3c4682491b477c63716864a035b2cfdd727e64ec3a61f2ca0c9af3c0116cfd","affectsGlobalScope":true,"impliedFormat":1},{"version":"b222d32836d745e1e021bb10f6a0f4a562dd42206203060a8539a6b9f16523f0","impliedFormat":1},{"version":"a3f6d8995864820a0207b7ef4ce1ed6a8dd2fccc7e70d015da15034807c38e1c","impliedFormat":1},{"version":"651df11341eff0b769fb83af75b1872e6cedf406674c5eaa2650551aceb5a816","impliedFormat":1},{"version":"7a1afeb09118e443bc3c62031eacd17ff9bc7ad27ad410dd7ea9e2fdff7c0b02","signature":"f15b4a91c10bf30ff3708a5e3968c0a52ca4e86a4b9a5b1c4fc8e9b5f1292f21"},{"version":"11917eb01fea24107d69b3de1cc31e0e3266b9cad477f41b03a69cc02480d27b","signature":"c788a956032e3d31d6b5f98a301941197e46d04ae68ad49a403f353405037607"},{"version":"7fe9d657e96c9a7c00c2b4b661558719f371995052348a27e47ecae052f329dc","signature":"f8da50cd80c40c61a7cccdcb36106cb8221bd19d4525d30d75575526310f35c2"},{"version":"1e9d4c3b066e0228765cef074a9bf49d0b8d3af461c97f2c511e8f7110b56235","signature":"e59faabf094dd75dcf08847ca1b8ae16daf269ca02c744c521b38ed5d297578c"},{"version":"9e8f19cd697c9fdfeadb3559f0ca26514075f7054ae1ba17e8fbc8684d12eb5a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9dfe98d745dfc706198852e032c3c936092a4484951872c0d18639cf2ab698cf","signature":"326009db2b0a0f3290cbde2271f91e51869148a3e286ccc3ac41559f6c830642"},{"version":"44310bdd7b563dd9b2df0e1b0c45a789492e6620b4718778fca88e029f1765f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"030523c514236148e0f25975ef74b2490c391cb9596cd668728f7db5b0702e08","signature":"411166ca21d7ae80b737bb1c7e9900e897ac7b27c06647063730ba8207522e46"},{"version":"4109b1fb0cffaf1f2bcaaecb49378d7971b795502484bc2dbe0d4deae60c8057","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ca52a606c31ddaae5f7c4055fccb3563cf7e85601c96dbf28b7d0795d2d21af0","signature":"82d82e5e9d2c282a1dddebbcc73d9ff5c89f60859cd8769d0f6eae7b76f3f4d1"},{"version":"edadad7db67a0db1a7d474ab075cc15b975d78780218a1860e14516208d7389c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78b3214dba4e86c41e4cdf5de5dcc853806ff74fad5df3dc1087cdbe19ad00e0","signature":"5c4de1b33c2b0c5b0b823fca5ba9e5dde3c995aa8a2c42db12b2e8b64f408805"},{"version":"37702123f9a2c93d39aeeec210394811ee157b6e32b600c5e083d271370f7733","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"45b82fd88270dd3d0aff39220b341eca789f77c82813dc6695b2058479d8dc28","signature":"3d67e5bdef7b1039301b51fc5ba5303c133b863f30e0de39c8aa1db5d897f1b6"},{"version":"15631c00468c358645d55b6267edf935ab0e94dea8748d619ae23a76f7388912","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f212078a0ecefabbbbc627b89e28c871b559c1c57a4dcae32c8d7c2b6c4f0a00","signature":"b495b6770d46b4ebe3dbe0c1f6d64e8b50ea14ea8d2b63377813ae35672a4535"},{"version":"ec4e1ceba158e76dce8afd51f6b13caa6b67fe8e9d0c0686448b77fb76586adf","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9376f1759d1a75cabbab4521ccd44d0ca28e6b99c53820622d1b95cf759796c3","signature":"cff79d4609413671705c5c6a3938e666996270f808bc0f7306a97322fd16220c"},{"version":"a3dd5bfebf022e5ba282b11bb4eb12ee3946347b3c722cf9a63fd9212a2460bb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b562002637e45c4b3fd1a788528c278413d425c3402e0595539490fa670dc56a","signature":"41ab7d48373752fdda9cb29625f8e6caf1aa2092e6ea95b52f1275a2241193d6"},{"version":"355c388a5063eded314867e35c8071e8c8b1bb497d8332ecc0ae3d560193a74f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"5521661fe646b6d63cea776daf417bfe4093d24f190401ad3ebbc95ceb9157db","signature":"3cf58c6e2f986bec77722cf989aaf33509dd005c81cc2d52d40f76f08d75bc84"},{"version":"69ca880a3a93b31b209252e06877f4a79f9a1fc97d546257492c0734ebaea3f5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a9a302a8cd4d0c581404b58535f90adcf072b9e3a2f5a09c31ddaae37ccf673e","signature":"c5d60e5f8e3d05ba1fb0a462cd5ccbd26c7eb4c5406441d8803362183520748c"},{"version":"fdaacbce1a58559bce1d428a5bb445dab6f82a051a66d516ebf3d00265571caa","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cadbf01db107d2e9cceb5ecdd3f8f0084d996f07f2fca714409247deb26f0b85","signature":"ed905f29ff05cd1d3cf260949314ca896356f5c8bc011fdee9d75cde0404c6a0"},{"version":"a1d1b662c8d1e1d8a20612adb2c4cecd718f3b0e57d283cad95afbee8a61d132","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ea7a4796f350344bbf39b19518d78169bd66a819f787b0a4d1a6ad642dbce7a2","signature":"8f6ef9414b1d23dc0a927160ed27770f93c1fe749a9af0e626f5a0e8918d5fc1"},{"version":"1941ed0a7c9ff9106612c8fcc6ce92f24d656769a5d739599e4a6c34ad478df0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cc5bfa36a8f86eea23a84b78f27ba0f842426cefb5a13d4b53b80a331aaaa268","signature":"3dcde6c75b6c7fc3bc7fca2ca1273467974a8b54061edd648ed854bc4c5d842a"},{"version":"246ed9157ed5791bffc9037a1aa2a1b709a0fc75e67102d13e71d899dbbb5e0a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"27494de6a730122abf7b860cc02bf6ce805359bdcdf09095d6e9c9c63418aba2","signature":"959594ee2bcc74404b29a3049c85d0c7f2ae48df7e2b725160708a4a725fda88"},{"version":"5c7afbe31fb1e68671c798cfc06221cbade2f174e243566cf5b8fd0763e4ed52","signature":"233c0355b5b37601f3d2cb2fb9b35829c28220fbf3acc7314eb1d67509c3a2ce"},{"version":"433609291756b990304a258d06f4cf73d1a677643a8053d3a99fb435c668a4bd","signature":"9b3d204f4e4f901d253d5847771729af0cb7c39742f7be0ad468623b34612284"},{"version":"8062a9596b370d008b1cd13429487a2ed9d35b28dca9a0f0aa78609fd0a96f9d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fcc8fb0967eda9256921f3cfc521d49f3d473232d4c754c09de1d50faa996e35","signature":"37e053a2b54ed97cab1c0e0e6a0ed4d610d6011b24e4cef7633007fb573468e1"},{"version":"b28e8eddf4e5bc7659dd8369e31a71ee482d55ecfa6b84831fcc80228518b701","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"21382d3d9886a24696afabde6ba98ac0f2caedc6e62d848be109d4e2025bbf80","signature":"1cb1eafc264cf66f6de9548387f23915f3ae8d9c9d09a61e2fb253f96bac2397"},{"version":"1b04df451640794168e4fffb092f9823a6694c11d3daf55b4994f847a2987322","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fac17c78b79e957a330460096e2c511a77d7c22835a137659c599427ee68fb5e","signature":"c9b3903dbe7bb883044f93988644ad26177b48141ba87185d1961e89dcbe12e1"},{"version":"662d45c385c6903b15921917388cbcbf6fe34fa1720ab9c073e628496af5e56d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"50fd5c95036fcdd7101e2cdd2a336fe0d11b9cbfdcbd474d74a917c355ebc7ea","signature":"fc1013b6b18ebb6c273407c0999e92a60c29dd73779f14f7177a2635eb2cfceb"},{"version":"72121c537d221078e87c97dab842628674750a9d6aa2cf1b6166cfa664ed7382","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ef6f6d882e73aeae8a7cce4a2492cbf0e4829fc963e79312e443a7a16a6cfb3f","signature":"3ef82075343927dd481ff965c2af0f6ed87106013a8e27638a4df73e242a6592"},{"version":"c8d012f3fdd6a52336f10bec7e4509ea2dbb0ed1f2e996eac4b1eede14f9f88d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f59d410bcaf6b219caf38e6e9a39866eabbcc1106f74ad50c914e7e40cb4a24","signature":"0df22e821771646f47f81523421585e601758795cd91a75d41f4979d1f4519de"},{"version":"c71ae4fc28d6b2f5b78a95af5335129851cac857d19b25d5f06c8b014d3feea4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6bbcb670c4632ba69c7910572999840ac47a10cb49a1c437c5a52a40cbcdf10b","signature":"7df0381f3f4101bc88fad9da0c420421e97430cef4742bb04cbf41cdadb1da6f"},{"version":"8b59281611e7ebf3a35207733d85f8b50a33cdb5d08492334db32622e0cd3212","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"044b593c01d82184a7e0fb5c2ac98447d44ae217f1405f239c3e1e62ae379abd","signature":"922164a6b539bf53726ce54aac6bce9f648e478e5708604f3987fac02099f050"},{"version":"ab88aa6a3eb5f4f91c1a097e6b6002d35bb3b7517e1ce49c742a96d203758de2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1008e6f116f8169e368d70feccb510f8400b9fb204a699cd25cfeee835c88cc6","signature":"b0a0d651b6a03d5b1ef718277284d2b4a765def58e205311d2f1b825eea30c64"},{"version":"4455c74ed4fddb9ae9ba8eb37188fa934b6cf7fcd005053ddfb7dae7e5a6f072","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3eb6dafdd02c962b39b09be9e31c695603b58c794e141d4eed2f49318f0af433","signature":"1b21d73c687df72e347c8f38b456988260af0b5423ac03d9aa92966719676d87"},{"version":"9b44504aab0385b4934bf502c8c06a5540c50a0090a34f4c2fb50e43e03f5505","signature":"eb8914c7f3c2b49544d286aff55eba63c69d013385296fb3ad07e586e76000be"},{"version":"1f08dcb29ad688246447d04e3b7e2d5dcf65311c36c09db4bce1bca316f0201c","signature":"35ad6df8b0834debb0585c63d2bb84bb81802a7a0424a8f4ce6b0acb0513619f"},{"version":"61cb83a110e7a89d2ad5a0a4bd91fadb619619a98a16437189a856b51e1e9e0d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a794572bfb0885ec1de20b701e498a415c0480e4fb6c20dbaa125b374ae691cb","signature":"46d3c81ba82aaaeabab3baa813cbb84b4c9808ec7ad495ad5fdc27d9358aa314"},{"version":"1b5be2e53f79f1f20967a235c011841152b2ca9f79b3b0640468ba8a8cf5cd7c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"016e4353e0414c98c2ed2eb75df1509a38b457ddf000e9ff98125f82ea42c335","signature":"767c0245753b6e027a6f0550eb7b69bbd09d3f758aa1e9a07dcfe0992c0f629e"},{"version":"f76cdd7b076354cf26a7414ce5ebcef0a2a3384fdb6e406d4976c522349b6c93","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3ef6579bdd5883302539b5370c5670331f3584dff9e2ef14bab39991f00a29cb","signature":"1e894cd9840e407527a3f67119371f90d7ae5f8179905de7776a345ed4af6d32"},{"version":"325d7dc15c789cc6358327f36e7cf2a916e969017c2b78decf9475af91dc86c3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3b642738f6cd019494d207949a63b67b6ad9013bb3f1fb5f8fa36687879db5c4","signature":"f583f64899de9c7804febba31f2808aad09101b0d3342e42793c8588ed608f63"},{"version":"492bb2bed9c43e010cd0a50263969a75a72c6279f67654a6bb7def1a9cf74571","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"35a644763f50424937b17109eb552ddc767270a5526a0a827810ab86585dc53a","signature":"1b4159a10366adf4fd777a6bd595b4b846f1d3d37d07498c3498fd4b71e0f813"},{"version":"2e8ff3406c54b9e6ab83cde71a8e02eec9796f58626bfb39825d11e4e03d8bbb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"eac3a35ef0fe0ab083e8cf36a29a821e014dce98f56d597ed2dd815ff4cace46","signature":"7a79bbeb7030ac58672abb37f4776cade6988cd7cf9a535cffd548658d85f1bc"},{"version":"628364563c513db63865187fbf6afd635d3dc6cdbb8472e639a2c60fc3c13b86","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c39c138e6eafd63689438247533cbd792f2487cfccf0ae812f9cde3087d5c1b8","signature":"2a88f7e64f6f67e4eb7770b1678fc459b8ddb3bed0cb40734c9c257313affd02"},{"version":"dd81cddda6088f01807ea61dcc81cb09805f09cd222c1cdc07783262cea74cd9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c81045d6db59c1c83c84797bc1f78422a691ced9ca9a3554e2f8ef9aacc7dcfa","signature":"441f2bb4f78eb9fb86d27aceee4d92908b532110aa6b7fac80adede58bab926d"},{"version":"0496434a31fc8cf9e490e135e87ec3202dd0298290fcffbfbe4d93b3c180756d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bce05280900b4eb3333d10064531569c9fcfce6aa7aeca1ca03b2120539892a5","signature":"c96264be178c5e42597043c462b21cbc073618f43d5e1e88bd32a516068a2380"},{"version":"13c1ff47b7b02fad7d84109dc37613ba445a7a084561485dc9aef335c4c94a84","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5a22523635ca6f47c20b386b010ca1258aa19af5f4299f8752809c599315bcf","signature":"48c3a71b54800c134fcff4becfedca8347ff86645e004f8754d1e2b1385d1e9c"},{"version":"4895c3e936eb6d99b94541c611983585d9b5d805ff5a83b8f710e27f52dbafed","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f8f48136b4a2c472edf03e168e658ac758173fb38773f7849009ee0b39a81e5","signature":"d5b2231a838d0dbe8d3c0076afbb7e0544b2e318632b638adfe8c5743ddab6e2"},{"version":"cb12e3fd9be59bf0e28d6f18ebecbb52f8bd90258cf6dd2af77f7b12545a45bf","signature":"ff15a056114c88b5f7e043ca0ba2cc2a62b4154e462c09a965df6cbb91cfa4b6"},{"version":"1bbd283b943af0cce0c72cde1e843e55093235b7dd3fa7d22dbe957039c0ec76","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4f69209ce0e934946c859c4cc6248ef4a2dc528f5baf9b4fcdea5cf3e08d9d38","signature":"e8f8aa08e63443a0cc63ce2f9fc9582addd622528fad2ecfc413b2a91c688fac"},{"version":"b1a319734a490e072da3cdb0e53c3d90aa63250323e29ef7d7d554acf5a174d1","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8e79adf7f7c76bf3874257d3692b76744e5a6784c91cd27f93c30a8a5cc842eb","signature":"2c6835a2dd6319674e4b339215c071bf5896757157ae471324b1bd95868a5285"},{"version":"117c70fd7a19a5369ac35034f89ab731c6f6629d791e35334fb307b7042f89a7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad15d915012090304670ff62dd5e48d7694c11787f4884f51cf80f873dd40aa4","signature":"ef3c092bb7ed970d2273e55a61b12bc4741bca2219e1fe703350550c99ca6f42"},{"version":"38786de27bad4d667abb5f948b2357fce8de7448a52feb500965735a5f3a5a6e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fb5ce2f104fde415289b4febf27da970e40afefe42b87b15db28818ca9f94132","signature":"ccefe690307d556c5e44dd7ffb7d23faa0a3388926925ed0352107cd2efc4d9c"},{"version":"cb504109f1c6b549fde2eed46b44eaedb73ee7754b3f3a1c1c32bc5dd1693a8c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3613723853e81414c2e1220e3e0a696a997cc49e4390060b6ac7fd91aec150e4","signature":"0334a8b41901a52fc9195c66c97cee6aaaecc5aae79b9fc31f245a5df48b3ecc"},{"version":"d63995951dabffa413e3114fc0d39108a1c183939ef54616a8019d6061ad0651","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"082900437524e8bc6903f0d26479f590af1814cc5080fc502e07d38c951500be","signature":"24564adc2074d75d218bfe8b711580df8848afbd3ce2890184cd98e76529d387"},{"version":"1bbff9fec02470dc8837722310fbcb7de786f75d442f3e3116a291dba247caaf","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"34eb44fd7813a487540b6afa04da3175388b1841aa3612569e8aa07652363b69","signature":"9bada675e959a3571ad60f30eaee87c92b05fea94beca1f7fd212dc65734177a"},{"version":"fb371cac529d70b2eb453e41a785acab5429502dbc8655db1bd8ad27e0808825","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fda5dde4fccdc3b8a2fa710f43940d74532a63d457c6513e3ad065c3e151a31c","signature":"31444c6666f2df000d4c29edccbfa15ffbe4dd5ce7237cd47478a0b81b2f4443"},{"version":"e3348585394b19f9dbbdc623c9c267562bd28d3dd089b4259673c5bce452eb38","signature":"eea3073b37074a4dc266f913c309381cd52887cb77f47c2b85d4390dca658dd7"},{"version":"5ae8a275a25f0b2c93d2c11a3aedff3e73cea77f41ac7f49bb5b2d49bc970174","signature":"955903ca3f9b55fe1bb48e3d152fb647f88edad09aa6ec695ff8dbd8eaf9ebac"},{"version":"da528774ea74ae18aca17514a05c547a648fa364f68982ce02b56e9aaaa7b7d6","signature":"a9779efd000ab950e2097f789266a26af006c56a54d7bac966f2162262d4bbc7"},{"version":"0ceaa48c199fa1aab84399d7ab8ade3302ba2515e61956cadf13e218e3e3bc70","signature":"cf4519b131be7adbfefddb180ea2e0a1f5d2f520954d1acc4646aa4dedcc0eb0"},{"version":"57f3de13ad44d856ed5e540986c352003cf45bdc0e8a6492c38c31c60472c7b6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"d7030b68636fd9884a76ed71a2d9e081b22697c77cc0b05112f95a569f4f39cb","signature":"fb09ff619aa414f4b2d422c19ea116cacc6fec3220db1dd2b51e2ce8b7b1812d"},{"version":"37ffcf089bbb1b6ea4d4340c2b30c5110b76d6ea1b09b6d7f0cb87485976616c","signature":"a4c188535b372f30652897819a780b1631b6353cb42171a93dd2570c6a2722f8"},{"version":"852c367110c2934ed35a33ad276111aa9b3016ec92b36b86a37b43642ad9458d","signature":"92a24950b269736d532ea9daf0cdd8ea7361b70095f3468e59afea80af884516"},{"version":"5f1a16cce7b79b724102f3b562c5795f38cf4f9541c761d689e12ce450fcb19a","impliedFormat":99},{"version":"e004826eac62081f867c66dabd92d3ef7d126d93a70430a2c88429228c3ecc50","impliedFormat":99},{"version":"38d6857b58d2ac42442e396311c542062d4f0dad40f2adb496dd5fd0756ee400","impliedFormat":99},{"version":"34b7d1e2d15845cf08bcf5e3c01adbb92cea1ec27564ee249ba486cdfb28526c","impliedFormat":99},{"version":"0d0861810ff9d344ab37f055edbc4d14e68e2fa18e113ce8cc33aef9bb500b5f","signature":"2791178671f71d9b41ed9ef814e549d85cc6a77c61e2f8a87ec25f2176d9a3fc"},{"version":"c57b441e0c0a9cbdfa7d850dae1f8a387d6f81cbffbc3cd0465d530084c2417d","impliedFormat":99},{"version":"ff4bfe3c147eadc1b722a8ac9e4d350e66411b5c9022eabe5f4ada871c9adc41","impliedFormat":1},{"version":"f1b1170112db2e1a93ea317419a35652f0e9a87ca1612674f514e6f93791d4c2","signature":"512960c0e955a2324b34354dac25e3e4d431a1af4cd33077935eda5e95c8b7e1"},{"version":"bc80289e02f9beaef4c34bbfefbcf8bb68a03db34dd5c51c5d7f9d5f2512f1c5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ce5231c11444fbe2b0c77b5e56e80daa0c64ae2117ae6059e13b2aa9a1440d3e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6d261c7be483027c9d281ba71314350ad8ce66efabf46f7b6099f39fe3b8d218","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c02d45c2a6bc50186c1972f44c31ff22e887b30205ce84607bb4661c0fdc9846","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7fc06e1e53688bb32ccd86e730e08bacd38d9ad7ad006a234211b230825efb2d","signature":"d95aac1823e54b4183acab8f7fe3bec5dd7bd4aa297f56004fbcb972d299e377"},{"version":"c2175e53ec5b411439a03c757c499d2bb30c48bef05e83f372b5095fc2f8fb3e","signature":"2c2a937dc3435a48e0ac2cc67072dad512dc5240282c217955d3fb55e2b6a4eb"},{"version":"6c05d0fcee91437571513c404e62396ee798ff37a2d8bef2104accdc79deb9c0","impliedFormat":1},{"version":"5990e5575acfc932889f3720cf5915b1f3514d66b9d0dbcb55d2b62130b2afd5","signature":"ec851dc73402d7cce3fe5da65eb1f2e56235ce4533de1c561ea9edd6ac4b941d"},{"version":"9b86423edac57f38f2652f623e0043eb2b47c1b97ed308fe893b84b68b951984","signature":"a36586c8dde641e45ee945ae65b13a4a1ab367c309a0e34d5d61b642e4b192a1"},{"version":"9e1b2a2af3ce2f7b7fd5add5d78fb666b55c6b2d5c5f7d981fa07af20ebc575e","affectsGlobalScope":true,"impliedFormat":99},{"version":"b51bca9749817b5ca6f945534921cff40d00fb1e44378011e8f0033ddb27a9cc","impliedFormat":99},{"version":"2e82d0ebe6ce039ec74ca4d1df4facdebda4efa23e60a6216b4176bc8f4a0aac","impliedFormat":99},{"version":"f92f4e4f821a787525611edfd46e7376c1bcb4565f70b20cc5f055b1164a8f17","affectsGlobalScope":true,"impliedFormat":99},{"version":"514ef5d527919c910089364684e1b562d5bddac33242680f7249b200c0e3bcf6","affectsGlobalScope":true,"impliedFormat":99},{"version":"65663900ecef4e8c5b485ba06ccf30feeb4cbfcd710ea0ca79a5cf22c974aa83","impliedFormat":99},{"version":"0295c7a5d5d956391ab9bf0410e73a89e25fe26810f9a1d823cc794d682cdafc","impliedFormat":1},{"version":"19826a846db870c2261a3c4cf0695df889d9fe3eebe7775f3f5bc76fe7ad07a7","impliedFormat":1},{"version":"e04cafd03370139cdb0c846273cb19eb4264be0073c7baf78e9b2c16ffb74813","impliedFormat":1},{"version":"7c01c77fb7d8664daa64819245d785e106e0a3cb6e43da64346e4400d7fa9401","impliedFormat":1},{"version":"8c2ca98f4713d989d610fbd38a44316bc43c50aa26983e62dc31002f32ce63fa","impliedFormat":1},{"version":"ee931610d1cf7a6e666fad138187751392fc88bee931b94ac8c4571208dc7370","impliedFormat":1},{"version":"53543b3b64e624a81fc5876da6d72c94dd87655e7afc10988cf82ce7cbc74180","impliedFormat":1},{"version":"967e68e99b8a80551837321442a0e2f12ef50aa1ce567ec991ac6bf062a0c7cf","impliedFormat":1},{"version":"144ab2f3ef7404caf39c6acc88d248d7e55ab3dd1c4c0d89367ad12169aec113","impliedFormat":1},{"version":"759002d4454b851c51b3585e0837c77d159c59957fc519c876449ee5d80a6643","impliedFormat":1},{"version":"07c50b6db67b8b943aed3e410bfeebfb6d3ba1fd1e2819bc889e48f81e94ed2d","impliedFormat":1},{"version":"e3a5287471fb08f053c06fd998632792aa5f022e45278f1e6dd55fb2fa9e7362","impliedFormat":1},{"version":"28a6c8eeb48e165920067b9193555649fc43c2a28c450f23f622e5eb043d9463","impliedFormat":1},{"version":"1147c3efa5a256bcd6a3d2cfaf764185b7120bf985f8412d9bae596a0348f77b","impliedFormat":1},{"version":"490cb6f5e6c90c4eeab1d237242901fc418bd1f08345523beaccc7357122166e","impliedFormat":99},{"version":"cd4908c240506b0aa278c4c8d6bc1705acb41b03fc5b1d86a47f7a9f76f0b075","impliedFormat":99},{"version":"e663cd7abf76422500d57d6500a546fb51e79a095c23914efc33ef73a32f4f8f","impliedFormat":1},{"version":"1ac8996804c14b6ad107fa17f47cfba0f9a4e6f5bfc3ca8b6e6f6a4cfa636398","impliedFormat":1},{"version":"7a317ff3b65c289d4344ea7b6e0a9793168703a79bf1ee889b3ce379b58a54a3","impliedFormat":1},{"version":"32f007650e1b504bb21fa424a3f86b956599d6617e975f0b81be6a2b9b9852ee","affectsGlobalScope":true,"impliedFormat":99},{"version":"d6d1d623816f9e679cf3ff0205e0e37009a6dabe3e6038224379ed0e937f2e20","impliedFormat":99},{"version":"00ae115ba20d5e4665270d1fb9f60cd1909f902bcae5463c6702f3906eecd44d","affectsGlobalScope":true,"impliedFormat":99},{"version":"04baec2b0229844d351e2187b3da5df20d9b862827cb6bf501819c98ae22811c","impliedFormat":99},{"version":"be0244e519bef160f4b48d37b9e5dc5fda6625f16b3902d7de821fbb2ea39857","affectsGlobalScope":true,"impliedFormat":99},{"version":"29db2c1e2e934e0c8cdbaf8f7f5601e1b4dfdac35f0c839ebf585ab242f1476e","affectsGlobalScope":true,"impliedFormat":99},{"version":"8663fa4279da9eaddb386c1bc28de7f5b433dd2518763ebf185ed198b4b60aec","affectsGlobalScope":true,"impliedFormat":99},{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"58eb22ce690f7941a952f307d51a210195798c9fbc381470fe7e0adb228d4c92","impliedFormat":1},{"version":"49a70606a1a2e65aa6daa491d863c7ff09336c69eccbfd88800bcfb487933fb8","impliedFormat":99},{"version":"4b11433ee25f0442e19488e266448ed50106210ec1bf70279fc60b270dc56c1b","impliedFormat":99},{"version":"e134052a6b1ded61693b4037f615dc72f14e2881e79c1ddbff6c514c8a516b05","impliedFormat":1},{"version":"af5fd0c8cde550fc2c081a18517fb30954461a16daf2ed7969db3805e5a8d7d9","impliedFormat":1},{"version":"d4da3464d2c4b7d80d203ae64bb5c75a66221db5eee12a4ef2c4ce2c9d19a821","impliedFormat":1},{"version":"db820c42ed9b21f2d81ba48889aeb1317c710959346b042f07cee69a545f1326","impliedFormat":1},{"version":"695f2b1040663810ccac946cc87dea5236e3a74efb4bf5964def8961309634b1","impliedFormat":99},{"version":"8cf30ed8685586888fe2a1f781c7bf433c07422648a24c05aa4761253f8e8fec","impliedFormat":99},{"version":"ac21bc74759274985ce9b5d4e4c46dfbdd345cb6439c856941193be9e9519cdf","impliedFormat":1},{"version":"7459d85c80f2971be954b562724106b13d5a2a57e8bfde51723e94e838f6fcbf","impliedFormat":99},{"version":"c24ab9ac84d65b417a807ada25456697bb2adf1189fa80cb240625dfb3e61c42","impliedFormat":99},{"version":"03952163760a479246853f7ee56fd231d703e8d6f78deb353c7892efeef3db3e","impliedFormat":99},{"version":"0d121799b2cbac398e84a4708fcd82f980f33bf3ac7721a3cf9db9f28d9eb2b2","impliedFormat":99},{"version":"b45c2c7da9952665f78397fccb03f67535535939497b11308c9409c8505eb32a","impliedFormat":99},{"version":"9c3663fa36fb976609d8fc43372ae38dc2e066dab014a955f40ae6430733fb71","impliedFormat":99},{"version":"e38a172f8912eebc79671e07b81687a304a9d366a47933fde9f97ad79f8ac08a","impliedFormat":99},{"version":"2fbe402f0ee5aa8ab55367f88030f79d46211c0a0f342becaa9f648bf8534e9d","impliedFormat":1},{"version":"b94258ef37e67474ac5522e9c519489a55dcb3d4a8f645e335fc68ea2215fe88","impliedFormat":1},{"version":"219758c1721d5c22bb0d09343123f45834f0abc46ae721c951477d9727d78957","signature":"a13e991d818ac84d807cb05e6c0a62c8a9528cf740d4fc75e1932273282e3f8b"},{"version":"689244c57f5bdc0c89f511d55220c8c4d0f54107084628e65fcd2330c4faaee0","signature":"53e6a8c9b6e07251a846bd666ac8edd440de1348020dad87cbfd1c5c97e42d90"},{"version":"c840e12da509f2c771dbc356a23750227a43d1864640c7e6404b8f9ec86b3871","impliedFormat":99},{"version":"6ae92eaaaef30fae975de604d3af31d5b00eca7f02d89fab589152df926685fd","impliedFormat":99},{"version":"26cfaec143443411bc7d5363f274f885ced430b8f4bee25a81f7827248848d7b","impliedFormat":99},{"version":"e3b3780b68c70ff42894b68ee8f49186ef3335fc86a9106dd07e11580b460cc1","impliedFormat":99},{"version":"459fda119c27213d3defde220c051414020029abd52fb6fb9aea0beedc512c6b","impliedFormat":99},{"version":"76c44fd2592ce9da919eec96ae48927ed2d8c6ef85875bff636cbe83ebf59f02","signature":"58a5ec371db12fd72d7b69a8c237fc87c5a131763b45d262a3d191c5d1356d6d"},{"version":"2b4276dde46aa2faf0dd86119999c76b81e6488cd6b0d0fcf9fb985769cd11c0","impliedFormat":99},{"version":"38d4cff03e87dc58bfd50ffe5a3fb25e6e6d4136a1282883285baf71d35967c5","impliedFormat":99},{"version":"5ecea63968444d55f7c3cf677cbec9525db9229953b34f06be0386a24b0fffd2","impliedFormat":99},{"version":"6ea9c8bf2ae4d47a0dbc2a1f9ac1e36c639b2ac9225c4d271c2f63a2faf24831","impliedFormat":99},{"version":"a3d603c46b55d51493799241b8a456169d36301cc926ff72c75f5480e7eb25bf","impliedFormat":99},{"version":"ad98c359284db8c984e88949b2c3394e4a35158880767b772491489788a6c5a0","impliedFormat":99},{"version":"5c117cca0b75ed634fe3085142a931df2e2214e26f2bbcb34c592b767f13c1e8","impliedFormat":99},{"version":"773c18e2bcc18598df8f8b2be930eb26b22608edf368e42e9ca3484828ec4122","impliedFormat":99},{"version":"37f96daaddc2dd96712b2e86f3901f477ac01a5c2539b1bc07fd609d62039ee1","impliedFormat":1},{"version":"4faca872dbd194a17b3ee267bd8ddc3daf3d16df96f4e43a02c7d9a862022c4f","impliedFormat":99},{"version":"f89aeba83b1744a0d697c1b7fb8a06d8dc4cf7c0d469d2419772121f499efa8e","impliedFormat":99},{"version":"144a4e5780b800c0553949169f50be285eccbdb0298afd83ef2ae03fef77e2d2","impliedFormat":99},{"version":"66aeb47bf8638d6767f7b4ff684c2d794391c981590073025e98f98e1afed499","impliedFormat":99},{"version":"26748898fec8579096c776866e8e6f07754845b3d08f5ae98c3a59baa9e85c2e","impliedFormat":99},{"version":"6d805abd62920edbd9ed4b20be26d040d01529f3ce53fdab9ca4d0fa9b589f02","impliedFormat":99},{"version":"edbeff52e73b0ff82898142aaf8ffb336910a8ccdfc31b79960ea4ad4c9f407b","impliedFormat":99},{"version":"b64a8c7a27133db3f181199d21c0c582e86f38ba57a031a126b77229526b4916","impliedFormat":99},{"version":"4d857105510df8011cfb5b3769dec55624a1df92e85d399cd03bc82bb89d090c","impliedFormat":99},{"version":"8eca47167dadd486582ecd4e41f7fba6ae66cc4a4c5202f1f7acf34129a0dadf","impliedFormat":99},{"version":"755fc4f6a7aca9da7b38e1f7a39abd36538a72711763c741f1bbbf36f999b654","impliedFormat":99},{"version":"c4e79e849fd94553e07d7386552c472ea4f4fbfc9d280c012018fe7a6c50e598","impliedFormat":99},{"version":"062c0cf9641ca90ff3ad8edc61c2e06299fe6585fb9a4014a8acdf7f11810d51","impliedFormat":99},{"version":"f68896096cba0f06ffbf39a67c2280f6f2e5b90c75db56a3f9ae5f7f3bb54460","impliedFormat":99},{"version":"97b02501eb45f487174d5a0ff89b6a95690d50e9eae242e2162118edd5f2705c","impliedFormat":99},{"version":"425a03d68f43164e0214b1c333cd58e777d4186f412b530467c18ef0d2b37a80","impliedFormat":99},{"version":"f9a591e5fe0be6728cc84e70325aacafffcf203b051ddef37d65651b43c05056","impliedFormat":99},{"version":"293c0a3e323608f4e20667acc74d7bdba727db954b840ee1db03f2fd5807c761","impliedFormat":99},{"version":"de8b4c367880fe92a0a740b706f08a46d1cf9e3981d55c2701e82423e81ef0ef","impliedFormat":99},{"version":"4a056a71ffda9ff3f2adec60c0189c906f7e46976a0c6650fa196674ff8c4dff","impliedFormat":99},{"version":"3a3fd6f5ca85ceeb293f2a010125f9455404958122b6dd0ba0b34f7dab74feb5","impliedFormat":99},{"version":"42b58bc8da11e9181ecf4ac498d41c74930c73c8ebef091474d0f8cf971b50ac","impliedFormat":99},{"version":"9e003336714371c98108af3dc53342829039327f8b1db97d826ca81805746b7f","impliedFormat":99},{"version":"d8954254110123f6f5c9fda6280674f6b853d47df7fc41c2090e741dc8b6b627","impliedFormat":99},{"version":"cc141bc69488825723b0d638491da8779c4c3e515712d5e1e63709e0136dfb2b","impliedFormat":99},{"version":"df600fddb7f36e4c4b3cc4e36073d0099a5b6352b88be02decf63869c8886313","impliedFormat":99},{"version":"9fe94c8f6b36cb41acd30d89567761a52246932dece21e1ce104baa2e84b07ac","impliedFormat":99},{"version":"5560b9471d610c2543dfaa66a88f7b7cf05cdac61a698d4106b3845b7b7b3b29","impliedFormat":99},{"version":"e80a0ca19ba1ea6d815b5017ce248fc817c076522d1b0f42f062a784897a30a7","impliedFormat":99},{"version":"e96bd939a55117abe6ccbc02839f2f4d9ce3893368fea528ca91c59ebddf496f","impliedFormat":99},{"version":"81f6bf27eedb1ed92466abfcee33795a6b2304691ae01f42e60f8c76894fade7","impliedFormat":99},{"version":"8ae2e6733fc535c43c514f871d28cb0fe5926238dbc3d64c978a1f6f00d73296","impliedFormat":99},{"version":"26a0c2d883e1ed55ba00810d957dedcde5d16d637e33063686e2bc3f58a5c64a","impliedFormat":99},{"version":"68099697ac4e919f6f1832389f32eb67d3d94fae744967f33e0cbc049a222a3a","impliedFormat":99},{"version":"bfb900f7de2066a4be644c269285fda8ccca40b065476a27b082173014d00467","impliedFormat":99},{"version":"fea750ade335a63f707e7fd7410cc11b81805f6b454880e1ed0e178c08fae8ee","impliedFormat":99},{"version":"7e2b8299e85423435784cc6244e2d559ea862d226e7b0ec871c6a53f88e5139f","impliedFormat":99},{"version":"51b6335f5a8e177306647558a3eefa1f6abe259b283c6462223be3d7d0f33300","impliedFormat":99},{"version":"31a63b6b49338cc6ddb1a318beda72d3d8fd523e50f8dd5d5ff3accd39a490d7","impliedFormat":99},{"version":"8fc7615fb8fa095406f5a58b1b817b9a685c516058877a3d2c06d4545fdf5dbe","impliedFormat":99},{"version":"f70e9fcb8172de822a729503eefece37f169e063cd7f97a978e3913dc872a82f","impliedFormat":99},{"version":"d5da26af31358a4883edb6112879018b14c7c1fbcc457aa36961b03ee17bedea","impliedFormat":99},{"version":"4e93fb2d2c59bbc1f1a5211b36c447efe4d0af568d682ef1e5eb5f84ca6ccc2e","impliedFormat":99},{"version":"b6cc07bc56c7418ab500421d2629c26acdb6ec2e9af85c5e2c5d7e8c9f92566d","impliedFormat":99},{"version":"3d13fe973e92e708ad3dbbf1b2385bb799f8e70c8da71a1ac72fcb5521c8a5e9","impliedFormat":99},{"version":"16f3f66b5182e57c554d0e374e29fdc0a899c1321b3f94fa997d19abf9faf931","impliedFormat":99},{"version":"df459c67b9eef71318851331b23926961f0476b4b9d1674addcb627eabe583b2","impliedFormat":99},{"version":"b93915733f1802a8d9093116a5e660385afda0f72a5bab9c9bd356549a61f1cd","impliedFormat":99},{"version":"1dfb1491b28cf3734f3c07745a56d7758a12d546f81a70fbfe8ee9faeb528c54","impliedFormat":99},{"version":"42b00a511331c7f79039ae3be83fd5941b212ce193654c3ac5d82157227b69ad","impliedFormat":99},{"version":"1a5914065c7c6e2b6e98437c4ba6059bfeda3441f0f55cb07189df1e78698922","impliedFormat":99},{"version":"66d3f42196f32c639d8240bbd520852abc024d673c713bc5ab26b4bdde750a74","impliedFormat":99},{"version":"18ce9175445b3edec11bbca1c682a8f3919e74fb3504abd883ac462ea8089a0d","impliedFormat":99},{"version":"aec96f94a27fc61d5f4ead202628e811199f887d48bb7614b7e1706b629cd1ba","impliedFormat":99},{"version":"2ab01a0368f65b3b891e25416ae785dca54808f70d8f6204b99589ed4f7f1d2f","impliedFormat":99},{"version":"efd87ca2190e2d0f905e30343443c8d2f0d32cb2b1c9e05b2cb97b0061abec08","impliedFormat":99},{"version":"3323edd649d2c85d3528912cdf6fb770258d59d75241575fcf25c7850837de79","impliedFormat":99},{"version":"e0c8deca5262879f11ee84fe6664dcd197a60f546930363bf2d78d37c3138c69","impliedFormat":99},{"version":"3b8ba0456ce7763676505d103eb336a6b97659d6fc8a7d7d186558ea0b8c7ea5","impliedFormat":99},{"version":"7bae4a3f50a844fcbdc504d717f5f13dd7178ca99f131b230305db6b55e1dbc3","impliedFormat":99},{"version":"f3a4668046e42e9f5ebfdc8b79238eb27c399443dd1dcfcd4c4c3652692ed031","impliedFormat":99},{"version":"07160ad59d49c6f2999f9703542918df3eb1ef93857bc560b4baa7c6775d22ce","impliedFormat":99},{"version":"642d33e1e66ab109cdcc5a5b5535535768c8b642fd5ef75de4d77de3a5632601","impliedFormat":99},{"version":"c23b9684ab48ea6956f0a510c3cf724277fa457efa7ff85d5907f4e4290ba0aa","impliedFormat":99},{"version":"3859635347ecc10d4ff5dd68bde66420f5a8539b7313dd62020aa025208d2222","impliedFormat":99},{"version":"f49e8feb6d7473579a5ceb5872598bbc1be3723da653993472720629c72fa0ba","impliedFormat":99},{"version":"14c727434cfe6a078b51a88d005033ad01a76bec36292d7b47369d82e48e1cb0","impliedFormat":99},{"version":"a986f6af9c8f27ba98239933aaf4e47368f3d89c4cf3fb37e80b2556d73bf253","signature":"b9acca28c776aec8d087257885a24491333a1727c1c7068f5de67816e6b85de2"},{"version":"110f4d9864e203460330d19430d757517b191830fb36819624529877e9f75c41","signature":"969ea527258e04281d145910d569e1a74c7156bb8bff03cec975e60b839ed69d"},{"version":"c3d577953f04c0188d8b9c63b2748b814efda6440336fa49557f0079f5cf748a","impliedFormat":1},{"version":"787fe950e18951b7970ec98cb05b3d0b11fcdfeb2091a7ea481ac9e52bf6c086","impliedFormat":1},{"version":"13ceda04874f09091da1994ba5f58bf1e9439af93336616257691863560b3f13","impliedFormat":1},{"version":"47039aa29dad4b33704e9ab56ca4ea4c918aaf86afdcd4e059f9dc6045850b6d","impliedFormat":99},{"version":"433839016857a5a785134d2d5e760fdcc9819d241a3ffb5cb76985e666a34a8c","impliedFormat":99},{"version":"ef23a7b386401847e3b4c3ed1b087c32d56e7307b2782aa2f0d0ff4f02d70473","impliedFormat":99},{"version":"2dfa6d1aea770ef46bd51a79cfb7aa10b9ae90c9971eb53b4f5def028425c800","impliedFormat":99},{"version":"66cd137411911fea6db4b352a98279470e386eb3b1cac5db3de1efad678a9015","impliedFormat":99},{"version":"f8d95db2d66d268765d447b554dffdb3f1cd2a22e8da7f6f57dfdcad6a19a1e8","impliedFormat":99},{"version":"32d14a5cf8b4430d2c5df3cd85a3e48ed3d780e24eb1dc08628cb8ddf2393b9e","signature":"9a38ce0e9264512c2af793453b7a0503e44a60a25100f53e3fb8d7b0c1c95d53"},{"version":"1dc2686db29ef5b3a12b29d6d402e8769314aff02b29d8cfb32676d189973e42","signature":"cdd90dec979b4367fcb144d5b08f8311cbf35a855f5aa80182bc418dbb3cce10"},{"version":"01eaf9f05e7800d7b5a21489b8dbe095467a5642abc55ae7b5acef6bba76c439","signature":"a4e79780a39c0fc2e61bc66f20a63f11515ae8cda63852bf56f75664aaed0438"},{"version":"2532372207d5d5484e67bec571c9d80da4c01a0ad0033b900f0881ff224651e8","signature":"a7b2f4e64735319a876c55f776eeccdab6cc50e664b55b1b62a27aa7fd09e743"},{"version":"532069a8eae99274aabc8f7eddb0562f92390ff4f63ee88165b81e7f41ce6b96","signature":"fb69d502157f1cf71cb8c737f6909c2e82f2a53b8157f840411444435f5da3d1"},{"version":"15abcaa279117eb516a90c09c4b60c53fb29c1242c7f67bb1003e0cce06f7d19","impliedFormat":99},{"version":"97a13c9556181373f99ab03f369ba031615835ba233563c6ce2dc3a6880a47da","signature":"bb4b02b4dd58d4e434c952da55797cf052d30d36b09bd14aadfbc70a037c84ce"},{"version":"8c8f462d06caec0a7e03f6e2d32880365a1b080b0949f17143817580c289544f","signature":"5056e9433f06040810240d3c09911d34ecb07fdc5a27820453cc1ea4708775f8"},{"version":"cc3a63721349e812f6ed685eebfb017325f36b0b40023f5bc9e68ea769d1851e","signature":"9bc26c030aef4b75d3bf757291b79add9c525e1ed6a83579c53c5077feaab77d"},{"version":"410b82507292ff0bb031a6beadaa7fddfa9646f3a0c16bcc4acbd3ef989c19fc","signature":"2e8ace1e09235d2b98e61fab48614e4e94c36f427c3c815b436570eed7068a41"},{"version":"3bed67fcc8c4bcba76bddc0693f9c428ce26d1d32571a1f21324842876c9d367","signature":"45d0a7610c0ff4db43d228e6c1f823fb3eb19f262ba58b54eb1ea0a39c2ef7ce"},{"version":"3db2d7065844cf28db1eff8a77477e58ee31f9e3ae4df9dfa042c2c964f83a3a","signature":"2dc7085a3856beb3291a4eb024a77421349c5b201844fa96a88b879ea9ade85d"},{"version":"c922c00c12a7f430fb02d99ef71ff861e8c2c29860df31b2dc3c6f922e7ec101","signature":"ebe17c0f79d00d4336e2868b21aec2251ee4d5a74c81d1380236997f5226ec89"},{"version":"e48ef81520ba3b6403efe621d96c7145bab0e6e498e7b2ca0de415dc13b6848d","signature":"3e394036285671868dc3fc182e60f36301bb4f0e58e32d802510009a51e3f7e9"},{"version":"7f19b8476658d25ff197c84030e58cd7395059d876a54630e025951e474ebdae","signature":"cbb6e618cfae37c1feb246b78260428cf2bcba79a0c9bf1ac60d511a4158692d"},{"version":"faf9a217d8d237b02ab6d95508d8736ae431bbeb38d98885eb5b8fb6dbe48cec","impliedFormat":99},{"version":"da08c6b6a9bce701e6d2775ac410ddb2a7189e19b020abea06c1932226248ae9","impliedFormat":99},{"version":"b2e5691a1a795e58a63d10b907248299ca96bb3433b7fb252279b4f00cc3a5d0","impliedFormat":99},{"version":"4244c6a12dc14768ddc3ee2737ccf1c6c0d07353785d5979df7d250eda461a61","impliedFormat":99},{"version":"cce820aba9ba9d1984461c67d0d543d8eba7ea25c6a1be7a47c31cc18907a631","impliedFormat":99},{"version":"3e7e929f14bc384ed6da8ef0c7dc3aae5ea191494c9554e4327186ef40db2573","impliedFormat":99},{"version":"2abf45177ecaca091e103f4d9557a7e8524d160b28a8dd457dcb7defca65245c","impliedFormat":99},{"version":"bb2b81bc35df3045d588a60ac6649c7b45f539c855660642ff1c12e9b83fbf1e","impliedFormat":99},{"version":"acd4a1daff6df3526b17094f8f30ef3c9c19c9bd5bb95c3fa46a1b8037290778","impliedFormat":99},{"version":"b719a8ea8321a8d636c2765f8b5f8efd20a43f8f7922dac4f2f7ce7e361ce233","impliedFormat":99},{"version":"2673620ee9cb5e62777ceeea4e84f0c53a2bea44d74f103ece49bfb7efe95aec","impliedFormat":99},{"version":"77d542e6871aea7774c7482cdedd9ca20cb03145ab9112e6845a1cc77b7b6c00","impliedFormat":99},{"version":"ab2a3d386e021bf5619d581692f14aac01bef911457d9960ef6c21d9b97c1c06","impliedFormat":99},{"version":"0f2d99aaa9d3574828b6f8c1912208e370a041d640497679ccf4a4f5e073efb3","impliedFormat":99},{"version":"466e0183c786efd5a3e1b69d6e0e445c77d771598871c9929137184206362b80","impliedFormat":99},{"version":"49149ce82e10dfa69eb4fb250755ffe74107279286aaf8305ffeb201a0a96658","impliedFormat":99},{"version":"ad06dc58b3a23a89eecc369690d5cf5ec2d141009fff32cdd68b94ad8de375c3","impliedFormat":99},{"version":"f2b23f711dc15c9ba970626eb5e654d4e7401177b13d16085751f40dd1d77d17","impliedFormat":99},{"version":"b958c46e432e3a260af58b20ed0fdc4f3ca0c02bbdcbf12f63113c1a1932b57c","impliedFormat":99},{"version":"7e560e86bc8f0793454de297413f0ee411418d71b0f002563ddffb26b2f8a438","impliedFormat":99},{"version":"808a1b31fb436c3f1c3bc9307623234442cdb742fac3cd39bd0e8cf4797f86a4","impliedFormat":99},{"version":"ec8657eae496906e9c2bf18ddf1f276d6472cd7b42975c609fe49db7d10c4716","impliedFormat":99},{"version":"8834848635faaf4a4c1b77936a3790515df4989872d12e1f586a903ba8a96d32","impliedFormat":99},{"version":"3c9e08a6171b77f9409f6fbeee92cc7cec90295165305f4749c15b66984094b7","impliedFormat":99},{"version":"d5f52fef25eebd772b28fe3acd0ef103cb3d338c73fd23dae81235aa3d3216f3","impliedFormat":99},{"version":"0c622f51867f7ec248ee30282d7358ffd4365023107747a8837819d38ae07713","signature":"73cc5ae3d7988a184c8adcd808b83f66c1d53fbabb555e00f81a610607fbc767"},{"version":"d9bd97b61a4c5a17c054f0b35a3d60ab0ea7b7cbeb3375ffc6f04b34a84cc137","signature":"f538f6763d0744b10bc6d491d559ea72dd3ce9fa645858ef79d8bcbac6877bf7"},{"version":"a0d8f36c675b3001a7722e9c26d87775473dfafb3d81a185f9c03920fa5834ba","signature":"a29e04cba58c684bebd17bd32f46a53acb5f8b1292a42d53b1a18374b80c0fe4"},{"version":"efce9dbb7e2a327391cdab96ba61fd870a6e9ab0ff198a35a0a794227a0b5573","signature":"c4a7c080c4efc31d1142796434513a88f078771f34b505eca798ce5a9bdfec31"},{"version":"ff9851aceb984155932fbf414e751263fd0b58e760f95640a1e6fd49572f1aa5","signature":"34aafa201e480563bf9f474edcd99b6c10c798eb18891de1f65e60f032fdb482"},{"version":"8221a836fcbef6908900340d3be08fbd839a7eb6046a4711914497616a5e4b1a","signature":"1c211f72b18ad57348fe0b7544548f7b330b4e306f7ff4974a0db403df514a51"},{"version":"63868919b8c92bddfa4ea91139ad9d62761c1a2cf3e4553410be3053e3c6c281","signature":"29b3c41bea83cfc6ee031ed69ce943d1a494fd352b60d673f0142d0e42c8b1d2"},{"version":"641447db29bb1eb3a9adafb6a63da7ba11845a0931de5d938804f38dd442d0b7","signature":"729a8e79214634324fe502fe0d85c64ee79bc11cdff7422acd64e22b4ec8bfbc"},{"version":"35d2091cafbe10f5796ea9c9bd08920e6a0b79a3877af0b2294d7bb46e35e87b","signature":"1cc4f87b153eb468fa435aa970bb93a46db33760cb339b641220b564f615636b"},{"version":"3eca09f3c02f96d38d46e308c5b3222716c84c379def6dcfbbb9d649450c6561","signature":"f60810de973f028b077417c92c4b2ea04ea8b87f33b85bfa5d3674749da37b7e"},{"version":"c035ef058f2451c04bd992c6281f8c435170298f50fa4712912053145e37a0dc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7bc503f605613278cfded74b9afdf98c3d708b835ef8856d44f0dfa2597306f4","signature":"453c00e11f1ffe23f85adca0e32b862b41c8f4dcc2dc969bfc9d666b303d7005"},{"version":"46488fed628b968b68977175df4d41ff7c305d305728a8556b0de611a79013dc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ec3bb34b23743c95ff292c3ddcf5ddfb4441630f3c71848fc5bbd18f3b5b3ed4","signature":"e41cb0738e24140c475bbd859b8ea3e99fb2a4dc5ff2471987e81f9a15cc56bb"},{"version":"26f0cfcff1aa76b2735a34b64fbfbefafeb9d807026576025521ba6abf6f422f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b57861e1b6a158a3e81044be6b1d5c9e94d968ba63c3e3f3de0b9e37356ced56","signature":"a458882652ffcd89d8e16df42a8723b41ff0875fd6dd931d012fd5fc8972cfcb"},{"version":"b5dd7ca3d7fc3468d822dd471aeef7f87ca35d1581866cf41ad9496eb6eb07c3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e930d0e5f7fe3aa18efe59bcc0d93deeb3b992cda037075f4b733b16bbdcb648","signature":"4476440845d41c5d6e22a9998d3c66cf2c750fea47d0a811c1ac0dacb15c1a9e"},{"version":"4c5aaf60156e5ce2f38171f24e1ff9079c66d3471e3c2a8e4cfd4e8ba04940ae","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b3162954559ebcae366f6a6a8a3d8e1daa86b03253a9f8b4667856d576632a8d","signature":"543ed74211b09aedfed2327ab5190ae5aae8020489cf9bcbcd7b6f5fed43b9ca"},{"version":"9e205dbaf6d76983bbd008f5858f4503edeb4e63d22144f395989b04aec80b49","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f5a301a7f2a1e24bfa948d720ad4a3e443966e1bca19c1ef21e870218ff00f1","signature":"1a745f1ceec14fca346b02f22a21bc9430d1114c92aa54de6a01026f22b40f91"},{"version":"a560680227e92043871bec12eb705924803cb7f51d3bd7a249746a5c66a777e3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c5d49f9abfb3eb1234bde5a49f8ccf74cd03992aba80e23a7f6127d932575173","signature":"f4bd3dc3555d2785ed107499684380e787ef42e1d786213f6105ad72bf5dc7d4"},{"version":"c536980a4710245b5d3aec5bd1a30297e14f2702be362fd88392a17beab701b2","signature":"a8ae6f331461073a442c9afaa1ca7cfeca78af9c137afa6e9233fbde0895959d"},{"version":"74310c6edad1f850774ca58fe5ed0fdfe5db3c9f142ef3a089666329d5dc2ceb","signature":"fc2d6dd4b30084cf2e4b653436bd05ede2b6d7513592c8c63d3b151d3ed30312"},{"version":"9e1755385eb94c17983b911ae3d6d4c6e012c725f4f211973d96917f49c8523f","signature":"ebe1835cb536933469a079046b10933b8c150543290b84088db4bc39d574c01a"},{"version":"9add9c771bc92c7e00c8bf360b43481847fccbd41a44825d5593dc9545f39661","signature":"5b348bc254dbf64472584721811d1c4365a8b9655c3b7fd36c313b0bcceed20b"},{"version":"4f77c0bcd3ec67f16e10eff3576c322b47d299948aa40885f9dfce3aa0b67fcd","signature":"244ebaee450265298f56f1b7cd1172b2daad3c8e1581a3c34bc7df376f9994da"},{"version":"ae859e9c1338b0569335d7532a3f4755d9805084b7e832afeb085b5451c9a918","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"67a4656148e1c74d57a43bb300ba8eeaf7435d364a0cb97abf326900d2bfce62","signature":"4db5a07ddcc6b732c3100ff4b1d9952b26f9f20ffa545bc4bfcc587e9264f614"},{"version":"3e49c4acbde5add78f1859975ed20c90c4e2c12678cb1d8e5a86a2c199f7176a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"aa8a81b075a4641ed6545c89e2faec209cd0cfe8c226ba06140897f751d59c02","signature":"97e3b787148ed980ef6fde084417456b56ae13963a7026a4784eddfbab619759"},{"version":"c0a9c345535170bf25c312955c7da4611e4e15cf2d5d09ee79473987c1839b67","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bb72eee654836edd2cdd7cfea23be9215922a20541b0830bdad6db53276935ec","signature":"e9c030958d79b92ea2f53fe5166d6152502804435ba1ab04301944e43d5e4f4f"},{"version":"861a4ba07da50ae79873f895baae98f82143dfb56cee577c271d0b7b22a877d3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9cd9eab693864566ac38836fdee14a3710fff747fd7f1a480caf810c1a8e0483","signature":"94f2d0b85003e9a9e6aaeaa55259a1772bb0407ac23842d1a4ba89380230a266"},{"version":"451305998accece385013ff11837798fde76a871cea8e3795a366d8fe04e5a13","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9d28b54569f4cd1c0c21d95d0e70f6fdbfd792121604b95d7c2d067ee0f0fd11","impliedFormat":99},{"version":"b92dbe495c1dee9f3cd7c1466fdf02476f19ae18a7ecc829e3ed8a380b2d597f","impliedFormat":99},{"version":"bd3e3a447a34cf3dd875ed550ea349589b2dfcd6b3a14abebdc946366bd812a4","impliedFormat":99},{"version":"6b100282ebfd5bfa4d8c72ad5dd3e9d325aa32d0a53623d6171fc2cc2899fbc4","impliedFormat":99},{"version":"29297b3a1e4b3503eef0ec9f601f2a8da149c8074a0813624be8ad4889038ba3","impliedFormat":99},{"version":"5cf87865e3a8b12bb8ddfb311846ff2eae24983faa365f53d9224fb182cd0269","impliedFormat":99},{"version":"98ef9c3f5f15c18abcd6fa9f12e93e1bbd608225501151603cce97642dbc961d","impliedFormat":99},{"version":"c80a56a10f1a8e01c4b7f08df6e9260ae076c910402dae8173fa6c7dcacc51fe","impliedFormat":99},{"version":"becca0fa1bbcc55ad69918f65f771b76904ac98b2563cb34f26fbe240c3d4db3","impliedFormat":99},{"version":"32b344c3765dc7c383516f3326c108ca84c33df44ece8ac789fce47d47ba8810","impliedFormat":99},{"version":"7e2442c976852a201ca2f298e1c57bada7476b501c865b1070e22d969d712740","signature":"f774a7e8875649db68f5952801f001f3c316560ede472e5663af7a8c9c18f938"},{"version":"4a021f26fd7efcc21acef07c38a3d57ba0ee1a966cc032004afa66c4f95f08c4","signature":"aa333ffa319c18796c9104d75082dfd218334109f702eb6e9ad369beac518833"},{"version":"f2b3e4319e013e593b8de35be84fd0b88fa97b67f870cc8af6fc801be093d95f","signature":"43958dbc81fcbf127e4104af0acde2116a5b0c0309ff3c811ecda47d849b324f"},{"version":"8673f100e1085ad46e0d8bcc16a112035488ff7baa89e9d3dd80d2ca1d9e6e22","signature":"ea7717cc4a65e1030dbead855313a4928f5b4804bad8165ef65ed75aca4a9de1"},{"version":"c0facee0934c3c35c24074d238318c624368a5f0650f7c5d7c1748ef906d7b7b","signature":"ddec8a41401ba491e6e3260495d11cf559f75e0d71c458891dd3d04302e91506"},{"version":"fc88b086529a46b3367bcfe3b0e6ed59421ea8eb5543921f185fd635243d0576","signature":"f7b8e8d653f68258e733978cb10977ef6502ff093127fff7c67c603258fbc0c1"},{"version":"3f567dcb87f20c3667daa8cd5e5e8f7f4062666562e3109af5cc2b0ff2c09378","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6f620a5da77a995a0557604c177f3126db4e04ced4b1ee133d1150c7e9af6687","signature":"65c0f4461b3b871b9fb8f09dfb6c6995231ce5c81756f7e2ad1443a8bb811853"},{"version":"9851df53571e11efea88950ff7cbc8f1c5d7dd14dcc3bf980b1f0829ea47bf05","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c145a1b94e8f126435db2db121bfda1831340de88737157b679a5e0a048b936a","signature":"9d3470ece4f34e063923ce5e95c6a2857dbe862ac4160986f6ca2ae7492a6584"},{"version":"83d57a594c11bb32c425b6a6d174703fc55681163bd57b494ebb03ef2f21c046","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"936a2ca9806a50b4e9be526c26e35798afa2a54e33e7ac66e63729f507f780e1","signature":"26f1753414b839143161e2cd5dc04694563b6060def71c7c557d41ad5ed670f5"},{"version":"ad10a345fbd8bf945c6722e75aedb0e84ce9e79a81a5e83a3fc36b1d01236607","signature":"68d3cb371946df9752181f498258b83df47a83c43b7c65fd8d9492ccb2cc38ad"},{"version":"b8b6b94c2e9eec6c2b56b0ef2fe797f151cdb7637e86baeaf6b4a850312fd9c2","signature":"2a894521fb9e328100a2fcb5ec9a06f498a1e3e7e85eb4552833cab91539b432"},{"version":"9172402390149d6c229a21dd3eb44adc22121588ee668f2bcfd760604de0d2db","signature":"89fd106e568e14763b9d88d102f99226f38f2f33a7f155fa5a5156b3f97a918b"},{"version":"ac71393bc1487fdb8618f761c86fb8de6768869ecf5a0af3df13b3fa5bc65d6f","signature":"35b620e13b6f758ca46caeb176e8a2058a354b71e6ac9725d85beb980e6429a2"},{"version":"35426102a525d13bf918c3913913d51879ec724f4e5cce01ab46803355e803bb","signature":"d3970af1c43979bc3c6f599f83b2d116723d4e75d157d840bbce825d4cc013c5"},{"version":"477e78351d1ddbbdacf8412cbc8e609d9cd36fe61690ae39e99be4c1e4b154b7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f3b79149a1e0f40f7a1b0cf5639c9bc8face06ac35e45a3a8fc9ac1c8649643c","signature":"e0cd82515bc8fcab3731ecf190854ca399c8fc231f83204b33430230974d12f9"},{"version":"594fbfb2b900134500e75b54d71facf66760bc4642a17066223a0399d0d13096","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f0fdc6461bff42d5541014d1c542a00cfe65384a57804db91b9b29e696671988","signature":"7cfabcb3c97a54b4e9ef65bf4c7497a349391494470ebe50e2b9640d09281937"},{"version":"c728712d0c33f525cda352b5aa9e8ffa7b240b041b984d75c8cc3ccce815e07a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"131befe057a5c020d346de1e9b9f75dabf9c8cf59f2266a9f0e27176c9c541a3","signature":"d6dd397c037c7ba718dac429f0779b6155c56c4a7a34c2b170599e670fc59d58"},{"version":"b16dd1286b3e030ede9d816904b15f4407a0fd925790379dbf0ba70a5887b182","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"41a3b967d70a34e703928622f0d3aca29b16d734336e19428f187d526f7365cb","signature":"e06b7e8020b282098867bf306f99543f3c5d681405c54d0ac70ea5361ff69ee3"},{"version":"0a9f5f674ed91be163df17e25e2dc2abd3d3d0c40d4b77029a11540cf2451c20","signature":"236d8105049232dd31d36b2329448b12893997f9d24dccd7b2b32b4906865dea"},{"version":"a42ddfb05fdba9d21b0ea09accbf2895722d81dfbd5ed38bb8506a946128a11c","signature":"2506217fe22f6a8e513b7b6967ba39d5ec2dcc3743711598517c53cc68d25fe8"},{"version":"77f995463d80efedc47f622c76a8bea0ede10272acc3c7af582dcd5207d30ad6","signature":"faad948aae2979c150d527b12809c76e0743cf37da0b322bbe9cdd3c653dcab6"},{"version":"a9067b4b6dba53667265a0453ce801d57f11f9be1cfd148ed1d1bc79e7a3cdd8","signature":"019fc063be62cb841a476836a1945b18ffcd3859ee8cb647f4f7595f1252bd56"},{"version":"e4661c0527cf99a803f7bbba2fb0016ddf5ccb73cf98496ed3a2c548b45fcf6c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a09c62dab83937f41b59f55f977cd324d8552d52ab718e8ccacb73b9ffcfe2f8","signature":"f57171caeab1d6dce3893d2ad203128a48bb522b73d4562df6b7d6d14073d300"},{"version":"1baddbe8a6db04ca7428d63545e4ce62ee4f02d6a3b6d53b67a9afee24b7e9a0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e3ddf1c4dd38cc2753022d4c792779e6679ba914eb80f1311d0096acef093084","signature":"d5ecb4d8ab5bd5fe8da42222cff9ca3e010ca2937a3e4f50393564a587b64863"},{"version":"e5362de739bcb309194fbf3040bca329820e0a22a64b05541f659da99ca41e97","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3e2a55dd3e9fd11aa6bc0efdeab42d2a2a4bebe0c9b0827e64700eb867e0e512","signature":"8b9ffa5dcf765665acab2a0091ac825c3d1ce9630a1a8c32201efcb6a957888b"},{"version":"610be97d1c2d516a9dc114dca6efa3c4fa2937ba7442cd75b4360ab4f0618d5b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9c144b3bddd8918c6c67a6332f3b37521091ec69238bb955d934c698284ba929","signature":"06cd193d4c4e6940be1724e7b05f69862ad5478bbec59330e1db1cb4118d6d78"},{"version":"6f4bfbb4cf1d1c613c69cce29d5d6ae6fb1769e9221f7fade9f04958473a5c61","signature":"ae3e12f3c039097ee481c8defa0ab07d60ddca0d31fdaf26256ee1245f192c70"},{"version":"160f09da555233cf373f0db23a6fa066dfa31caaf752c3c097f26bcd40a7d148","signature":"3523fc8a3c6937588fa23496d6b09929b2379370370fb97a64a3414699aba4a4"},{"version":"51f2fb96ba5f2b0bd8bd2b81ac11a9960e65eb69b09832668b3bc48b2459c8bb","signature":"79a68f57fe1f9c742bf6923d57f7e92c5b62603b12b899ae169533dd6082a796"},{"version":"169c1094152195286e228a52b63aa645d3fb1ed768050ae03132ba44482752fb","signature":"f8031c5d8ade1b3f8a7f1f69bde71c3b62611226c6e2a9e499b7f9f6ccca47c8"},{"version":"3b1ff19587d5fcacf45a3ceb96684cadab07bf4ec6921b300bbf842436fccdf8","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e972be31cf6fd527685144bede5500f756340311b279ec7e0a2fedd1c22e2332","signature":"bd777dde2efa6743bafee805dc193e2f9fc27bb906eb504bd4092358c087d9d8"},{"version":"d749f24c658563616b334c9c568c8bcc693aa694fe198fb795178030fe293a57","signature":"a11d9515f4c5d19671b6c37944bcb81a2dccc0a56706f8852d131b2abec95abf"},{"version":"b67c073532f2441b3c897c1cf1b66e745f8ba56401bb7425ad47532e0d13d014","signature":"32b449f30d9776d304580e6f60603441d0b83760886003d66fee8f76740dfd34"},{"version":"b85de74ecd6ec138f1c11d97f44dfe699cb5abbcd9cad5e5e1ebaf38c620bcc4","signature":"2d535a9817e3d46b755503968e6f8c3ea57d16d7ac23cd8a7a0552f52e13d4a1"},{"version":"e6d6b98def1884c4244983270a5a2ec0a12d9e0b5f6084c8fec813cf8a191fb6","signature":"361558614187493181e5d1edd5b20f45a02a2fb65a8da11cdcd34d31d015613d"},{"version":"ef98c8b35dfe4bc2035ebc94670489af69dfdf623d2b8f42904965b4f96e4e1d","signature":"48b3e9ea98f1738cc20345d7d720efbe21371ae9748b964437b4a6f3c16d273b"},{"version":"b65dd9a55b58b3d2a6896c8f774a211d9517c230289371bdc80eddca9f303594","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"969a1c858486ed669e7247f5d8a95fac07e4a0b9271833a9aa225230831bbe5c","signature":"654116ab23597a1ef8427f7aef38e90812d3c3d4ab00af62aecb0444d4c44a7c"},{"version":"1ac3e56e87a485272c20b38c8125c79e42a1e16ed05373784d6ec84302d8148d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"764477ddc5f1dcb0b5b5de50649f16dcf4d36842e02e8ade2ad9b25340b8450e","signature":"8bdb56dff5459055b5a21f9fe895e95219f3460f641181f4bc37b5d5b3cfaabf"},{"version":"26fae1ef5da0aef3777e8f5447ae0f766c5f4eeb6036caf42ce5e0780ad52328","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3f7802b51aba52d5c4e91d59c4892e44c338dc10cab2560b8143979d990d75d3","signature":"b7d80dabdb6e9195629a2265e9cdee7afb8c8f2406cba5f1ce75ff62ec116091"},{"version":"738e8b1de143e25f4958482f0b3df6d5f67a094d15c1b6c73b397b37f388e7d0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e0cbde227bad31a3979ce1452ddbaaabd18727c91ab6146918c79b003c30b831","signature":"ecb61779944dd6d04f594dd817e2f127ca0c200ec71ae38a0a1f5fdc178f6f14"},{"version":"573abd03ce0bc9cc3a30747081bb6b5ecaa16ba1b59a29aa922fc1d485ce3b6f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"74a2404e9e409051b600c8abd0d876686ad1731e392393503dab8ad59d050038","signature":"d9c851283d7cfab93e0a2e5a988c442c7072087ef70fa8961ef6fd24df49bf0a"},{"version":"bbaff6c7e4288b46ba6e26ebc491e9128409f19403eb77d947e466454d0b183f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"13471ae3aedc24284930a5bf581a0e615865098436be58eea44fe0329855abc2","signature":"7b9d793ad8206eb5be40eaed18277e6126f7ab546b27c3e51413556f9bcb3225"},{"version":"03f7e0ae462d4b87ebd0c5b8d6779d882844a29c255ad4cf5cb2e2a0225f62a3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"04e75818d473e1616641bd374d057a812dd180459efe7d883ae185204d0f1a49","signature":"3c9c67a52e0704dd321352b7e2fd8918795b4fc4e44b7df370eec0f7ab16129a"},{"version":"2ae79a27ce4b25d6fdadb97bec297bef61f1c322403beb4804937bc554ed5a12","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4846776e25faf9dbb036333c666d964402950e66ff3b833bd5271ba18c19e14e","signature":"19123916b8cd7ba8b4872eaae51396cf72bc03d7946367ccfc3042c46f960d27"},{"version":"9494ffe3aad78947e0bfee0b7a914a3fa86b334b6144dad176b32102671fdd37","signature":"c33f8676872363b71b9aa3e66904c567efc1e6b405727401fc79e64c7eda06d6"},{"version":"54b65c57e0f5e3075fbc16e09469a25fa8d672c97845ce7fe8557dcf26ab98b0","signature":"f3c49b0c61f163d65c6d4db11b650595c613e1fbf317d61c73ab1ac6dcd7fe27"},{"version":"482e2b694ed8337c104285b0ce6ab875d7d979a0402f77b2948aa51449b6fea6","signature":"7f6fe6c5958772c345b151123e80fba4599da1c468af7058379d9b0426c6eebc"},{"version":"03a5203bf7f9fe0910eb22ecfe3230ba64e857f1b894445e91b262ebacad97e1","signature":"42adaddfe174cb67f75b599b3d7523fa842a74240d9586fdf18a0d3e77067309"},{"version":"a0099846790996a5432d7cc8225e35606d5ec55531a0333eeed9b2ccae8b872a","signature":"301f1efd2fed246d9e9d867f0584ccfabdb6ea9c13dbc243ec75fe604fe4e433"},{"version":"a403a473febb7938def5984571f440859986ba3368cd57b170e2228cdadbf4bc","signature":"7a3d773a0215697ed69ff6e87b6e6174b03312bee975ab8c7270e3d67de2a3b9"},{"version":"a1b8e47d183dc1a02170eb7888eb33440b141907251848bae238b00e21d01f76","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f911612fe6fbb59498bd042d845e42091b622ff60d62f62899ae9c9061da04db","impliedFormat":99},{"version":"df8d828dcc7036d38beba1b517c8bb7cbe632e23cd13819aac0b7454714d7ea5","impliedFormat":99},{"version":"6b97572b898559bde5ead60c3df72bea74dee67a9497f40ca2063af7cdf84bcf","impliedFormat":99},{"version":"d4c1d20b42f4c4b6710aae2ea61f16b05688871fc88c50164be241001c2ed2fb","impliedFormat":99},{"version":"4e93f1e1aed54628991b8092a5eff614ebd874ab3ff1f18903fb1ddd7543a45b","impliedFormat":99},{"version":"3758c87b27039448554b38154244bcc524735bee89342626ff9eb05b8834d05d","impliedFormat":99},{"version":"17e94aa1431a293fbb231edc284dc251555908f46e40b7648574725bd7acfbc6","impliedFormat":99},{"version":"0e1cab758e8f8c92c6d1b8b2a51fe46c31db41c462aaa21bd05929e3facd8b1e","impliedFormat":99},{"version":"6d37cb963c5288b5225af7fa9d072c429920718fa87d8352f72b5b80978aaf0b","impliedFormat":99},{"version":"e429692aefa26a42888b1e63959688bc0c75ef34fd3eb7c246f32b8872aeeb31","impliedFormat":99},{"version":"619d1633f5f39ba40bbbdef8613baa01cf242a1f70f9c0477b28a3211f2370fe","signature":"498e25119d58bd28ece6da55dc0c395a6f7cc59897155f4da92bd6eab82f6b46"},{"version":"370b83d6d96110053e63ee78938fafe0a9c893b6ddc08e7477bf241b91c20c83","signature":"8fc7d39a45aa3c24992fe36bb7839ec341dbcf7bd4e2987b5a2783b0c3f397dd"},{"version":"8d695b3774b714f73b6cbb390389ad2f3193e4f4f62c7e34984ad2326c888f18","signature":"c9a8078413759cfdd3f720dc9e034c98955887cfd68817fe231764fcc0f6ea08"},{"version":"cecbe4e72992345ef522f338bd7e7338a069d800a6b1d38539b639f8b83a0367","signature":"bd5d8d4128b3b2a4a38ca364b140dab68c68b5f19d3882d51cf7c47b9392aaf6"},{"version":"c7d3d5fa3b5916a86b4cfa15ff2287c4b99033974634248c4732d848045ae612","signature":"f6f17bf71d975dbadca58a5c181bff6014b9d53db53f3404d80d73a6cc2af328"},{"version":"05343aef23c8990348d58d87fbcd47d64a2bb6c0e4eb98f9af633bf747b6cf73","signature":"bd9cebf5aea15036a99fbbab8914601e163bfb3fa5a8bc5aa9822713d39fe71e"},{"version":"3a8f2320562326dabbe6093b3c61740997757cbd16ba12864fc9fd9363091e97","signature":"d31ad77eafe5d65cb8e7eba8ec6958e3406b3f74876df2675398dbf17c0dfe91"},{"version":"a538311c345949cc9c25d9fa687e6ded7d306ff4ef3bbb47ec1dfe850a309581","signature":"7606b20785e6f62ee46fc1890e53a4a824f59b6442389fec7db7ad1eb73fc022"},{"version":"b34890404502ee8a71f01e9b32caf020e71d89588173f1f5f10b830b11776f58","signature":"3faafdf02a62868541af7bf01d0aed7a5698fc894a120978a62ea1ad4492fa84"},{"version":"48e589080f69a8cd40fa2a08937dfd5003a4b8074620623b29906a74dc439ea6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4aafa314a294c32ff59181887260bd166656c18562f1609d9cacfe2106e9d5d7","signature":"b04135d4b71449cd8e67a8878a8a58d5ecd65c77011972258f65d26e7cf1b9ad"},{"version":"c82aa540e266447aa1cddc9419e8763ab8143e49f720043ff93816b6b02f98ce","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ff8a3540a563165756b576980733f694086d2194be10c5c7e9b51ffbbd4c0fd1","signature":"c977839e9336e5541d8461e5298712a6306a506d762c73564eac9bebecfb8aca"},{"version":"e16dcaaaaf9b6575168b27d0a18175d21a430371f30f625dced8df4d1e228cb0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ab4f6860892a6d59e3d8c04a9f195cc27c9dfd7d8bdc9005618db993a393c1c2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"1eb99355d91f03273d1068f8bfe1f99f83ef677328ea3eeb3fe0ef47245e0ea2","signature":"051b94a6cd2f3cae511e63f7b5e573ddf4e06d04a8658973f0fc1967cb72d294"},{"version":"63bd40218c140d01792a2649faf74327ebf9154e8143e77755a020515592a8eb","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"e04960cabaac62010909964ae19863386b41d43d39ab9fa1675a2d042900c910","signature":"f6289ac2f311cde6f534de63f64ab970e152123f5876d0f340aa8944db87c143"},{"version":"f4170fbf34ce6b0389878f2c475ae2153f42a2927e847b6e9b14a2d321e8e2c0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4aeecef6615b62f1c87505550a7293ed79d384e547638ef3368f1e1139a676c8","signature":"1163c69c506c6a8501957e3982395d56ceb11ae2d16db771929f78d495641e56"},{"version":"b28890cafea8971468be201da9a41f14e03f9df086ab516cb833e1b60b26d64b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ca3cf79417333e61e5ac11b3ef5196fe1aaeff717131e3359158328b97e8522f","signature":"173cc8c851f783e54b101f62d9dda7563df9d7bfaad2b647aebe40c4f5a60953"},{"version":"9d9681c05ec3d7727ae1468afe5f2d2e10634be8ed29c5c2a9ef1acfccdf7c64","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"0d968953ea99e3c652c4f27655bc3d19f8bb456866c9a434ccf6e83a7f249ffa","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"89b02824819d6efa8dab377a621cb6c2d09c5a51607c55eb276c8933f8e42fb5","signature":"a5df17ac4af58d3d3e09cde22268291b429cbcbdfd2d128ad65f2889c07bff92"},{"version":"48c9e5da30b52e7b8a2605c9a99a3062f788c5f4a64df383f553fc1bbc6bc586","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b114175f37c9c8d6d6e61c219110ca5f09e77a50a983950bae6c9830e5e672b5","signature":"778194f31c2dacc8973beb8904327d3a7f982c97b5d7462ccf937a581429fa8b"},{"version":"741a3d45b5ef82e29381353d96494eee81a2d9dc6c0b7237ff9e915fe4fcfc69","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f5ca6296c0197ddc5dc8e2ede74bc07e8ffb010e402d9031aad05935bd8fb4ed","signature":"194663ce8d586caef675a246b39aa86f7ab30c653d3e2401766d0cc52bfb44b6"},{"version":"3114c03fd3ec537066470342f8587a5512f0adacadae4a5d1b05e275cb01704c","signature":"33f04edbfd8687165c0b924c9c7dcd233048792911cb852ede409b2ed9982c2d"},{"version":"4624943f01c41a6ee4eeb326bb99144ccc7f84bf09cea72b04a5b216b30ea70b","signature":"dfaeb1e2d57e53b3f2b4c2a7506e46f1128d33c6b935f8e236e32848f26ac536"},{"version":"5bbe04c91ee41b0fab6b12c5b9660dd8c26cdf1f2bbb75690b0d3c99f16ef1f1","signature":"01a84e145cfdc39ca7318e8e46f431e49b289a7715c8cdbfe1297a11399e3931"},{"version":"2fd50355e32aba3b252e58b1b6f84078381eaa7db835503154efd59eed269633","signature":"aebcbabea1a5fcc3989730c68500831b6bf1c8763e30523acce69d25a85d2709"},{"version":"b1ba8f7d8664b1965cee1720697efec2df263e7e0d31242cd4f8ab0a3891f1a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"bfa544d9b15f168f41ed03430d49820b26ffcd012207c46a1625c2ef403a4f2e","signature":"7751ad05cbea188242c53dbb5b29b765e6160303b8f1efe34743caf20bf51abe"},{"version":"5641e8f1469ec49f4f2b5f27e4865cf240ee77fd012a94d23634d384b18a3e2f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2e2b1d8190d03c28abed18f5c4f7768339162d0da92b5619376bd0b9eea74831","signature":"40d2d310f365b4fb5f2796ae880901fa7c87f904976afc3525417e6b651442b6"},{"version":"25230ddd8c1fe680979c15aaa771e4ac15e6521a729c2d59d8f244f4c595a15d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"23c1fdacc80645510e9484fa710a3140dd6d9dbcf7e71e50be44d57c2f78e335","signature":"3ffa82795b12f4ae2f96768e399eb093048f4f3b623ed238d10f5ff1e839c725"},{"version":"1d03cf8d6ad3d5d370b8d05a88b3b08cce493aee26bcc8d0ab2d5cb6587bf1fe","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4ab0bac2aa8b6558968c5169e3e1f0a204317ef57e8144f86eeb558cd8af4eb5","signature":"4632114f72f93bcce0d7fd6f0e7cc0caa5d1ee5d385e2679f5caf11c0ff8a38f"},{"version":"65313f8f52bbc60e351bba4070e24c0ccc46fe73b7b9bd05875576625dc6534f","signature":"bb1c48e634d76de23d15ccf97bac8bda66c8fd514959dae65494c9cde2808609"},{"version":"5fe78dbd5c690b4fb35281b33854ba229a7bba7ccf9a188b147c84abcffbd4ba","signature":"a195b7a56ca781d46d3d7a556ef5f6af6e6bbff20ac71ac60e497829247752d1"},{"version":"62219a83631748f026e8af7eec54ddd58f85e7809d21ed684e0759a06e3ab5fd","signature":"5422c05a52ae74c87c282beb2561e0f26ab52fe73a9b9575a8fc2cb76cd79d76"},{"version":"0dd0a78c6b5523926fae4be11a8132035f0f5a988c2220c22a94d4377feb5251","signature":"b08d0d2cd36b530a9249aeca87fac3db99074107fd570ed0b1dee24a6720c5b3"},{"version":"179ec2be3ba2bd6cef09e1943bdefce84888c53cbf75dd0cc66e25fe73066cf3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"2ca412b5767dd81af30a59e60e3174c04cf59e49c89d675404abf5f3c5fba8eb","signature":"0b91a9b21cd5ad0cd265376cbaa5ad1cd6202825416c4bcf1609c6673d78a0be"},{"version":"d48da27c508d6bffaf72588e0d9fc749adff03b575cc20d9027c063c6c7c5a98","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"412859bcb9fca9198bc3ccf1c96237fff62f03345ea3c5f96f18c49462b2259f","signature":"920e406c286c1b48a00a712bd6869df6197866c11966095b4fced2a4d6942401"},{"version":"f1722a5276aee04daa7145c8e2bd29a24820e9657f251528eda17a30d7de94f6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8bc45826c7d7e7f553304d8a4b37b072b5663a51f2d33a389dad7cf75ce03423","signature":"48123915b9ba16e4bc3d308eb1eb7aaeb86dc0430b97fa83e8cbf6a29888d05c"},{"version":"28ed21c38dc56a3115c873e312aa3335aac2a11cae23253653bef25537c7d3db","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"894b2568a3f56d106d65d2e46c731cc8cc6b335a9c1dfee76a6193eaa79c9f6b","signature":"02ef334a3a2f20926816192ab2941ea3cb09636faa1286c2296be828869f03bf"},{"version":"5f15566bc9158450ac45ff3e4e1c2a3fcc0120bb19a18ce9258c33602ca9dcb6","signature":"e6a550648e241a622a4c69fa2f80e6ff23d38e05582c5cfbf4f2c0cbb96863d7"},{"version":"1941e12ac7464555d86b70e0a6302b91b1a1407ba037fa16c60790e15f3e2e93","signature":"a0cb0206a91ceaee5103cd13a3370a74249b3f45b576ddb1dd76804de2302167"},{"version":"706976704dfaddf508c933961bcae27e0053d8c772238b757605ad22c7e2f1d0","signature":"0dd4ef3114c29c6efb50dca6d70b26b848ce37d839f02976161da75d2d59127c"},{"version":"8139d708b00ccd6b7db5f14a87c7554acccfa089999f5e6727e681fb4c999cb2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"78d99658239c9fdd6d38802abb0e872202991c0a074c96e7c4056b9d422bceba","signature":"eaa105ab2d02c7c8ec63155bff6c1308b210e9ab6448bec935ecd3438e85d36a"},{"version":"cea7c4314d9f8462751a38f2fbd7b1aea8c1312f1d0e90df77b36e8302afad2e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"23fb0c2cf17a963141a6077b1f5cfead30943c4eb120bf81bb75ab5b3e80eec4","signature":"15a5a3d534b880dbef913890a97fb50db32658c03917ff3861d604d440a90907"},{"version":"6911e9471f25ba8fae13de0c072b72c66dad502e40f8b6c8740d5cf9ba0f2b3a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b9039512a27d6a5a687011f0af8ccb0adcc07218a8ddbb4b5ae02d62fece2742","impliedFormat":99},{"version":"d11ebedbe79c2e165456a142132ab453b2572938ed76a6fe9324914363b7d6ac","impliedFormat":99},{"version":"f6ba66f3f6c4409e878f48161529c585b3c0e687c8917dd8f6c6464fc5cd4f8e","impliedFormat":99},{"version":"0366e97d9c966d748ad91b782e8ce843ebb692d93a1d211ef5b84cccbe8f53a8","impliedFormat":99},{"version":"56497e1670f16495b1f17d3f4ebda63e5be6fe2fea9f26f0ff3e38568fc95f4e","signature":"308695a8fda6f510e51efb0430009a61897fb5067344c8d84a84496de1274a3a"},{"version":"0e6023f7b750aa03e8871830a8d02d5635ff10de1bab7e00040adb7b9852feb5","signature":"76fc5947686d1f4f95854ebdd328b964d50dc1067e43149867290203d03238b1"},{"version":"9482082c63c541680d7501d2a9c68dc9adfa4ae76e7b6cbfcf11839da9620f78","signature":"776f4607976ab87e84944a655484a4183fca74b8cbd17cb07d9a11a6ad28c12d"},{"version":"8e8ab957a73b47d61ab55b6c968d492d1d98a4271c1ea7bb9044b98545d75a5b","signature":"7017943360e6ac93765cab271d3b04f1b54ca26211e099453e4c9051b4d25df8"},{"version":"f736e672c15e293f44efda0d8f530cdac558d5396f0e7b2d666875a8b16e7943","signature":"178e81c7913720c7c7c48b66c4abac8bc4643bcfcd0633e0055079f724f4b73c"},{"version":"e551a312691df9bb4cb420112f040e571203a8dfc74f0989edc2ef475c5ecaa9","signature":"3f89da23f0c251a47f8534a0b51510d1d782de69990984e592c2cc629746140b"},{"version":"de0fc40bebc22ae7b3014a2ce51f29e5623be0436ff6a3cc858fa6417d8822ad","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6dbed82deb6e075663ddd982c37d544858cf5ca1a172298db093e8a49ee35cc8","signature":"80a5ca154cbf85949324181c775bd2ec7ec77dbbca27a29bbacc8aec4613d811"},{"version":"f16b100f317a322f5529e296714dcc99c19e9e5fb5916045624f8de197c02772","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"77277a37f6635718d6bb01c118b46c0f39c00417713978b61bc6ce5daca56939","signature":"26968396ba8985bd4a1c3e6fdeb001801e0c9e1f159f1cd4d6863bdb2276b9e7"},{"version":"483799096ed70750cb98ed3a868e8d5fa1df422756c75dca6bd4a182bca1a410","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6684293008b434c53e960a1c87f9aa2279730b46aedc963206e05351a21aa911","signature":"67fdc67fa1fd9a9022c6fce8bcd6f18b41747340fa20ec6a9a9f2e6bb274439d"},{"version":"97faeac00a2758e5c4b496f6b47d4a1294226a42b7956bfb0062600b548e52a5","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a5438e90c6ba81b6a34080ef1cafd101ee3c0aae9ff6031cd46bb8d59b81c3b2","signature":"eabb04fad55013cf12ee2e212586c4bdc9b245a0a6d4fadf3bcea95bf6bcf7c4"},{"version":"f02c001f18a3bc1d300b7ed52f97c4938b06c50fee761a7053a0183c6e7849de","signature":"53454454c0a7ad3aeef6f7e336a87accb3dd1dcc2abcb696e71619d10e047268"},{"version":"7da4b13f583065968785e4d99cbfffbc337fdb1764bf2ba2b3e63fb2a0249ddc","signature":"e043f7aeb977f236d72c39317e14816a043cd4dd3fcddea658ae06eb78524b21"},{"version":"19e5474a9d8c0a0dd667ec0c343a424ae6e3c4897898a00f440a42dc9976ddd7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3c392554d5bc28caf0738f363702e0003c38f10c280ac114141d373ad4703f29","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"da0f84fcd93700b4a5fbf9c6f166a6cc19fc798231bff56dd1e3875bfc6966eb","impliedFormat":1},{"version":"634ff08e0143bec98401c737de7bfc6883bfec09200bd3806d2a4cfc79c62aaa","impliedFormat":1},{"version":"90a86863e3a57143c50fec5129d844ec12cef8fe44d120e56650ed51a6ce9867","impliedFormat":1},{"version":"472c0a98c5de98b8f5206132c941b052f5cc1ae78860cb8712ac4f1ebf4550ca","impliedFormat":1},{"version":"538c4903ef9f8df7d84c6cf2e065d589a2532d152fa44105c7093a606393b814","impliedFormat":1},{"version":"cfcb6acbb793a78b20899e6537c010bfbbf939c77471abcdc2a41faf9682ca1a","impliedFormat":1},{"version":"a7798e86de8e76844f774f8e0e338149893789cdc08970381f0ae78c86e8667f","impliedFormat":1},{"version":"eebc21bb922816f92302a1f9dcefc938e74d4af8c0a111b2a52519d7e25d4868","impliedFormat":1},{"version":"6b359d3c3138a9f4d3a9c9a8fda24be6fd15bd789e692252b53e68ce99db8edc","impliedFormat":1},{"version":"9488b648a6a4146b26c0fd4e85984f617056293092a89861f5259a69be16ca5c","impliedFormat":1},{"version":"e156513655462b5811a8f980e32ccd204c19042f8c9756430fe4e8d6f7c1326e","impliedFormat":1},{"version":"5679b694d138b8c4b3d56c9b1210f903c6b0ca2b5e7f1682a2dd41a6c955f094","impliedFormat":1},{"version":"ca8da035b76fb0136d2c1390dda650b7979202dbe0f5dc7eaefcde1c76dee4f4","impliedFormat":1},{"version":"4b1022a607444684abeee6537e4cace97263d1ef047c31b012c41fdc15838a79","impliedFormat":1},{"version":"dd0271250f1e4314e52d7e0da9f3b25a708827f8a43ceff847a2a5e3fd3283e8","affectsGlobalScope":true,"impliedFormat":1},{"version":"47971d8a8639a2a2dd684091c6e7660ec5909fed540c4479ca24e22ac237194e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e1075312b07671ef1cbf46409a0fa2eb2b90bb59c6215c94f0e530113013eeda","impliedFormat":1},{"version":"1bfd63c3f3749c5dc925bb0c05f229f9a376b8d3f8173d0e01901c08202caf6f","impliedFormat":1},{"version":"da850b4fdbabdd528f8b9c2784c5ba3b3bedc4e2e1e34dcd08b6407f9ec61a25","impliedFormat":1},{"version":"e61c918bb5f4a39b795a06e22bc4d44befcefd22f6a5c8a732c9ed0b565a6128","impliedFormat":1},{"version":"ee56351989b0e6f31fd35c9048e222146ced0aac68c64ce2e034f7c881327d6d","impliedFormat":1},{"version":"f58b2f1c8f4bcf519377d39f9555631b6507977ad2f4d8b73ac04622716dc925","impliedFormat":1},{"version":"4c805d3d1228c73877e7550afd8b881d89d9bc0c6b73c88940cffcdd2931b1f6","impliedFormat":1},{"version":"4aa74b4bc57c535815ae004550c59a953c8f8c3c61418ac47a7dcfefba76d1ba","impliedFormat":1},{"version":"78b17ceb133d95df989a1e073891259b54c968f71f416cd76185308af4f9a185","impliedFormat":1},{"version":"d76e5d04d111581b97e0aa35de3063022d20d572f22f388d3846a73f6ce0b788","impliedFormat":1},{"version":"0a53bb48eba6e9f5a56e3b85529fbbe786d96e84871579d10593d4f3ae0f9dba","impliedFormat":1},{"version":"d34fb8b0a66f0a406c7ce63a36f16dda7ff4500b11b0bd30a491aa0d59336d1f","impliedFormat":1},{"version":"282b31893b18a06114e5173f775dd085597ca220d183b8bd474d21846c048334","impliedFormat":1},{"version":"ed27d5ce258f069acf0036471d1fbb56b4cb3c16d7401b52a51297eca651db62","impliedFormat":1},{"version":"ec203a515afd88589bf1d384535024f5b90ebe6b5c416fb3dcca0abd428a8ba4","impliedFormat":1},{"version":"32a2a1374b57f0744d284ca93b477bd97825922513a24dfe262cbf3497377d96","impliedFormat":1},{"version":"a8b60d24dc1eb26c0e987f9461c893744339a7f48e4496f8077f258a644cffab","impliedFormat":1},{"version":"3f9df27a77a23d69088e369b42af5f95bcb3e605e6b5c2395f0bfcd82045e051","affectsGlobalScope":true,"impliedFormat":1},{"version":"9fd080a9458c6d6f3eb6d4e2b12a3ec498d7d219863e9dca0646bdee9acce875","impliedFormat":1},{"version":"e5d31928bee2ba0e72aeb858881891f8948326e4f91823028d0aea5c6f9e7564","affectsGlobalScope":true,"impliedFormat":1},{"version":"9a9ba9f6fd097bb2f57d68da8a39403bbe4dc818b8ccd155a780e4e23fa556f2","impliedFormat":1},{"version":"e50c4cd1f5cbce3e74c19a5bbf503c460e6ae86597e6d648a98c7f6c90b596dd","impliedFormat":1},{"version":"fa140f881e20591ce163039a7968b54c5e51c11228708b4f9147473d06471cf5","affectsGlobalScope":true,"impliedFormat":1},{"version":"295eca0c47be1191690fd2fe588195fff9d4dc43852aceb8b4cab2aa634579f0","impliedFormat":1},{"version":"59ee7346e19b0050508a592702871dc943083c6dcb69a47d52e888115d840781","impliedFormat":1},{"version":"067712491fb2094c212c733dd8e2d56e74c309a9ce9dac9e919286b7245a1eb4","impliedFormat":1},{"version":"a5eae58ac55bd30c42359e4b01fb2be5eddac336869d3f04ffb4daa54b58f009","impliedFormat":1},{"version":"d12d691ef8933e8db39f2ca81d6973940ff5e37bb421752f5b6e7bc15dea3abf","impliedFormat":1},{"version":"4c5f8bd9b3a1aae4e4fddfee41667e495a045f73ed603993038fa6a8ba92fa14","impliedFormat":1},{"version":"dfb274ab0f319cf18ce7152067c25f984c7fd1924fc72b3f66734588444c934a","impliedFormat":1},{"version":"108c8c05cbc3fbbbd4ff4fc0779c9bef55655c28528eb0f77829795dc9f0b484","impliedFormat":1},{"version":"a7e5444d24cdec45f113f4fb8a687e1c83a5d30c55d2da19a04be71108ad77bd","impliedFormat":1},{"version":"41ec17e218b7358fcff25c719bc419fec8ec98f13e561b9a33b07392d4fec24c","impliedFormat":1},{"version":"23c204326746e981e02d7f0a15ab6f8015f9035998cb3766c9ddbf8ea247aea2","impliedFormat":1},{"version":"25f994b5d76ce6a3186a3319555bbba79706dac2174019915c39ac6080e98c7e","impliedFormat":1},{"version":"dfa4e2c6a612d43851ccbc499598cb006a3a78bc8c7f972c52078f862fa84e47","impliedFormat":1},{"version":"02c1705fa902f172be6e9020d74bcd92ce5db8d2ef3e1b03aabc2ac8eb46c3db","impliedFormat":1},{"version":"99d2d8a0c7bb3dd77459552269a7b5865fa912cedab69db686d40d2586b551f7","impliedFormat":1},{"version":"b47abe58626d76d258472b1d5f76752dd29efe681545f32698db84e7f83517df","impliedFormat":1},{"version":"3a99bbbbbf42e45c3d203e7c74f1319b79f9821c5e5f3cdd03249184d3e003ce","impliedFormat":1},{"version":"aaacc0e12ab4de27bdf131f666e315d8e60abec26c7f87501e0a7806fc824ae6","impliedFormat":1},{"version":"3b4195afd41a9215afc7be0820f8083f6bd2e85e5e0b45bb0061fb041944711e","impliedFormat":1},{"version":"108df8095f5e25d7189dd0d1433ac2df75ec40c779d8faf7d2670f1485beb643","impliedFormat":1},{"version":"ddd3c1d3c9ff67140191a3cf49b09875e20f28f2fc5535ae5ea16e14293a989b","impliedFormat":1},{"version":"7b496e53d5f7e1737adcb5610516476ee055bf547918797348f245c68e7418fe","impliedFormat":1},{"version":"577f44389d7faedd7fc9c0330caf73140e5d0d5f6c968210bff78be569f398a7","impliedFormat":1},{"version":"3046c57724587a59bceefadd30040d418e9df81b9f3cfd680618a3511302ed7a","impliedFormat":1},{"version":"15ccc911ed15397e838471bfe6d476c28deffe976c05cb057e6b1ea7491242c2","impliedFormat":1},{"version":"64b5a5ebdaead77a9a564aa938f4fb7a45e27cda7441d3bee8c9de8a4df5a04f","impliedFormat":1},{"version":"a48037f7af5f80df8973db5e562e17566407541de284b8dadf1879ea3aed8a2f","impliedFormat":1},{"version":"dab97d96ce986857150db03f0d435b44c060d126b4a387c7807f4e9f6c92e531","impliedFormat":1},{"version":"85f39366ea7bc5e34b596fc97de18a7e377856755e789d8e931054f2191d9b8b","impliedFormat":1},{"version":"daf3ea3d49f6e8a2fa70b7ca1f21bd97f1b65021b31fbfccb73dd55f86abb792","impliedFormat":1},{"version":"b15bd260805f9dd06cd4b2b741057209994823942c5696fd835e8a04fb4aab6b","impliedFormat":1},{"version":"6635a824edf99ed52dbd3502d5bce35990c3ed5e2ec5cef88229df8ac0c52b06","impliedFormat":1},{"version":"d6577effa37aae713c34363b7cc4c84851cbabe399882c60e2b70bcbb02bfa01","impliedFormat":1},{"version":"8eaf80ad438890fe5880c39a7bbf2c998ce7d29d4c14dd56d82db63bd871eefb","impliedFormat":1},{"version":"9b3e7f776f312c76ac67e1060e5398d7ac2c69d6a3a928a9daaae2eb05b15f56","impliedFormat":1},{"version":"202042eccb4789b7dee51ba9ecab0b854834ea5c1d6a3946504bfc733d4468c3","impliedFormat":1},{"version":"2b2ef76a9f36094b07ee6f76a5ac6903f2f65c0a20283201814a8d1e752cb592","impliedFormat":1},{"version":"8882e4e087d0bc8cc713cb3d8090c45d33e373e6f5c83e0f8d00fe6a950ef875","impliedFormat":1},{"version":"6325b4daefd385c07b6949daaf20a2723a75b3a86159f2bca3c53fe70f98e603","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"882b28abe64dae4932c83ebb71e4155da340929fe08a2055f3e573ef17f70fc3","impliedFormat":1},{"version":"4a3e425808751200a7709671667ad3d7e7cbfd0a06d469cab42adf06c2601f4a","impliedFormat":1},{"version":"401da46338f5b4f97c2a5f8a0faaace045c51aabd751d2dc704159f64feafe89","impliedFormat":1},{"version":"c705d4594093bcde53fc292c5526aedd3145170ceba73a9476ee97de6a915fe2","impliedFormat":1},{"version":"5ba85c521dc7008fef9e3d23b0b505fac64368af31d819fbacfb170045e383f4","signature":"9810c75cae8b5bb5a64856a1249d92898fe0bc08fb7edfdf26e7d1fb64674978"},{"version":"ec8579872bbc5589d66bb4cbdc0ef8bd681d79b8a814fe2bb392f02986920468","signature":"b2a6477636bdba25498d233db9cbd9d4aaacb75b90ca6fd8168554a8f6a1329a"},{"version":"a17a05c8f917ec2c80f31f3aa0c16d9742b93288832f1f9297e0765b2e3526a4","signature":"805a5ea0e11b94dbf29528d7ae91318d07549b5c4813f106921ce6abe3abe3f7"},{"version":"9ee1560888daaebf156ffe288e58a1b02b7127405caec41d3fb966af67f4b5c2","signature":"d5492346380d075b56787e452acfd7bfea5896bea56f64444d9170b0d003c9a9"},{"version":"4cd85a8230d17c0e948b99d06021edff6a35340e511dcd49a30b5f3a740fc5d7","signature":"96205459c4d05027bd9f523979b9e020162812330652fedd004ef39343cd8cb7"},{"version":"887cfea1dbb87aa317bbfe0ad3e85fdeab7e0eafd040d8339fc3e1ae43ea95dd","signature":"9b365a218919f6dade45f83796f8f4b0e820871ac894a1991010fa77ab0619c0"},{"version":"7999dc74b57cc2dfa1640ac746bb755420a4375541732f4bff420751a375d7a3","signature":"ece1d6742ed1d0860af638493866df8b946c3f583875e62e22264d7454f8bb46"},{"version":"6856bfd11dbca6b6cbc812621a11ed11c4b4296d7b971c33e2ffcf1b7060a5f4","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"83530f88eb02119125b1730602056e1dc2a7689fb1f4e3a8eff57602e0cf8791","signature":"e23e63f94215cf92ed44b17c81a5cc023571e6e57668d286d0f66dfb7fd29e64"},{"version":"02dd581476d418e9d891f9bc234ba68f0a22bbb92bd4fd5402e62367572cfa3c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"08eb7930ae7af32aa4070200e2209a82ca0d5191f4c6a56dcb3d7af666b61d10","signature":"0cc69bfa0dc106cc3e9f90bcda9fc24745adb88adc9aed46cec3211ca418bf30"},{"version":"d779cc3dfaea48cc4e8d9225bce2a930fe7f5c19d35f1362bdd915044261768f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"dd218a0ea3d23ec928bbfa257718e39a6064f56115d0e47678a10a5f86182920","signature":"3a12ea40f44d366677ba0e45a0817a8b4b4efb30aa40fbfd559c99465078e197"},{"version":"86153db5e23d65b4cbae5b7ea8e730119094faa1865aec8373cdca4849c08d7c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3f8c06236d42491454328842e4b8d96bff6ef3d6ba897677cf012eae9a8a8531","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"fbad2026e5d0aca8314c0e69b47a599e03a96a00bdb09bc5dbeca747aa71f1a3","signature":"dbf7d075c346bf692337c676353683710738fc6cdfed3b25b4ab26858a25014f"},{"version":"d20530ea19f436fd93bdd18d96ade390f04712323e4d8811bd2f8453182129e3","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4e27d5f69742c8bdf2da332909d403778a263ff986536d9adbc768a0f9eeaf11","signature":"768c2dfab2b90c6569c42a473907528eb55a45c3ed0d6256e35a8becaad3d8c5"},{"version":"6a3e54c5512881e1a8a4a0199482a0e68dff96200fbcb7a08b38a969d2cd6699","signature":"cfaa98bda2da5362425dc511b0c2447d3b25087f0b74c61e1990f47a20913766"},{"version":"c635a564785c5f590f4ece775c5df77f97d17f34c48a649678e845510f1b446c","signature":"21b4f8dcaed267776a667be6fa7f28b8fd59c1c8c0fa2cc517c0192b8f06c31d"},{"version":"799f40d6da71436ad2d692688e04566bc8c1fcd1926a254af306fa8ae9ff4aa8","signature":"995274a829121df36f8d9cc22f7a63ec3bd987fcf732eb65962dea5570751876"},{"version":"2cbdee534591575c81aaf2d99884635361a510a470bb83dfd2269eb6c92b2261","signature":"ff371292b082e5e53f6fdce86ad4ed7771991b553916741aaabcb63196b69d4f"},{"version":"510a6f4314bfbc0b4ee033439734a3c3c3a7bdf0705145e3c654ef372a3b579d","signature":"53e4c6a61b47ecd85ceb70d1d4512130dcc531c850b3b6bfa0ee82ff83a70698"},{"version":"e65cb4ccf7c87a1d754e9b9a6e94d084c7ab28e16f8a78cfda92a6f5918a69e0","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"613bd3011a678abe67087d230ea80ffd528d4e28955502fe7f4774362e77bbcb","signature":"49ef7c07fd81b587af6908134ec0e249b2fe2ae26d8326492cabe8030f6e8693"},{"version":"bc9c4225c4c1dab675f49a71a8d971eee3e2e543f1771c14276b45010f6ffaae","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"47684074834c5e06d27d5b3e0db002782ec0f2dea069aa2135e590118c9362f7","signature":"2823ac5458951858044bcf11891ec9ea7838615d0bdcba0817aadc71cf1dcdef"},{"version":"41a457cad7ac02cacd0c9fccdbcf80d2b44bd92bd0d4875b69c94c364a6e401e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9f9c05a43a0a2d8e72c00f9173e85b6fad81e8499961cfa611fa905a257c638b","signature":"17604e5e845fc8ec8fb28f8fd4c99fce6f29b8f67eec439bab4fcd479ca8a50e"},{"version":"5052f6d600f9d1e9cede06feec1faf439378112b9c3c64e9311e6b495df5e393","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6a3e54c5512881e1a8a4a0199482a0e68dff96200fbcb7a08b38a969d2cd6699","signature":"cfaa98bda2da5362425dc511b0c2447d3b25087f0b74c61e1990f47a20913766"},{"version":"76b0c789fd0a3b5335a1cf8aa8ec3a07c8384c22f7a3de18e858f57442de30d9","signature":"315bc7c950d58772966319f8401256cbf36525ea88146a7f78073fdabb3a9388"},{"version":"3ece61c6c08abccf9a57edbc4ad491f7fee0976b510f2eb9a902989353ea1796","signature":"13dbd27bb1d1ca3404cc28d37239454129f6ab96aa921294af5bdbc0acfd4c6f"},{"version":"1ab721520d98f380fc2eecbea24559da263bb12c054ffe407653e20a779d33b0","signature":"4a6172213bc97ac83a39d9c3ba59a98239476225d76cd68004f66e1e0adae847"},{"version":"069b2f5c89a854b8d0777017d287ce6603eb74f8db3f1c207a699c9d830ed276","signature":"b562f48096a09af4b58c0f90e6f1921065cbf0bcc25fccdf75d21d5faca587a5"},{"version":"33173d2b3c235c36cc9e77882f146d21e429f0383e2db5483b14ddefe9a3b066","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"6e4c5d3ba96c381b0534c4985f099a284d77b69d2d6c2da77f09bbacb0d0b183","signature":"0d9f9121e609de171240a484d3f39b5060bafb8d8534d3b6e95addc07501583c"},{"version":"ce73073c216074a4015ff67a3a4fae8cd6f2b27cfc3b61df2c82f86b8ff90b6b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cfa4b4101c50834e5d7173fbe21ca806365ba53e8d8d11285310670f043f7a8a","signature":"c0d9f1c0d6656828f9d79b6d490457d5c98fb93c2e8ef8e3aeab889bbc57c543"},{"version":"31bb743670487e571f1477dc0100cea116bf9f13f235dfcf7eed78353baafe3d","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"843808a59deb22ee90a34b86a22487ca183dd7316115edaa12707a8ef764ed00","signature":"eb0881c5e929fd7fb41dbaea899cea8453ebbff25a53b40bde3e722dca90b2ad"},{"version":"faec6f376d10c439d4c1a16a386e1cca77353c9d330431699e300ee0dbdf6045","signature":"1f71876bf509d705f6dadfd7dfbbb169e6b2d024697691d6037e9ddc3c0c577b"},{"version":"7fb02a4d0ba956a908489ad5a14afe53ea8a6af1671f9563790b8d51adf6a18e","signature":"9cae4fbcaab119861558b35bc06b902c5e12b56cc61fe00b09b74c1b2753ecd8"},{"version":"8a4ca198b7c28931aaca474d2e53f36e111005a8f01f59ff146d96dadef42044","signature":"102d68887b41127a6195e88f8f68bfc1ed172389c1398c194218f8d6ca22cfc4"},{"version":"45a4b3a6aecea5c2a988907602b904792f62a432fbf60b3a2567f8b8f80abcc3","signature":"5ea51e2a055800151ae8c66f7c757c61af6bb7e5aae59d3f930aa2e3fd2aafcf"},{"version":"c8c23d40c91ea3e482e770a495036472551e4e3d63805d7e59f6b529aa80ee7b","signature":"566de8a1ba7227562827de899c257a5c4cb2f52f688526226ff9a3a77b6d81f6"},{"version":"aed84edf8705c35fd9505a2f4fabfee7e40fb40df3c604546c26cb6a75c82b5a","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"a21b16e26fefcf0fa3501377571130b6b1dfea4e4e39ae9ca49e5202d397d289","signature":"3b3104ebcc3528ad6233c8b56eb2851ba71709eb570c6f3375e5af1ae4da6e9c"},{"version":"1b4c4ddb69c3c6aced74dbdbc0cb0952d54c2f3f525f7539dc7f8b879ff398ac","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"d4aafc88f449b3dece1139b101063857a243632c11cf46d7a237947c60aaf73d","signature":"ba2014afd9d39b84d2a3d448395b5bfefce1b7a643bb0b65a7b7bd96e871b48f"},{"version":"699b6a641c44aec0620bcac676d4ba9c952935d59ba95995ee2f95cf0df9e4b6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"5a85448d059a9707654aa9862b15002204eeba605295f72b7adfac3f06b15e5f","signature":"b992b8d365a25f19870ac2673d0c934bbc07693d412bc6e0dc3348690f080e65"},{"version":"cb09494c8782717711e8cf5272e3487b93ba8d06b1f739e547d9685bcb23350c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"219de40f19c7d6f958c19b656a60c6111e9c0b134b3621fea6a921dab795575d","signature":"b372ed7a377647cbf7b25df64b2801a596b28a57f5ebe7ea2ebf637482503238"},{"version":"644b85acd27474f4ca95c499429ac548bbe2a4502ead86204fbe319762f2478c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad9eb59127a4d0523892c0849ad0a8ece6d0affc43f1cd31cfe822357e2825f1","signature":"ca2d5a2abb4eb48f173d4015b325550d8a0c2b2bdfb9c5b36f9a61531c06668f"},{"version":"cc3127d12e27b24862881f34a1b8819deba2c72cbb5f82e9e09f481cfb9f08ec","signature":"6297275caa40f3a2b188c436d6c30a93ceefea5342c419e8b696ee5c9f8a5595"},{"version":"94fab564ab8fb55af74a70509403921c86fe03d19633811a40f959fbdd1b2730","signature":"f96f3f7e1612d413c95a66e638a4fec032fd9653f3a6210ec79e5830fcfe5761"},{"version":"b6e6b0a412ff6fdb88ce7f49bd887583b1ceaf5855b6180fd820b266a46c83c4","signature":"675dc0498b58f7e94e3b28fc542c50db2a2176fbe2a5f5013013b1033b688ba1"},{"version":"38d9f96b332c0b24da4d3a3367626af48004b065bb7fd1e95c22b88da4c4986b","signature":"6af03c8615fb7afb7f610384616178c60e5cc43124d3ace60d60b5fea8308af3"},{"version":"48a2da34607388aaa01eed1bd40fbdc9ab1b9de9a82a882e8f2b9099e7dd61e0","signature":"cbd70b0bec138f5d35584afa22ceee7eac21d579f6a0052a4d50fe445a0acdd4"},{"version":"671d5db9155f51909fa66748be739332b1052b841bd3a896606d02ddb689074c","signature":"2a33371cb98b40596420d07b9ff28fac6bc0da83e6f774350376538f255fea8a"},{"version":"3369191f57b3f65105ee8f2186a7b2d7be9750a53574e5bd309be00825d5c244","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"997c98da784db39942db1efa0d850088df1da2bc1a04c9c7fd90c9a2b21904bc","signature":"295371aa99b13288c58c44dbcf52c22c9b6ff0bca44b8a544ce31ef710033ff3"},{"version":"f74357d99a2413be1b753a97bf2c436cecb581d8fbb9631e222ace9226e298d9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"dc5546c1213036db022c5ddf2879e8937abf18a8c8251a0560431482ad46b357","signature":"ada185e845c9ebd71e720a1794e7fc1d6362fc35692d62a918b965dc0800285c"},{"version":"213211567b80be96c91882e0ef5517ec58c3a4c137e497fda3531298462f2e93","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"edab3e0f19ae936330c970a57fde46cace366feb30992e241ff4a216c5265bd8","signature":"233d2d80a6eba34812e44e27ddc1e0d04c95aca7ac4d981ab432dde3b6ea6ee2"},{"version":"3a1b6cae1a558f9282a5e02fef2c73127f796239c52f6499e2d360e2f96ae68e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9bc5f0f923a7e8c610dd1a4931effd93e741c3d214fe9c92ca33849ffb8602ec","signature":"777b1090495d3301be05b674c1716d3dea2caac13ec0a8afdf5d30a2c02fc4b5"},{"version":"d9bf100319a2f94c431bc7e65433ba0907fe9479f865299be35e924b692414fe","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"57f89d3e2d3321849da9ae7f95e4b1e066a46d9f9ad2918ac92e5c162a55a41e","signature":"6f937a36a0554c0813743f5ada9665f6bcb67d0d2839ea9c404a5aec7feef8de"},{"version":"4836c444f4aa4a95580aef5461a01e64308da39bd0cffcafde960eb359092b30","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"588addb4eb7f34d4c962d2cb0c45ad24698f7a70b5d8c7973297a5f5ca940f3e","signature":"b32d06279ac12db33a254ce2424c37b0e757502d55ae1dacf7d77240842a07cb"},{"version":"1bdaaa929028e63260ee0b975c2287efd7f44c1696c9673be52e412d29882012","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cd0f1cb9083130a1097493a9f6cb24131a4e9bb44c6a844f8599508250d6c480","signature":"a0126b2500faf07aa59d3709c18de53b15162f63373203d0784cca319b1e960b"},{"version":"1a6791eda07ec1773ac54d49702a624735b74311e380e93871470a211137bcb6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"72f8b6e281615cecba1e5507d2facd65664e5d6cd86d4e9a93d1f7098418086c","signature":"ca1eee8eda1dcec392d95c606fabf41ded7f9e872dfe8f1a9c52db8795af332d"},{"version":"d5ef090496ad9a7606522d5c3e876387a14efa0e1047364dab0a177f234d7907","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"899caf3419de9c34926f6706348ef57abbf7db30c87003627e598a155b7e0c78","signature":"08b19930b4ba7c5a6eeafab4eac1c3b2ff7be1cf733f566fa4c8c93f3618d847"},{"version":"0921c871035f591a549407a9333e8bbefb59252c66d9c37b5378f705e9b5225e","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"c561cbe6385a58164f5f8635fd27ce5f9f742aec3bf1a7a0b03c32ec3199ec31","signature":"3050c3f3df8238f4ff45a8f8f38ea5ad94ad7e2b9bdc259d0aaddcc511dc91fd"},{"version":"c0a07e39216c64633b28b826115227a3bee607d69a5272f88e2c5d8da3a5932f","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"64f2ecf30dcc0ea64804a831a9a8da446b6c634d0dfafa44a2a1f5235558bb5b","signature":"b4bf6c60a7c19eabea44ff7ccb1cf608d20fa0a72a93ab170587ec1eabe613c6"},{"version":"efb5303e134c6c6348ddbe69c79bf1bdfde3b061f6993cf29684b9a4d68562dd","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7f24bb51cea61986ecce977b2bd9493e322386b68e10d5d605d947873e2804e3","signature":"2628e648dc231f207f77a9996a765453fbc5b751716c6d14d06d82f46c8834ce"},{"version":"f1a039a40bf3f6dffa76f07e26b71c4783377396501c12e2a1961bdc1190643b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"9de5dc3aa0f8d23427555e44776e3c84c6c3f013de25a43173fd63750f9628a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"04471dc55f802c29791cc75edda8c4dd2a121f71c2401059da61eff83099e8ab","impliedFormat":99},{"version":"8e5a1adaae977af48997dec89d6cc6075aee453b661faf7252b4b3a44c7f2bbb","impliedFormat":1},{"version":"5c54a34e3d91727f7ae840bfe4d5d1c9a2f93c54cb7b6063d06ee4a6c3322656","impliedFormat":99},{"version":"db4da53b03596668cf6cc9484834e5de3833b9e7e64620cf08399fe069cd398d","impliedFormat":99},{"version":"ac7c28f153820c10850457994db1462d8c8e462f253b828ad942a979f726f2f9","impliedFormat":99},{"version":"f9b028d3c3891dd817e24d53102132b8f696269309605e6ed4f0db2c113bbd82","impliedFormat":99},{"version":"fb7c8d90e52e2884509166f96f3d591020c7b7977ab473b746954b0c8d100960","impliedFormat":99},{"version":"0bff51d6ed0c9093f6955b9d8258ce152ddb273359d50a897d8baabcb34de2c4","impliedFormat":99},{"version":"ef13c73d6157a32933c612d476c1524dd674cf5b9a88571d7d6a0d147544d529","impliedFormat":99},{"version":"13918e2b81c4288695f9b1f3dcc2468caf0f848d5c1f3dc00071c619d34ff63a","impliedFormat":99},{"version":"120a80aa556732f684db3ed61aeff1d6671e1655bd6cba0aa88b22b88ac9a6b1","affectsGlobalScope":true,"impliedFormat":99},{"version":"cdbc46ed576c5d7c0ec7f51c628a136ba25cfa050148d5ceaa3ccfc2f228f91c","impliedFormat":99},{"version":"5d2bcff208b51a9e89e69c72ef77ee0d8ec631955709d58f27d6b28cb9f500a2","signature":"871edfa067149771f512d8a77c2167eec9e532825fac0da705a31b62082bed71"},{"version":"cdd2c537282e5b19b1334da23868924c3b24efd7c1c81973d6943fd0816be588","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"42e73b2f164621c10cac49c2df0666983657dd29932a8fcc52951040c57c3f89","signature":"c968550f9d6302f54147c45eb86fbb741711b8fcde9470071d016c5cb45306b0"},{"version":"7b1fd7f07aa61be279f29c42f7dc8a876478f9a10cc49bbda4551db640a771da","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"7e50d2807063b83ffafd2655d9928e646d9d061cae4e68af73ecb132af825ca7","signature":"196cc58681e570c9eb8969a03df1a4c09ca1871fb30c0fa8731f16b76031df42"},{"version":"0d82601d36ed5bfcd384f00489072984fc418c09861bda1187f515426901cfc6","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"ad42288f8c9ebfd4451e4256a2f091cfc26b958d29db3612c19efbbb476882a2","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"59b54146333f3ad62db9f636c988f09378b61c55c4bde0541602011d458a1ca9","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"b7724c5f8c92bf25a109abae1de5f939f4b2f08421c288329bbc01bcc515a265","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"d1986184a09a52db8228cb2bb2a61a8c05c9354e5b93cec8e2628d8579c892d7",{"version":"f486c50bb72b9ee6ee6ee90d4547d215a99b43dcf475839c945bf4f6359e44d7","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"8d7cbeea0454e05a3cdf3370c5df267072c4f1dc6c48a45a9ad750d7890443d7","affectsGlobalScope":true,"impliedFormat":99}],"root":[[553,555],772,773,779,[802,809],[860,962],967,[970,976],978,979,1032,1033,1039,1116,1117,[1127,1131],[1133,1141],[1167,1204],[1215,1285],[1296,1356],[1361,1378],1456,[1461,1548],[1561,1571]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":4,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[1570,1],[553,2],[1571,3],[554,4],[773,5],[1178,6],[1179,7],[1140,8],[1180,9],[1181,10],[1168,11],[1182,12],[1183,13],[1169,14],[1184,15],[1185,16],[1170,17],[1186,18],[1187,19],[1173,20],[1188,21],[1189,22],[1172,23],[808,24],[807,25],[1175,18],[1177,26],[1176,27],[1197,28],[1198,29],[1190,30],[1199,31],[1200,32],[1191,33],[1201,34],[1202,35],[1192,36],[1203,37],[1204,38],[1193,36],[809,39],[1194,40],[1196,41],[1195,42],[1222,43],[1223,44],[1217,45],[1224,46],[1225,47],[1218,48],[1219,49],[1221,50],[1220,51],[1233,52],[1234,53],[1227,54],[1235,55],[1236,56],[1228,57],[1237,58],[1238,59],[1229,60],[862,61],[1230,62],[1232,63],[1231,64],[864,65],[863,66],[866,67],[865,68],[870,69],[869,68],[868,70],[867,68],[872,71],[871,68],[874,72],[873,68],[876,73],[875,68],[880,74],[879,68],[882,75],[881,68],[878,76],[877,68],[884,77],[883,68],[888,78],[887,68],[886,79],[885,68],[894,80],[893,68],[896,81],[895,68],[891,82],[898,83],[897,84],[900,85],[899,84],[902,86],[901,84],[906,87],[905,84],[904,88],[903,84],[908,89],[907,84],[910,90],[909,68],[913,91],[914,91],[915,91],[916,92],[911,91],[912,93],[922,94],[921,68],[920,95],[919,68],[918,96],[917,68],[926,97],[925,68],[924,98],[923,68],[928,99],[927,68],[930,100],[929,68],[932,101],[931,68],[934,102],[933,68],[936,103],[935,68],[937,91],[938,91],[939,104],[892,82],[941,105],[940,68],[943,106],[942,68],[945,107],[944,68],[890,108],[889,68],[947,109],[946,68],[949,110],[948,111],[953,112],[952,68],[951,113],[950,68],[1245,114],[1246,115],[1240,116],[1247,117],[1248,118],[1241,119],[1249,120],[1250,121],[1239,122],[954,39],[1242,123],[1244,124],[1243,125],[1264,126],[1265,127],[1257,128],[1266,129],[1267,130],[1258,131],[1268,132],[1269,133],[1260,134],[1270,135],[1271,136],[1259,137],[1261,138],[1263,139],[1262,140],[1272,141],[1273,142],[1251,119],[1274,143],[1275,144],[1252,17],[1276,145],[1277,146],[1253,147],[1254,148],[1256,149],[1255,150],[1306,151],[1307,152],[1298,153],[1308,154],[1309,155],[1297,156],[1310,157],[1302,158],[1311,159],[1312,160],[1299,161],[1313,162],[1314,163],[1300,131],[1315,164],[1316,165],[1301,166],[1303,167],[1305,168],[1304,169],[1317,170],[1318,171],[1279,172],[1319,173],[1282,174],[1320,175],[1321,176],[1280,177],[1322,178],[1323,179],[1281,180],[1278,181],[1330,182],[1331,183],[1324,184],[1332,185],[1333,186],[1325,137],[1334,187],[1335,188],[1326,189],[955,39],[1327,190],[1329,191],[1328,192],[1283,193],[1285,194],[1284,195],[1342,196],[1343,197],[1336,14],[1344,198],[1345,199],[1337,200],[1346,201],[1347,202],[1338,203],[956,82],[1339,204],[1341,205],[1340,206],[1353,207],[1354,208],[1348,209],[1355,210],[1356,211],[1349,212],[1350,210],[1352,213],[1351,214],[979,215],[1368,216],[1369,217],[1362,218],[1370,219],[1371,220],[1364,221],[1372,222],[1373,223],[1363,224],[1365,219],[1367,225],[1366,226],[1378,227],[1375,228],[1456,229],[1374,230],[1377,231],[1376,232],[1469,233],[1470,234],[1461,235],[1471,236],[1472,237],[1462,238],[1473,239],[1474,240],[1463,241],[1475,242],[1466,243],[1476,244],[1477,245],[1464,246],[1465,247],[1468,248],[1467,249],[1485,250],[1486,251],[1478,252],[1487,253],[1488,254],[1481,255],[1489,256],[1490,257],[1480,258],[1479,82],[1497,259],[1498,260],[1492,261],[1499,262],[1500,263],[1493,264],[1491,82],[1494,262],[1496,265],[1495,266],[1482,253],[1484,267],[1483,268],[1508,269],[1509,270],[1503,271],[1510,272],[1511,273],[1501,274],[1512,275],[1513,276],[1504,277],[1514,278],[1515,279],[1502,14],[957,82],[1505,275],[1507,280],[1506,281],[1524,282],[1525,283],[1516,284],[1526,285],[1527,286],[1517,287],[1528,288],[1529,289],[1518,290],[1530,291],[1531,292],[1520,293],[1532,294],[1533,295],[1519,296],[959,297],[958,39],[1521,291],[1523,298],[1522,299],[1534,300],[1535,301],[1226,302],[1536,303],[1537,304],[1139,305],[1538,306],[1539,307],[1033,308],[1540,309],[1541,310],[1128,311],[1544,312],[1545,313],[1134,314],[1546,315],[1547,316],[1136,317],[1542,318],[1543,319],[1137,320],[1548,321],[1117,322],[1563,323],[1564,324],[1129,325],[1565,326],[1566,327],[1130,328],[1561,329],[1562,330],[1131,331],[1138,332],[1127,333],[1039,334],[1032,335],[1135,336],[1361,337],[1116,338],[1215,339],[1141,336],[1167,340],[1133,341],[1296,342],[1171,336],[1216,336],[971,343],[972,344],[973,345],[1567,346],[974,347],[861,348],[860,349],[806,350],[960,39],[961,39],[962,39],[1174,82],[804,39],[803,82],[967,351],[970,352],[805,39],[1568,353],[976,354],[1569,355],[978,356],[555,357],[975,82],[779,358],[802,359],[772,360],[1124,361],[1123,362],[1122,363],[1126,364],[1125,365],[1121,366],[1028,366],[1029,367],[1360,368],[1359,369],[1358,370],[1357,371],[1092,372],[1093,373],[1208,371],[1207,374],[1206,375],[1212,376],[1211,377],[1210,374],[1205,374],[1147,378],[1209,379],[1081,380],[1064,381],[1066,382],[1062,383],[1079,384],[1047,385],[1082,386],[1067,385],[1068,387],[1083,385],[1077,388],[1069,385],[1071,389],[1072,385],[1073,390],[1070,385],[1046,391],[1074,384],[1075,385],[1084,392],[1076,380],[1085,393],[1078,394],[1080,395],[1045,2],[1145,396],[1146,397],[1213,398],[1214,399],[1025,400],[1144,401],[1036,2],[1291,366],[1023,2],[1024,402],[1142,363],[1027,403],[1065,404],[1057,363],[1087,405],[1088,406],[1097,406],[1096,407],[1099,366],[1098,366],[1115,408],[1114,409],[1100,366],[1101,366],[1102,410],[1103,411],[1104,405],[1105,407],[1107,406],[1106,366],[1095,412],[1090,413],[1094,414],[1089,415],[1109,416],[1108,417],[1113,366],[1110,418],[1111,366],[1091,419],[1035,420],[1034,366],[1160,405],[1153,406],[1164,366],[1163,366],[1151,366],[1166,421],[1165,422],[1158,406],[1159,366],[1157,366],[1148,374],[1156,366],[1155,410],[1152,411],[1154,405],[1143,423],[1161,366],[1162,366],[1149,374],[1150,366],[1112,366],[1132,424],[1289,425],[1295,426],[1294,427],[1293,425],[1287,425],[1286,371],[1292,428],[1290,425],[1288,425],[1026,429],[1038,430],[1037,404],[1061,431],[1060,432],[1058,2],[1059,433],[1086,434],[1051,363],[1054,435],[1052,2],[1055,436],[1049,437],[1050,438],[1056,439],[1053,440],[1063,363],[1041,441],[1043,442],[1044,443],[1040,2],[1042,2],[1019,444],[397,2],[560,2],[562,2],[563,2],[565,445],[564,2],[566,446],[1013,447],[631,2],[776,448],[1015,449],[1021,450],[1020,451],[1011,452],[963,2],[964,453],[965,454],[966,455],[797,2],[794,2],[793,2],[788,456],[799,457],[784,458],[795,459],[787,460],[786,461],[796,2],[791,462],[798,2],[792,463],[785,2],[783,464],[782,465],[1550,465],[781,458],[801,466],[1441,467],[1442,467],[1444,468],[1443,467],[1436,467],[1437,467],[1439,469],[1438,467],[1414,2],[1416,2],[1415,2],[1418,470],[1417,2],[1381,471],[1379,472],[1382,2],[1429,473],[1383,467],[1419,474],[1428,475],[1420,2],[1423,476],[1421,2],[1424,2],[1426,2],[1422,476],[1425,2],[1427,2],[1380,477],[1455,478],[1440,467],[1435,479],[1445,480],[1451,481],[1452,482],[1454,483],[1453,484],[1433,479],[1434,485],[1430,486],[1432,487],[1431,488],[1446,467],[1450,489],[1447,467],[1448,490],[1449,467],[1384,2],[1385,2],[1388,2],[1386,2],[1387,2],[1390,2],[1391,491],[1392,2],[1393,2],[1389,2],[1394,2],[1395,2],[1396,2],[1397,2],[1398,492],[1399,2],[1413,493],[1400,2],[1401,2],[1402,2],[1403,2],[1404,2],[1405,2],[1406,2],[1409,2],[1407,2],[1408,2],[1410,467],[1411,467],[1412,494],[780,2],[634,495],[632,2],[1012,2],[155,496],[156,496],[157,497],[94,498],[158,499],[159,500],[160,501],[92,2],[161,502],[162,503],[163,504],[164,505],[165,506],[166,507],[167,507],[168,508],[169,509],[170,510],[171,511],[95,2],[93,2],[172,512],[173,513],[174,514],[214,515],[175,516],[176,517],[177,516],[178,518],[179,519],[180,520],[181,521],[182,521],[183,521],[184,522],[185,523],[186,524],[187,525],[188,526],[189,527],[190,527],[191,528],[192,2],[193,2],[194,529],[195,530],[196,529],[197,531],[198,532],[199,533],[200,534],[201,535],[202,536],[203,537],[204,538],[205,539],[206,540],[207,541],[208,542],[209,543],[210,544],[211,545],[96,516],[97,2],[98,546],[99,547],[100,2],[101,548],[102,2],[146,549],[147,550],[148,551],[149,551],[150,552],[151,2],[152,499],[153,553],[154,550],[212,554],[213,555],[218,556],[482,363],[219,557],[217,558],[484,559],[483,560],[800,363],[215,561],[480,2],[216,562],[83,2],[85,563],[479,363],[249,363],[778,564],[777,2],[663,565],[771,566],[659,2],[662,567],[672,568],[660,569],[661,570],[1559,571],[635,572],[641,573],[642,574],[640,2],[1551,2],[609,2],[618,575],[617,576],[637,575],[620,577],[622,578],[665,578],[621,579],[1549,2],[629,580],[628,2],[1558,581],[1553,582],[1556,583],[1557,584],[1552,2],[616,585],[610,586],[657,587],[612,588],[614,589],[656,2],[643,588],[613,2],[615,586],[611,2],[1002,2],[1004,590],[1003,2],[633,2],[988,591],[987,592],[986,593],[994,594],[995,595],[992,596],[993,597],[990,598],[991,599],[989,600],[1031,601],[1030,602],[968,2],[84,2],[577,2],[651,603],[653,604],[652,605],[650,606],[649,2],[1458,607],[1457,2],[1459,608],[843,609],[812,610],[822,610],[813,610],[823,610],[814,610],[815,610],[830,610],[829,610],[831,610],[832,610],[824,610],[816,610],[825,610],[817,610],[826,610],[818,610],[820,610],[828,611],[821,610],[827,611],[833,611],[819,610],[834,610],[839,610],[840,610],[835,610],[811,2],[841,2],[837,610],[836,610],[838,610],[842,610],[601,2],[603,612],[602,2],[1022,363],[810,613],[1118,614],[849,615],[848,616],[855,617],[857,618],[853,619],[852,620],[856,616],[850,621],[847,622],[858,623],[859,623],[851,624],[845,2],[846,625],[1120,626],[1119,627],[854,2],[977,363],[505,628],[510,1],[517,629],[500,630],[253,2],[261,631],[401,632],[404,633],[376,2],[389,634],[396,635],[278,2],[378,2],[259,2],[375,636],[421,637],[260,2],[251,638],[403,639],[405,640],[406,641],[477,642],[370,643],[323,644],[383,645],[384,646],[382,647],[381,2],[377,648],[402,649],[262,650],[447,2],[448,651],[289,652],[263,653],[290,652],[326,652],[229,652],[399,654],[398,2],[388,655],[495,2],[238,2],[516,656],[455,657],[456,658],[452,659],[534,2],[353,2],[457,660],[453,661],[539,662],[538,663],[533,2],[304,2],[356,664],[355,2],[532,665],[454,363],[309,666],[316,667],[318,668],[308,2],[313,669],[315,670],[317,671],[312,672],[310,2],[314,673],[535,2],[531,2],[537,674],[536,2],[307,675],[526,676],[529,677],[297,678],[296,679],[295,680],[542,363],[294,681],[283,2],[544,2],[545,363],[546,682],[221,2],[385,683],[386,684],[387,685],[225,2],[390,2],[245,686],[220,2],[469,363],[227,687],[468,688],[467,689],[458,2],[459,2],[466,2],[461,2],[464,690],[460,2],[462,691],[465,692],[463,691],[258,2],[255,2],[256,652],[410,2],[415,693],[416,694],[414,695],[412,696],[413,697],[408,2],[475,660],[250,660],[504,698],[511,699],[515,700],[344,701],[343,2],[338,2],[491,702],[499,703],[371,704],[372,705],[450,706],[360,2],[473,707],[348,363],[365,708],[476,709],[361,2],[364,710],[362,2],[474,711],[471,712],[470,2],[472,2],[368,2],[446,713],[233,714],[346,715],[350,716],[366,717],[369,718],[358,719],[351,720],[498,721],[424,722],[342,723],[230,724],[497,725],[226,726],[417,727],[409,2],[418,728],[435,729],[407,2],[434,730],[91,2],[429,731],[254,2],[449,732],[425,2],[239,2],[241,2],[380,2],[433,733],[257,2],[281,734],[367,735],[287,736],[347,2],[432,2],[411,2],[437,737],[438,738],[379,2],[440,739],[442,740],[441,741],[391,2],[431,724],[444,742],[341,743],[430,744],[436,745],[266,2],[270,2],[269,2],[268,2],[273,2],[267,2],[276,2],[275,2],[272,2],[271,2],[274,2],[277,746],[265,2],[333,747],[332,2],[337,748],[334,749],[336,750],[339,748],[335,749],[246,751],[325,752],[494,753],[492,2],[521,754],[523,755],[487,756],[522,757],[234,758],[231,758],[264,2],[248,759],[247,760],[243,761],[244,762],[252,763],[280,763],[291,763],[327,764],[292,764],[236,765],[235,2],[331,766],[330,767],[329,768],[328,769],[237,770],[478,771],[279,772],[486,773],[451,774],[481,775],[485,776],[374,777],[373,778],[354,779],[340,780],[322,781],[324,782],[321,783],[443,784],[345,2],[509,2],[242,785],[445,786],[493,787],[352,2],[282,788],[359,789],[357,790],[284,791],[419,792],[488,2],[285,793],[420,793],[507,2],[506,2],[508,2],[490,2],[489,2],[422,794],[349,2],[319,795],[240,796],[298,2],[224,797],[286,2],[513,363],[223,2],[525,798],[306,363],[519,660],[305,799],[502,800],[303,798],[228,2],[527,801],[301,363],[302,363],[293,2],[222,2],[300,802],[299,803],[288,804],[363,525],[423,525],[439,2],[427,805],[426,2],[311,675],[232,2],[320,363],[496,686],[503,806],[86,363],[89,807],[90,808],[87,363],[88,2],[400,547],[395,809],[394,2],[393,810],[392,2],[501,811],[512,812],[514,813],[518,814],[520,815],[524,816],[552,817],[528,817],[551,818],[530,819],[540,820],[541,821],[543,822],[547,823],[550,686],[549,2],[548,824],[669,548],[670,825],[844,826],[769,827],[673,2],[674,827],[768,828],[770,829],[597,830],[595,831],[596,832],[584,833],[585,831],[592,834],[583,835],[588,836],[598,2],[589,837],[594,838],[600,839],[599,840],[582,841],[590,842],[591,843],[586,844],[593,830],[587,845],[790,846],[789,2],[1018,847],[1017,848],[1460,849],[996,2],[997,850],[998,2],[999,851],[1048,2],[606,852],[568,853],[569,854],[572,855],[561,856],[571,857],[567,858],[559,2],[573,859],[574,860],[428,861],[581,2],[1000,862],[982,2],[1005,863],[984,864],[1007,865],[1001,866],[1010,867],[1008,868],[985,2],[983,869],[980,363],[1006,870],[1560,871],[981,363],[1009,872],[969,2],[638,2],[1554,2],[1555,873],[630,2],[81,2],[82,2],[13,2],[14,2],[16,2],[15,2],[2,2],[17,2],[18,2],[19,2],[20,2],[21,2],[22,2],[23,2],[24,2],[3,2],[25,2],[26,2],[4,2],[27,2],[31,2],[28,2],[29,2],[30,2],[32,2],[33,2],[34,2],[5,2],[35,2],[36,2],[37,2],[38,2],[6,2],[42,2],[39,2],[40,2],[41,2],[43,2],[7,2],[44,2],[49,2],[50,2],[45,2],[46,2],[47,2],[48,2],[8,2],[54,2],[51,2],[52,2],[53,2],[55,2],[9,2],[56,2],[57,2],[58,2],[60,2],[59,2],[61,2],[62,2],[10,2],[63,2],[64,2],[65,2],[11,2],[66,2],[67,2],[68,2],[69,2],[70,2],[1,2],[71,2],[72,2],[12,2],[76,2],[74,2],[79,2],[78,2],[73,2],[77,2],[75,2],[80,2],[1016,2],[121,874],[134,875],[118,876],[135,877],[144,878],[109,879],[110,880],[108,881],[143,882],[138,883],[142,884],[112,885],[131,886],[111,887],[141,888],[106,889],[107,883],[113,890],[114,2],[120,891],[117,890],[104,892],[145,893],[136,894],[124,895],[123,890],[125,896],[128,897],[122,898],[126,899],[139,882],[115,900],[116,901],[129,902],[105,877],[133,903],[132,890],[119,901],[127,904],[130,905],[137,2],[103,2],[140,906],[1014,907],[557,908],[608,909],[576,910],[558,908],[556,2],[575,911],[607,2],[605,2],[578,912],[604,913],[570,914],[580,2],[579,915],[664,916],[775,917],[658,918],[639,919],[627,920],[623,921],[655,2],[624,577],[647,922],[644,923],[667,924],[666,925],[625,926],[646,927],[619,2],[626,928],[774,929],[668,930],[654,931],[671,932],[648,933],[1572,934],[645,2],[636,2],[754,935],[757,936],[759,937],[766,938],[761,2],[762,2],[760,939],[763,940],[755,2],[756,2],[767,941],[758,942],[764,2],[765,943],[748,944],[752,945],[749,945],[745,944],[753,946],[750,947],[751,945],[746,948],[747,949],[741,950],[682,951],[684,952],[740,2],[683,953],[744,954],[743,955],[742,956],[675,2],[685,951],[686,2],[677,957],[681,958],[676,2],[678,959],[679,960],[680,2],[687,961],[688,961],[689,961],[690,961],[691,961],[692,961],[693,961],[694,961],[695,961],[696,961],[697,961],[698,961],[699,961],[700,961],[702,961],[701,961],[703,961],[704,961],[705,961],[706,961],[707,961],[739,962],[708,961],[709,961],[710,961],[711,961],[712,961],[713,961],[714,961],[715,961],[716,961],[717,961],[718,961],[719,961],[720,961],[722,961],[721,961],[723,961],[724,961],[725,961],[726,961],[727,961],[728,961],[729,961],[730,961],[731,961],[732,961],[733,961],[734,961],[735,961],[738,961],[736,961],[737,961]],"affectedFilesPendingEmit":[1571,773,1178,1179,1140,1180,1181,1168,1182,1183,1169,1184,1185,1170,1186,1187,1173,1188,1189,1172,808,807,1175,1177,1176,1197,1198,1190,1199,1200,1191,1201,1202,1192,1203,1204,1193,809,1194,1196,1195,1222,1223,1217,1224,1225,1218,1219,1221,1220,1233,1234,1227,1235,1236,1228,1237,1238,1229,862,1230,1232,1231,864,863,866,865,870,869,868,867,872,871,874,873,876,875,880,879,882,881,878,877,884,883,888,887,886,885,894,893,896,895,891,898,897,900,899,902,901,906,905,904,903,908,907,910,909,913,914,915,916,911,912,922,921,920,919,918,917,926,925,924,923,928,927,930,929,932,931,934,933,936,935,937,938,939,892,941,940,943,942,945,944,890,889,947,946,949,948,953,952,951,950,1245,1246,1240,1247,1248,1241,1249,1250,1239,954,1242,1244,1243,1264,1265,1257,1266,1267,1258,1268,1269,1260,1270,1271,1259,1261,1263,1262,1272,1273,1251,1274,1275,1252,1276,1277,1253,1254,1256,1255,1306,1307,1298,1308,1309,1297,1310,1302,1311,1312,1299,1313,1314,1300,1315,1316,1301,1303,1305,1304,1317,1318,1279,1319,1282,1320,1321,1280,1322,1323,1281,1278,1330,1331,1324,1332,1333,1325,1334,1335,1326,955,1327,1329,1328,1283,1285,1284,1342,1343,1336,1344,1345,1337,1346,1347,1338,956,1339,1341,1340,1353,1354,1348,1355,1356,1349,1350,1352,1351,979,1368,1369,1362,1370,1371,1364,1372,1373,1363,1365,1367,1366,1378,1375,1456,1374,1377,1376,1469,1470,1461,1471,1472,1462,1473,1474,1463,1475,1466,1476,1477,1464,1465,1468,1467,1485,1486,1478,1487,1488,1481,1489,1490,1480,1479,1497,1498,1492,1499,1500,1493,1491,1494,1496,1495,1482,1484,1483,1508,1509,1503,1510,1511,1501,1512,1513,1504,1514,1515,1502,957,1505,1507,1506,1524,1525,1516,1526,1527,1517,1528,1529,1518,1530,1531,1520,1532,1533,1519,959,958,1521,1523,1522,1534,1535,1226,1536,1537,1139,1538,1539,1033,1540,1541,1128,1544,1545,1134,1546,1547,1136,1542,1543,1137,1548,1117,1563,1564,1129,1565,1566,1130,1561,1562,1131,1138,1127,1039,1032,1135,1361,1116,1215,1141,1167,1133,1296,1171,1216,971,972,973,1567,974,861,860,806,960,961,962,1174,804,803,967,970,805,1568,976,1569,978,975,779,802],"version":"5.9.3"} diff --git a/ingestion/api.py b/ingestion/api.py deleted file mode 100644 index e18b267b..00000000 --- a/ingestion/api.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Ingestion-domain API viewsets kept under the existing nested project routes.""" - -from rest_framework import viewsets -from rest_framework.permissions import BasePermission - -from core.api import ( - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import IsProjectMember, IsProjectMemberWritable -from ingestion.models import IngestionRun -from ingestion.serializers import IngestionRunSerializer - - -@document_project_owned_viewset( - resource_plural="ingestion runs", - resource_singular="ingestion run", - create_description="Create a new ingestion run record for the selected project to track a content ingestion attempt and its status.", - tag="Ingestion", - action_overrides=build_crud_action_overrides( - IngestionRunSerializer, - resource_plural="ingestion runs for the selected project", - resource_singular="ingestion run", - ), -) -class IngestionRunViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Inspect ingestion-run history for a project.""" - - serializer_class = IngestionRunSerializer - queryset = IngestionRun.objects.select_related("project") - - def get_permissions(self): - """Allow all members to read ingestion runs and contributors to manage them.""" - - permission_classes: list[type[BasePermission]] - if self.action in {"create", "update", "partial_update", "destroy"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] diff --git a/ingestion/api_urls.py b/ingestion/api_urls.py deleted file mode 100644 index 0663ec78..00000000 --- a/ingestion/api_urls.py +++ /dev/null @@ -1,13 +0,0 @@ -"""API route registration for ingestion resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from ingestion.api import IngestionRunViewSet - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested ingestion-run endpoints.""" - - project_router.register( - r"ingestion-runs", IngestionRunViewSet, basename="project-ingestion-run" - ) diff --git a/ingestion/ninja_api.py b/ingestion/ninja_api.py new file mode 100644 index 00000000..39f11999 --- /dev/null +++ b/ingestion/ninja_api.py @@ -0,0 +1,206 @@ +"""Django Ninja endpoints for project-scoped ingestion resources.""" + +from __future__ import annotations + +import datetime +from typing import Any + +from django.core.exceptions import ValidationError +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from ingestion.models import IngestionRun +from projects.ninja_helpers import _get_project_or_404, _require_project_writable + +router = Router(tags=["Ingestion"]) + + +class IngestionRunSchema(Schema): + """Serialized ingestion-run payload.""" + + id: int + project: int + plugin_name: str + started_at: datetime.datetime + completed_at: datetime.datetime | None = None + status: str + items_fetched: int + items_ingested: int + error_message: str + + +class IngestionRunCreateInput(Schema): + """Writable ingestion-run fields for create requests.""" + + plugin_name: str + completed_at: datetime.datetime | None = None + status: str + items_fetched: int = 0 + items_ingested: int = 0 + error_message: str = "" + project: int | None = None + + +class IngestionRunUpdateInput(Schema): + """Writable ingestion-run fields for update requests.""" + + plugin_name: str | None = None + completed_at: datetime.datetime | None = None + status: str | None = None + items_fetched: int | None = None + items_ingested: int | None = None + error_message: str | None = None + project: int | None = None + + +def _serialize_ingestion_run(ingestion_run: IngestionRun) -> dict[str, Any]: + """Return one ingestion-run response body.""" + + return { + "id": int(ingestion_run.pk), + "project": ingestion_run.project_id, + "plugin_name": ingestion_run.plugin_name, + "started_at": ingestion_run.started_at, + "completed_at": ingestion_run.completed_at, + "status": ingestion_run.status, + "items_fetched": ingestion_run.items_fetched, + "items_ingested": ingestion_run.items_ingested, + "error_message": ingestion_run.error_message, + } + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _validated_ingestion_run_payload( + payload: dict[str, Any], + *, + project, + instance: IngestionRun | None = None, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate one ingestion-run payload.""" + + validated_payload = dict(payload) + validated_payload.pop("project", None) + + ingestion_run = instance or IngestionRun(project=project) + for field_name, value in validated_payload.items(): + setattr(ingestion_run, field_name, value) + ingestion_run.project = project + try: + ingestion_run.full_clean() + except ValidationError as exc: + return validated_payload, _validation_error_payload(exc) + return validated_payload, None + + +def _get_ingestion_run_or_404(project_id: int, run_id: int) -> IngestionRun: + """Load one ingestion run for the selected project.""" + + ingestion_run = IngestionRun.objects.filter( + project_id=project_id, pk=run_id + ).first() + if not ingestion_run: + raise HttpError(404, "Not found.") + return ingestion_run + + +@router.get("/", response=list[IngestionRunSchema], auth=api_authenticate) +def list_ingestion_runs(request: Any, project_id: int = Path(...)): + """List ingestion runs visible to the current project member.""" + + _get_project_or_404(request, project_id) + ingestion_runs = IngestionRun.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_ingestion_run(ingestion_run) for ingestion_run in ingestion_runs] + + +@router.post( + "/", + response={201: IngestionRunSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_ingestion_run( + request: Any, + payload: IngestionRunCreateInput, + project_id: int = Path(...), +): + """Create one ingestion run under the selected project.""" + + project = _require_project_writable(request, project_id) + validated_payload, errors = _validated_ingestion_run_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=project, + ) + if errors is not None: + return Status(400, errors) + ingestion_run = IngestionRun.objects.create(project=project, **validated_payload) + return Status(201, _serialize_ingestion_run(ingestion_run)) + + +@router.get("/{run_id}/", response=IngestionRunSchema, auth=api_authenticate) +def get_ingestion_run( + request: Any, + project_id: int = Path(...), + run_id: int = Path(...), +): + """Return one ingestion run.""" + + _get_project_or_404(request, project_id) + return _serialize_ingestion_run(_get_ingestion_run_or_404(project_id, run_id)) + + +@router.patch( + "/{run_id}/", + response={200: IngestionRunSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_ingestion_run( + request: Any, + payload: IngestionRunUpdateInput, + project_id: int = Path(...), + run_id: int = Path(...), +): + """Update one ingestion run.""" + + _require_project_writable(request, project_id) + ingestion_run = _get_ingestion_run_or_404(project_id, run_id) + validated_payload, errors = _validated_ingestion_run_payload( + payload.model_dump(exclude_unset=True, exclude_none=False), + project=ingestion_run.project, + instance=ingestion_run, + ) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(ingestion_run, field_name, value) + ingestion_run.save() + return _serialize_ingestion_run(ingestion_run) + + +@router.delete("/{run_id}/", response={204: None}, auth=api_authenticate) +def delete_ingestion_run( + request: Any, + project_id: int = Path(...), + run_id: int = Path(...), +): + """Delete one ingestion run.""" + + _require_project_writable(request, project_id) + ingestion_run = _get_ingestion_run_or_404(project_id, run_id) + ingestion_run.delete() + return Status(204, None) + + +__all__ = ["router"] diff --git a/ingestion/serializers.py b/ingestion/serializers.py deleted file mode 100644 index a89a7dc2..00000000 --- a/ingestion/serializers.py +++ /dev/null @@ -1,25 +0,0 @@ -"""DRF serializers for ingestion-domain models.""" - -from rest_framework import serializers - -from core.serializer_mixins import ProjectScopedSerializerMixin -from ingestion.models import IngestionRun - - -class IngestionRunSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize ingestion-run audit records.""" - - class Meta: - model = IngestionRun - fields = [ - "id", - "project", - "plugin_name", - "started_at", - "completed_at", - "status", - "items_fetched", - "items_ingested", - "error_message", - ] - read_only_fields = ["id", "project", "started_at"] diff --git a/ingestion/tasks.py b/ingestion/tasks.py index db737d06..ce2f117d 100644 --- a/ingestion/tasks.py +++ b/ingestion/tasks.py @@ -1,15 +1,19 @@ -"""Celery tasks and helpers for source ingestion.""" +"""Background tasks and helpers for source ingestion.""" import logging from datetime import timedelta -from celery import shared_task -from django.conf import settings from django.db.models import Model, Q from django.utils import timezone from content.deduplication import canonicalize_url from content.models import Content +from digest_engine.taskiq import ( + broker, + enqueue_task, + run_task_inline, + task_always_eager, +) from ingestion.models import IngestionRun, RunStatus from ingestion.plugins import get_plugin_for_source_config from notifications.emit import notify_project_admins @@ -30,7 +34,7 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -@shared_task(name="core.tasks.run_ingestion") +@broker.task(task_name="core.tasks.run_ingestion") def run_ingestion(source_config_id: int): """Fetch new content for one source config and record an ingestion run.""" @@ -75,7 +79,7 @@ def run_ingestion(source_config_id: int): return {"items_fetched": items_fetched, "items_ingested": items_ingested} -@shared_task(name="core.tasks.run_all_ingestions") +@broker.task(task_name="core.tasks.run_all_ingestions") def run_all_ingestions(): """Queue ingestion for every active source configuration.""" @@ -83,10 +87,10 @@ def run_all_ingestions(): SourceConfig.objects.filter(is_active=True).values_list("id", flat=True) ) for source_config_id in source_config_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): run_ingestion(source_config_id) else: - run_ingestion.delay(source_config_id) + enqueue_task(run_ingestion, source_config_id) return len(source_config_ids) @@ -173,13 +177,13 @@ def _schedule_content_processing(content: Content) -> None: upsert_content_embedding(content) assign_content_to_topic_cluster(content.id) - if settings.CELERY_TASK_ALWAYS_EAGER: - process_content(content.id) + if task_always_eager(): + run_task_inline(process_content, content.id) else: - process_content.delay(content.id) + enqueue_task(process_content, content.id) -@shared_task(name="core.tasks.refresh_linkedin_tokens") +@broker.task(task_name="core.tasks.refresh_linkedin_tokens") def refresh_linkedin_tokens(): """Refresh LinkedIn tokens that are missing expiry metadata or expiring soon.""" diff --git a/ingestion/tests/test_ninja_api.py b/ingestion/tests/test_ninja_api.py new file mode 100644 index 00000000..2197fc92 --- /dev/null +++ b/ingestion/tests/test_ninja_api.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from ingestion.models import IngestionRun, RunStatus +from projects.models import Project, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class IngestionRunNinjaApiTests(TestCase): + """Exercise project-scoped Ninja ingestion-run endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.reader = _create_user( + user_model, + username="reader", + password="testpass123", + ) + self.other_user = _create_user( + user_model, + username="other", + password="testpass123", + ) + + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.reader, + project=self.owner_project, + role=ProjectRole.READER, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + + self.owner_run = IngestionRun.objects.create( + project=self.owner_project, + plugin_name="rss", + status=RunStatus.SUCCESS, + items_fetched=5, + items_ingested=4, + ) + self.other_run = IngestionRun.objects.create( + project=self.other_project, + plugin_name="reddit", + status=RunStatus.FAILED, + items_fetched=2, + items_ingested=0, + error_message="Boom", + ) + + self.client.force_login(self.owner) + + def test_list_ingestion_runs_is_project_scoped(self): + response = self.client.get( + reverse( + "ninja-api:list_ingestion_runs", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_run)) + + def test_create_ingestion_run_uses_project_from_url(self): + response = self.client.post( + reverse( + "ninja-api:create_ingestion_run", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "plugin_name": "bluesky", + "status": RunStatus.RUNNING, + "items_fetched": 0, + "items_ingested": 0, + "error_message": "", + "project": _require_pk(self.other_project), + }, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.CREATED) + created_run = IngestionRun.objects.get(plugin_name="bluesky") + self.assertEqual(created_run.project, self.owner_project) + self.assertEqual(created_run.status, RunStatus.RUNNING) + + def test_create_ingestion_run_rejects_invalid_status(self): + response = self.client.post( + reverse( + "ninja-api:create_ingestion_run", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "plugin_name": "bluesky", + "status": "invalid-status", + "items_fetched": 0, + "items_ingested": 0, + }, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["status"][0], + "Value 'invalid-status' is not a valid choice.", + ) + + def test_reader_can_list_but_cannot_create(self): + self.client.force_login(self.reader) + + list_response = self.client.get( + reverse( + "ninja-api:list_ingestion_runs", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + + create_response = self.client.post( + reverse( + "ninja-api:create_ingestion_run", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "plugin_name": "mastodon", + "status": RunStatus.RUNNING, + "items_fetched": 1, + "items_ingested": 0, + }, + content_type="application/json", + ) + self.assertEqual(create_response.status_code, HTTPStatus.FORBIDDEN) + + def test_update_and_delete_ingestion_run(self): + update_response = self.client.patch( + reverse( + "ninja-api:update_ingestion_run", + kwargs={ + "project_id": _require_pk(self.owner_project), + "run_id": _require_pk(self.owner_run), + }, + ), + { + "status": RunStatus.FAILED, + "items_fetched": 7, + "items_ingested": 3, + "error_message": "Partial failure", + }, + content_type="application/json", + ) + + self.assertEqual(update_response.status_code, HTTPStatus.OK) + self.owner_run.refresh_from_db() + self.assertEqual(self.owner_run.status, RunStatus.FAILED) + self.assertEqual(self.owner_run.items_fetched, 7) + self.assertEqual(self.owner_run.error_message, "Partial failure") + + delete_response = self.client.delete( + reverse( + "ninja-api:delete_ingestion_run", + kwargs={ + "project_id": _require_pk(self.owner_project), + "run_id": _require_pk(self.owner_run), + }, + ) + ) + self.assertEqual(delete_response.status_code, HTTPStatus.NO_CONTENT) + self.assertFalse( + IngestionRun.objects.filter(pk=_require_pk(self.owner_run)).exists() + ) diff --git a/ingestion/tests/test_tasks.py b/ingestion/tests/test_tasks.py index 796a50e4..623f8841 100644 --- a/ingestion/tests/test_tasks.py +++ b/ingestion/tests/test_tasks.py @@ -1,5 +1,6 @@ from datetime import datetime, timezone from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from django.db.models import Model @@ -50,9 +51,17 @@ def source_plugin_context(django_user_model): return SimpleNamespace(user=user, project=project, entity=entity) -def test_run_ingestion_creates_content_from_rss_entries(source_plugin_context, mocker): +def test_run_ingestion_creates_content_from_rss_entries( + source_plugin_context, + mocker, + settings, +): + settings.TASKIQ_ALWAYS_EAGER = False upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) parse_mock = mocker.patch("ingestion.plugins.rss.feedparser.parse") source_config = SourceConfig.objects.create( project=source_plugin_context.project, @@ -81,7 +90,7 @@ def test_run_ingestion_creates_content_from_rss_entries(source_plugin_context, m assert content.project == source_plugin_context.project assert content.entity == source_plugin_context.entity upsert_embedding_mock.assert_called_once_with(content) - process_content_delay_mock.assert_called_once_with(_require_pk(content)) + process_content_queue_mock.assert_awaited_once_with(_require_pk(content)) assert ( SourceConfig.objects.get(pk=_require_pk(source_config)).last_fetched_at is not None @@ -94,7 +103,10 @@ def test_run_ingestion_creates_content_from_rss_entries(source_plugin_context, m def test_run_ingestion_skips_same_source_duplicate_urls(source_plugin_context, mocker): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) parse_mock = mocker.patch("ingestion.plugins.rss.feedparser.parse") source_config = SourceConfig.objects.create( project=source_plugin_context.project, @@ -130,7 +142,7 @@ def test_run_ingestion_skips_same_source_duplicate_urls(source_plugin_context, m assert result["items_fetched"] == 1 assert result["items_ingested"] == 0 upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() + process_content_queue_mock.assert_not_awaited() assert Content.objects.filter(url="https://example.com/post-1").count() == 1 @@ -138,7 +150,10 @@ def test_ingest_source_config_allows_cross_plugin_duplicate_urls_for_pipeline_de source_plugin_context, mocker ): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) source_config = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.REDDIT, @@ -177,12 +192,15 @@ def test_ingest_source_config_allows_cross_plugin_duplicate_urls_for_pipeline_de assert items_ingested == 1 assert Content.objects.filter(project=source_plugin_context.project).count() == 2 upsert_embedding_mock.assert_called_once() - process_content_delay_mock.assert_called_once() + process_content_queue_mock.assert_awaited_once() def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context, mocker): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) reddit_mock = mocker.patch("ingestion.plugins.reddit.praw.Reddit") source_config = SourceConfig.objects.create( project=source_plugin_context.project, @@ -212,7 +230,7 @@ def test_run_ingestion_creates_content_from_reddit_posts(source_plugin_context, assert result["items_ingested"] == 1 content = Content.objects.get(title="Reddit Post") upsert_embedding_mock.assert_called_once_with(content) - process_content_delay_mock.assert_called_once_with(_require_pk(content)) + process_content_queue_mock.assert_awaited_once_with(_require_pk(content)) assert content.source_plugin == SourcePluginName.REDDIT assert content.entity is None assert content.source_metadata["score"] == 14 @@ -223,7 +241,10 @@ def test_ingest_source_config_deduplicates_bluesky_posts_by_post_uri( source_plugin_context, mocker ): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) source_config = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.BLUESKY, @@ -265,14 +286,17 @@ def test_ingest_source_config_deduplicates_bluesky_posts_by_post_uri( assert items_ingested == 0 assert Content.objects.filter(project=source_plugin_context.project).count() == 1 upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() + process_content_queue_mock.assert_not_awaited() def test_ingest_source_config_deduplicates_mastodon_statuses_by_status_uri( source_plugin_context, mocker ): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) source_config = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.MASTODON, @@ -319,14 +343,17 @@ def test_ingest_source_config_deduplicates_mastodon_statuses_by_status_uri( assert items_ingested == 0 assert Content.objects.filter(project=source_plugin_context.project).count() == 1 upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() + process_content_queue_mock.assert_not_awaited() def test_ingest_source_config_deduplicates_linkedin_posts_by_post_urn( source_plugin_context, mocker ): upsert_embedding_mock = mocker.patch("core.embeddings.upsert_content_embedding") - process_content_delay_mock = mocker.patch("core.tasks.process_content.delay") + process_content_queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) source_config = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.LINKEDIN, @@ -370,7 +397,7 @@ def test_ingest_source_config_deduplicates_linkedin_posts_by_post_urn( assert items_ingested == 0 assert Content.objects.filter(project=source_plugin_context.project).count() == 1 upsert_embedding_mock.assert_not_called() - process_content_delay_mock.assert_not_called() + process_content_queue_mock.assert_not_awaited() def test_refresh_linkedin_tokens_refreshes_expiring_credentials( @@ -394,7 +421,10 @@ def test_refresh_linkedin_tokens_refreshes_expiring_credentials( def test_run_all_ingestions_enqueues_active_source_configs( source_plugin_context, mocker ): - delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") + queue_mock = mocker.patch( + "ingestion.tasks.run_ingestion.kiq", + new_callable=AsyncMock, + ) active_one = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.RSS, @@ -415,17 +445,20 @@ def test_run_all_ingestions_enqueues_active_source_configs( enqueued_count = run_all_ingestions() assert enqueued_count == 2 - delay_mock.assert_any_call(_require_pk(active_one)) - delay_mock.assert_any_call(_require_pk(active_two)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(_require_pk(active_one)) + queue_mock.assert_any_await(_require_pk(active_two)) + assert queue_mock.await_count == 2 def test_run_all_ingestions_executes_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True run_ingestion_mock = mocker.patch("ingestion.tasks.run_ingestion") - delay_mock = mocker.patch("ingestion.tasks.run_ingestion.delay") + queue_mock = mocker.patch( + "ingestion.tasks.run_ingestion.kiq", + new_callable=AsyncMock, + ) active_one = SourceConfig.objects.create( project=source_plugin_context.project, plugin_name=SourcePluginName.RSS, @@ -443,7 +476,7 @@ def test_run_all_ingestions_executes_inline_when_eager( run_ingestion_mock.assert_any_call(_require_pk(active_one)) run_ingestion_mock.assert_any_call(_require_pk(active_two)) assert run_ingestion_mock.call_count == 2 - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_run_ingestion_marks_failure_when_plugin_errors(source_plugin_context, mocker): @@ -499,7 +532,7 @@ def test_run_ingestion_notifies_project_admins_when_plugin_errors( def test_ingest_source_config_truncates_fields_and_processes_inline( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True plugin = mocker.Mock() plugin.fetch_new_content.return_value = [ SimpleNamespace( @@ -520,7 +553,10 @@ def test_ingest_source_config_truncates_fields_and_processes_inline( mocker.patch("ingestion.tasks.get_plugin_for_source_config", return_value=plugin) upsert_mock = mocker.patch("core.embeddings.upsert_content_embedding") process_mock = mocker.patch("core.tasks.process_content") - delay_mock = mocker.patch("core.tasks.process_content.delay") + queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) items_fetched, items_ingested = _ingest_source_config(source_config) @@ -531,5 +567,5 @@ def test_ingest_source_config_truncates_fields_and_processes_inline( assert len(created.title) == 512 assert len(created.author) == 255 upsert_mock.assert_called_once_with(created) - process_mock.assert_called_once_with(_require_pk(created)) - delay_mock.assert_not_called() + process_mock.original_func.assert_called_once_with(_require_pk(created)) + queue_mock.assert_not_awaited() diff --git a/justfile b/justfile index 1cdd8c7a..198aebb5 100644 --- a/justfile +++ b/justfile @@ -11,7 +11,8 @@ backend_python_targets := "conftest.py manage.py content core digest_engine enti frontend_env := "if [ ! -f frontend/.env.local ]; then cp frontend/.env.example frontend/.env.local; fi" pnpm_setup := "corepack enable && corepack prepare pnpm@11.1.0 --activate" pnpm_exec := "pnpm" -turbo_exec := "pnpm turbo" +turbo_setup := "./scripts/ensure_turbo_link.sh" +turbo_exec := "./scripts/ensure_turbo_link.sh && node node_modules/turbo/bin/turbo" frontend_filter := "--filter=@digestengine/frontend" django_manage := "docker compose exec django python manage.py" host_backend_test_env := "uv sync --frozen && set -a && . ./.env.test && set +a &&" @@ -35,7 +36,8 @@ bootstrap: frontend-install: @{{frontend_env}} @{{pnpm_setup}} - @{{pnpm_exec}} install {{frontend_filter}} + @{{pnpm_exec}} install --frozen-lockfile + @{{turbo_setup}} # Install pre-commit hooks when running inside a git checkout install-hooks: @@ -60,7 +62,7 @@ clean: # Start the backend development stack with Django, workers, and dependencies backend-dev: @{{backend_env}} - @{{compose}} up django celery-worker celery-beat postgres redis qdrant nginx + @{{compose}} up django taskiq-worker taskiq-scheduler postgres redis rabbitmq qdrant nginx # Start only the app frontend development server frontend-dev: diff --git a/messaging/api.py b/messaging/api.py deleted file mode 100644 index e5b3e584..00000000 --- a/messaging/api.py +++ /dev/null @@ -1,151 +0,0 @@ -"""REST API endpoints for direct-message threads.""" - -from __future__ import annotations - -from django.contrib.auth import get_user_model -from django.db.models import Prefetch -from django.utils import timezone -from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_view -from rest_framework import mixins, status, viewsets -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from core.api import AUTHENTICATION_REQUIRED_RESPONSE -from messaging.models import DirectMessage, Thread, ThreadParticipant -from messaging.serializers import ( - DirectMessageCreateSerializer, - DirectMessageSerializer, - ThreadCreateSerializer, - ThreadSerializer, -) - - -@extend_schema_view( - list=extend_schema( - summary="List direct-message threads", - description="Return the current user's 1:1 messaging threads, newest first.", - tags=["Messaging"], - responses={ - 200: ThreadSerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ), - create=extend_schema( - summary="Open or find a direct-message thread", - description="Find an existing 1:1 thread with another visible user or create it.", - tags=["Messaging"], - request=ThreadCreateSerializer, - responses={201: ThreadSerializer, 403: AUTHENTICATION_REQUIRED_RESPONSE}, - ), -) -class ThreadViewSet( - mixins.ListModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet -): - """List, create, and interact with the current user's direct-message threads.""" - - permission_classes = [IsAuthenticated] - serializer_class = ThreadSerializer - queryset = Thread.objects.all() - http_method_names = ["get", "post", "head", "options"] - - def get_queryset(self): - """Limit threads to those the authenticated user participates in.""" - - user_model = get_user_model() - return ( - self.queryset.filter(participants=self.request.user) - .prefetch_related( - Prefetch( - "participants", - queryset=user_model.objects.all(), - ), - Prefetch( - "participant_states", - queryset=ThreadParticipant.objects.filter(user=self.request.user), - to_attr="current_participant_states", - ), - Prefetch( - "messages", - queryset=DirectMessage.objects.select_related("sender").order_by( - "-created_at" - ), - to_attr="prefetched_messages", - ), - ) - .distinct() - ) - - def create(self, request, *args, **kwargs): - """Find or create the direct-message thread for the requested recipient.""" - - input_serializer = ThreadCreateSerializer( - data=request.data, - context=self.get_serializer_context(), - ) - input_serializer.is_valid(raise_exception=True) - thread = input_serializer.save() - thread = self.get_queryset().get(pk=thread.pk) - output_serializer = self.get_serializer(thread) - return Response(output_serializer.data, status=status.HTTP_201_CREATED) - - @extend_schema( - summary="List thread messages", - description="Return messages for one direct-message thread.", - tags=["Messaging"], - responses={ - 200: DirectMessageSerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["get", "post"], url_path="messages") - def messages(self, request, *args, **kwargs): - """List or send messages within one thread.""" - - thread = self.get_object() - if request.method.lower() == "get": - serializer = DirectMessageSerializer( - thread.messages.select_related("sender").all(), - many=True, - ) - return Response(serializer.data) - - input_serializer = DirectMessageCreateSerializer(data=request.data) - input_serializer.is_valid(raise_exception=True) - message = DirectMessage.objects.create( - thread=thread, - sender=request.user, - body=input_serializer.validated_data["body"], - ) - thread.last_message_at = message.created_at - thread.save(update_fields=["last_message_at"]) - ThreadParticipant.objects.filter(thread=thread, user=request.user).update( - last_read_at=message.created_at - ) - output_serializer = DirectMessageSerializer(message) - return Response(output_serializer.data, status=status.HTTP_201_CREATED) - - @extend_schema( - summary="Mark a thread as read", - description="Update the current user's last-read cursor for one thread.", - tags=["Messaging"], - request=None, - responses={ - 200: OpenApiResponse(description="Thread marked as read."), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path="read") - def read(self, request, *args, **kwargs): - """Move the current user's read cursor to the latest message.""" - - thread = self.get_object() - participant_state = thread.participant_states.get(user=request.user) - participant_state.last_read_at = thread.last_message_at or timezone.now() - participant_state.save(update_fields=["last_read_at"]) - return Response( - { - "thread_id": int(thread.pk), - "last_read_at": participant_state.last_read_at, - } - ) diff --git a/messaging/api_urls.py b/messaging/api_urls.py deleted file mode 100644 index c39d98b0..00000000 --- a/messaging/api_urls.py +++ /dev/null @@ -1,11 +0,0 @@ -"""API route registration for direct-message thread resources.""" - -from rest_framework.routers import DefaultRouter - -from messaging.api import ThreadViewSet - - -def register_root_routes(router: DefaultRouter) -> None: - """Register top-level messaging endpoints.""" - - router.register("messaging/threads", ThreadViewSet, basename="messaging-thread") diff --git a/messaging/ninja_api.py b/messaging/ninja_api.py new file mode 100644 index 00000000..bf249c6b --- /dev/null +++ b/messaging/ninja_api.py @@ -0,0 +1,348 @@ +"""Django Ninja endpoints for direct-message threads.""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any + +from django.contrib.auth import get_user_model +from django.db.models import Prefetch +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from ninja import Router, Schema +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from messaging.models import ( + DirectMessage, + Thread, + ThreadParticipant, + users_share_project, +) + +router = Router(tags=["Messaging"]) + + +class ThreadCounterpartSchema(Schema): + """The other participant shown in a 1:1 thread.""" + + id: int + username: str + display_name: str + avatar_url: str | None = None + avatar_thumbnail_url: str | None = None + + +class ThreadSchema(Schema): + """Serialized direct-message thread summary.""" + + id: int + counterpart: ThreadCounterpartSchema | None = None + has_unread: bool + last_message_preview: str + last_message_at: datetime | None = None + last_read_at: datetime | None = None + created_at: datetime + + +class ThreadCreateInput(Schema): + """Input payload for opening or finding a direct-message thread.""" + + recipient_user_id: int + opening_message: str | None = None + + +class DirectMessageSchema(Schema): + """Serialized direct message payload.""" + + id: int + thread: int + sender: int + sender_username: str + sender_display_name: str + body: str + created_at: datetime + edited_at: datetime | None = None + + +class DirectMessageCreateInput(Schema): + """Input payload for posting one direct message.""" + + body: str + + +class ThreadReadResponseSchema(Schema): + """Response body for the thread read action.""" + + thread_id: int + last_read_at: datetime | None = None + + +def _threads_queryset(request) -> Any: + """Return the current user's visible thread queryset.""" + + user_model = get_user_model() + return ( + Thread.objects.filter(participants=request.user) + .prefetch_related( + Prefetch("participants", queryset=user_model.objects.all()), + Prefetch( + "participant_states", + queryset=ThreadParticipant.objects.filter(user=request.user), + to_attr="current_participant_states", + ), + Prefetch( + "messages", + queryset=DirectMessage.objects.select_related("sender").order_by( + "-created_at" + ), + to_attr="prefetched_messages", + ), + ) + .distinct() + ) + + +def _serialize_threads(request, threads: list[Thread]) -> list[dict[str, Any]]: + """Return serialized thread payloads.""" + + return [_serialize_thread(request, thread) for thread in threads] + + +def _serialize_thread(request, thread: Thread) -> dict[str, Any]: + """Return one serialized thread payload.""" + + current_user_id = int(request.user.pk) + participant_states = getattr(thread, "current_participant_states", None) + participant_state = ( + participant_states[0] + if participant_states + else thread.participant_states.filter(user_id=current_user_id).first() + ) + prefetched_messages = getattr(thread, "prefetched_messages", None) + messages = ( + list(prefetched_messages) + if prefetched_messages is not None + else list(thread.messages.select_related("sender").order_by("-created_at")) + ) + counterpart = next( + ( + participant + for participant in thread.participants.all() + if int(participant.pk) != current_user_id + ), + None, + ) + last_read_at = participant_state.last_read_at if participant_state else None + return { + "id": int(thread.pk), + "counterpart": ( + { + "id": int(counterpart.pk), + "username": counterpart.username, + "display_name": counterpart.display_name or counterpart.username, + "avatar_url": counterpart.avatar_url, + "avatar_thumbnail_url": counterpart.avatar_thumbnail_url, + } + if counterpart is not None + else None + ), + "has_unread": any( + message.sender_id != current_user_id + and (last_read_at is None or message.created_at > last_read_at) + for message in messages + ), + "last_message_preview": messages[0].body[:140] if messages else "", + "last_message_at": thread.last_message_at, + "last_read_at": last_read_at, + "created_at": thread.created_at, + } + + +def _serialize_messages(messages: list[DirectMessage]) -> list[dict[str, Any]]: + """Return serialized direct message payloads.""" + + return [_serialize_message(message) for message in messages] + + +def _serialize_message(message: DirectMessage) -> dict[str, Any]: + """Return one serialized direct message payload.""" + + return { + "id": int(message.pk), + "thread": message.thread_id, + "sender": message.sender_id, + "sender_username": message.sender.username, + "sender_display_name": message.sender.display_name or message.sender.username, + "body": message.body, + "created_at": message.created_at, + "edited_at": message.edited_at, + } + + +def _error_payload(field: str, message: str) -> dict[str, list[str]]: + """Return the native Ninja validation payload shape.""" + + return {field: [message]} + + +def _validated_thread_payload( + payload: dict[str, Any], + *, + request, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate one thread-create payload.""" + + validated_payload = dict(payload) + recipient_user_id = int(validated_payload["recipient_user_id"]) + if recipient_user_id == int(request.user.pk): + return validated_payload, _error_payload( + "recipient_user_id", "You cannot message yourself." + ) + + user_model = get_user_model() + if not user_model.objects.filter(pk=recipient_user_id).exists(): + return validated_payload, _error_payload( + "recipient_user_id", "Recipient not found." + ) + + if not users_share_project(int(request.user.pk), recipient_user_id): + return validated_payload, _error_payload( + "recipient_user_id", + "You can only message users who share a project with you.", + ) + + validated_payload["opening_message"] = str( + validated_payload.get("opening_message", "") + ).strip() + return validated_payload, None + + +def _validated_message_body( + body: str, +) -> tuple[str | None, dict[str, list[str]] | None]: + """Normalize and validate one outbound direct-message body.""" + + normalized = body.strip() + if not normalized: + return None, _error_payload("body", "This field may not be blank.") + return normalized, None + + +def _get_thread_for_request(request, thread_id: int) -> Thread: + """Return one thread visible to the current user or 404.""" + + return get_object_or_404(_threads_queryset(request), pk=thread_id) + + +@router.get("/messaging/threads/", response=list[ThreadSchema], auth=api_authenticate) +def list_threads(request): + """Return the current user's 1:1 messaging threads, newest first.""" + + return _serialize_threads(request, list(_threads_queryset(request))) + + +@router.post( + "/messaging/threads/", + response={201: ThreadSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_thread(request, payload: ThreadCreateInput): + """Find an existing 1:1 thread with another visible user or create it.""" + + validated_payload, errors = _validated_thread_payload( + payload.model_dump(exclude_unset=True), + request=request, + ) + if errors is not None: + return Status(400, errors) + + recipient_user_id = int(validated_payload["recipient_user_id"]) + opening_message = str(validated_payload.get("opening_message", "")) + + thread = Thread.find_between_users(int(request.user.pk), recipient_user_id) + if thread is None: + thread = Thread.objects.create() + ThreadParticipant.objects.bulk_create( + [ + ThreadParticipant(thread=thread, user=request.user), + ThreadParticipant(thread=thread, user_id=recipient_user_id), + ] + ) + + if opening_message: + message = DirectMessage.objects.create( + thread=thread, + sender=request.user, + body=opening_message, + ) + thread.last_message_at = message.created_at + thread.save(update_fields=["last_message_at"]) + ThreadParticipant.objects.filter(thread=thread, user=request.user).update( + last_read_at=message.created_at + ) + elif thread.last_message_at is None: + thread.last_message_at = timezone.now() + thread.save(update_fields=["last_message_at"]) + + hydrated_thread = _threads_queryset(request).get(pk=thread.pk) + return Status(201, _serialize_thread(request, hydrated_thread)) + + +@router.get( + "/messaging/threads/{thread_id}/messages/", + response=list[DirectMessageSchema], + auth=api_authenticate, +) +def list_thread_messages(request, thread_id: int): + """Return messages for one direct-message thread.""" + + thread = _get_thread_for_request(request, thread_id) + messages = list(thread.messages.select_related("sender").all()) + return _serialize_messages(messages) + + +@router.post( + "/messaging/threads/{thread_id}/messages/", + response={201: DirectMessageSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_thread_message(request, thread_id: int, payload: DirectMessageCreateInput): + """Send one direct message in the specified thread.""" + + thread = _get_thread_for_request(request, thread_id) + normalized_body, errors = _validated_message_body(payload.body) + if errors is not None: + return Status(400, errors) + message = DirectMessage.objects.create( + thread=thread, + sender=request.user, + body=normalized_body, + ) + thread.last_message_at = message.created_at + thread.save(update_fields=["last_message_at"]) + ThreadParticipant.objects.filter(thread=thread, user=request.user).update( + last_read_at=message.created_at + ) + return Status(201, _serialize_message(message)) + + +@router.post( + "/messaging/threads/{thread_id}/read/", + response=ThreadReadResponseSchema, + auth=api_authenticate, +) +def read_thread(request, thread_id: int): + """Move the current user's read cursor to the latest message.""" + + thread = _get_thread_for_request(request, thread_id) + participant_state = thread.participant_states.get(user=request.user) + participant_state.last_read_at = thread.last_message_at or timezone.now() + participant_state.save(update_fields=["last_read_at"]) + return { + "thread_id": int(thread.pk), + "last_read_at": participant_state.last_read_at, + } + + +__all__ = ["router"] diff --git a/messaging/serializers.py b/messaging/serializers.py deleted file mode 100644 index 6993cc31..00000000 --- a/messaging/serializers.py +++ /dev/null @@ -1,213 +0,0 @@ -"""Serializers for direct-message threads and messages.""" - -from __future__ import annotations - -from typing import cast - -from django.contrib.auth import get_user_model -from django.utils import timezone -from rest_framework import serializers - -from messaging.models import ( - DirectMessage, - Thread, - ThreadParticipant, - users_share_project, -) - - -class ThreadCounterpartSerializer(serializers.Serializer): - """Serialize the other user visible in a 1:1 thread.""" - - id = serializers.IntegerField() - username = serializers.CharField() - display_name = serializers.CharField() - avatar_url = serializers.CharField(allow_null=True) - avatar_thumbnail_url = serializers.CharField(allow_null=True) - - -class DirectMessageSerializer(serializers.ModelSerializer): - """Serialize one direct message within a thread.""" - - sender_username = serializers.CharField(source="sender.username", read_only=True) - sender_display_name = serializers.SerializerMethodField() - - class Meta: - model = DirectMessage - fields = [ - "id", - "thread", - "sender", - "sender_username", - "sender_display_name", - "body", - "created_at", - "edited_at", - ] - read_only_fields = fields - - def get_sender_display_name(self, obj: DirectMessage) -> str: - """Return the sender's display label for message lists.""" - - return obj.sender.display_name or obj.sender.username - - -class ThreadSerializer(serializers.ModelSerializer): - """Serialize thread summaries for the current participant.""" - - counterpart = serializers.SerializerMethodField() - has_unread = serializers.SerializerMethodField() - last_message_preview = serializers.SerializerMethodField() - last_read_at = serializers.SerializerMethodField() - - class Meta: - model = Thread - fields = [ - "id", - "counterpart", - "has_unread", - "last_message_preview", - "last_message_at", - "last_read_at", - "created_at", - ] - read_only_fields = fields - - def _current_user_id(self) -> int: - request = self.context["request"] - return int(request.user.pk) - - def _participant_state(self, obj: Thread) -> ThreadParticipant | None: - prefetched_state = getattr(obj, "current_participant_states", None) - if prefetched_state: - return prefetched_state[0] - return obj.participant_states.filter(user_id=self._current_user_id()).first() - - def _messages(self, obj: Thread) -> list[DirectMessage]: - prefetched_messages = getattr(obj, "prefetched_messages", None) - if prefetched_messages is not None: - return list(prefetched_messages) - return list(obj.messages.select_related("sender").order_by("-created_at")) - - def get_counterpart(self, obj: Thread) -> dict[str, object] | None: - """Return the other participant shown in thread lists.""" - - current_user_id = self._current_user_id() - counterpart = next( - ( - participant - for participant in obj.participants.all() - if participant.pk != current_user_id - ), - None, - ) - if counterpart is None: - return None - return { - "id": int(counterpart.pk), - "username": counterpart.username, - "display_name": counterpart.display_name or counterpart.username, - "avatar_url": counterpart.avatar_url, - "avatar_thumbnail_url": counterpart.avatar_thumbnail_url, - } - - def get_has_unread(self, obj: Thread) -> bool: - """Return whether the current user has unread incoming messages.""" - - participant_state = self._participant_state(obj) - last_read_at = participant_state.last_read_at if participant_state else None - current_user_id = self._current_user_id() - return any( - message.sender_id != current_user_id - and (last_read_at is None or message.created_at > last_read_at) - for message in self._messages(obj) - ) - - def get_last_message_preview(self, obj: Thread) -> str: - """Return a compact preview of the latest message body.""" - - messages = self._messages(obj) - if not messages: - return "" - return messages[0].body[:140] - - def get_last_read_at(self, obj: Thread): - """Return the current participant's last-read timestamp.""" - - participant_state = self._participant_state(obj) - return participant_state.last_read_at if participant_state else None - - -class ThreadCreateSerializer(serializers.Serializer): - """Validate the recipient and optional opening message for a new thread.""" - - recipient_user_id = serializers.IntegerField() - opening_message = serializers.CharField( - required=False, allow_blank=True, trim_whitespace=True - ) - - def validate_recipient_user_id(self, value: int) -> int: - """Require a real user distinct from the current sender.""" - - request = self.context["request"] - if value == request.user.pk: - raise serializers.ValidationError("You cannot message yourself.") - - user_model = get_user_model() - if not user_model.objects.filter(pk=value).exists(): - raise serializers.ValidationError("Recipient not found.") - return value - - def validate(self, attrs: dict[str, object]) -> dict[str, object]: - """Require both users to share at least one project membership.""" - - request = self.context["request"] - recipient_user_id = cast(int, attrs["recipient_user_id"]) - if not users_share_project(int(request.user.pk), recipient_user_id): - raise serializers.ValidationError( - { - "recipient_user_id": "You can only message users who share a project with you." - } - ) - return attrs - - def create(self, validated_data: dict[str, object]) -> Thread: - """Find or create the 1:1 thread and optionally add the opening message.""" - - request = self.context["request"] - recipient_user_id = cast(int, validated_data["recipient_user_id"]) - opening_message = str(validated_data.get("opening_message", "")).strip() - - thread = Thread.find_between_users(int(request.user.pk), recipient_user_id) - if thread is None: - thread = Thread.objects.create() - ThreadParticipant.objects.bulk_create( - [ - ThreadParticipant(thread=thread, user=request.user), - ThreadParticipant(thread=thread, user_id=recipient_user_id), - ] - ) - - if opening_message: - message = DirectMessage.objects.create( - thread=thread, - sender=request.user, - body=opening_message, - ) - thread.last_message_at = message.created_at - thread.save(update_fields=["last_message_at"]) - ThreadParticipant.objects.filter( - thread=thread, - user=request.user, - ).update(last_read_at=message.created_at) - elif thread.last_message_at is None: - thread.last_message_at = timezone.now() - thread.save(update_fields=["last_message_at"]) - - return thread - - -class DirectMessageCreateSerializer(serializers.Serializer): - """Validate a new outbound direct message body.""" - - body = serializers.CharField(trim_whitespace=True) diff --git a/messaging/tests/test_api.py b/messaging/tests/test_api.py deleted file mode 100644 index c04d7b8c..00000000 --- a/messaging/tests/test_api.py +++ /dev/null @@ -1,172 +0,0 @@ -"""API tests for direct-message threads and messages.""" - -from __future__ import annotations - -from typing import Any, cast - -from django.contrib.auth import get_user_model -from django.db.models import Model -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from messaging.models import DirectMessage, Thread, ThreadParticipant -from projects.models import Project, ProjectMembership, ProjectRole - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF client so type checkers see authentication helpers.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create one user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -class MessagingApiTests(APITestCase): - """Exercise the direct-message thread endpoints.""" - - def setUp(self): - user_model = get_user_model() - self.alice = _create_user(user_model, username="alice", password="testpass123") - self.bob = _create_user(user_model, username="bob", password="testpass123") - self.carol = _create_user(user_model, username="carol", password="testpass123") - self.shared_project = Project.objects.create( - name="Shared Project", - topic_description="Delivery", - ) - self.private_project = Project.objects.create( - name="Private Project", - topic_description="Security", - ) - ProjectMembership.objects.create( - user=self.alice, - project=self.shared_project, - role=ProjectRole.ADMIN, - ) - ProjectMembership.objects.create( - user=self.bob, - project=self.shared_project, - role=ProjectRole.MEMBER, - ) - ProjectMembership.objects.create( - user=self.carol, - project=self.private_project, - role=ProjectRole.ADMIN, - ) - _typed_client(self.client).force_authenticate(self.alice) - - def _create_thread(self) -> Thread: - thread = Thread.objects.create() - ThreadParticipant.objects.create(thread=thread, user=self.alice) - ThreadParticipant.objects.create(thread=thread, user=self.bob) - return thread - - def test_thread_list_is_scoped_to_the_current_participant(self): - visible_thread = self._create_thread() - hidden_thread = Thread.objects.create() - ThreadParticipant.objects.create(thread=hidden_thread, user=self.bob) - ThreadParticipant.objects.create(thread=hidden_thread, user=self.carol) - DirectMessage.objects.create( - thread=visible_thread, - sender=self.bob, - body="Draft is ready.", - ) - - response = self.client.get(reverse("v1:messaging-thread-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - payload = response.json()[0] - self.assertEqual(payload["id"], _require_pk(visible_thread)) - self.assertEqual(payload["counterpart"]["username"], "bob") - self.assertEqual(payload["last_message_preview"], "Draft is ready.") - self.assertTrue(payload["has_unread"]) - - def test_thread_create_requires_a_shared_project(self): - response = self.client.post( - reverse("v1:messaging-thread-list"), - {"recipient_user_id": _require_pk(self.carol)}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.json()["errors"][0]["attr"], "recipient_user_id") - - def test_thread_create_creates_a_thread_and_optional_opening_message(self): - response = self.client.post( - reverse("v1:messaging-thread-list"), - { - "recipient_user_id": _require_pk(self.bob), - "opening_message": "Want to review this draft together?", - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - thread = Thread.objects.get(pk=response.json()["id"]) - self.assertEqual(thread.participants.count(), 2) - self.assertEqual(thread.messages.count(), 1) - self.assertEqual( - thread.messages.get().body, "Want to review this draft together?" - ) - self.assertEqual(response.json()["counterpart"]["username"], "bob") - - def test_messages_action_lists_and_creates_messages_for_a_participant(self): - thread = self._create_thread() - DirectMessage.objects.create( - thread=thread, sender=self.bob, body="Initial note" - ) - - list_response = self.client.get( - reverse("v1:messaging-thread-messages", kwargs={"pk": _require_pk(thread)}) - ) - - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(len(list_response.json()), 1) - self.assertEqual(list_response.json()[0]["sender_username"], "bob") - - create_response = self.client.post( - reverse("v1:messaging-thread-messages", kwargs={"pk": _require_pk(thread)}), - {"body": "Replying now."}, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - self.assertEqual(create_response.json()["sender_username"], "alice") - thread.refresh_from_db() - self.assertIsNotNone(thread.last_message_at) - - def test_read_action_updates_the_current_users_last_read_cursor(self): - thread = self._create_thread() - message = DirectMessage.objects.create( - thread=thread, - sender=self.bob, - body="Unread message", - ) - thread.last_message_at = message.created_at - thread.save(update_fields=["last_message_at"]) - - response = self.client.post( - reverse("v1:messaging-thread-read", kwargs={"pk": _require_pk(thread)}), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - participant_state = ThreadParticipant.objects.get( - thread=thread, user=self.alice - ) - self.assertEqual(response.json()["thread_id"], _require_pk(thread)) - self.assertIsNotNone(participant_state.last_read_at) diff --git a/messaging/tests/test_ninja_api.py b/messaging/tests/test_ninja_api.py new file mode 100644 index 00000000..78ddec82 --- /dev/null +++ b/messaging/tests/test_ninja_api.py @@ -0,0 +1,198 @@ +"""Tests for the Django Ninja messaging API surface.""" + +from __future__ import annotations + +from http import HTTPStatus +from typing import Any, cast + +import pytest +from django.contrib.auth import get_user_model +from django.test import Client + +from messaging.models import DirectMessage, Thread, ThreadParticipant +from projects.models import Project, ProjectMembership, ProjectRole + +pytestmark = pytest.mark.django_db + + +def _response(value: object) -> Any: + """Return a typed response object for assertions.""" + + return cast(Any, value) + + +def _create_user(**kwargs: object): + """Create one user through the custom manager with a typed escape hatch.""" + + user_model = get_user_model() + return cast(Any, user_model.objects).create_user(**kwargs) + + +def _create_fixture_users_and_projects(): + """Build a shared messaging fixture.""" + + alice = _create_user(username="alice-ninja", password="testpass123") + bob = _create_user(username="bob-ninja", password="testpass123") + carol = _create_user(username="carol-ninja", password="testpass123") + shared_project = Project.objects.create( + name="Shared Project", topic_description="Delivery" + ) + private_project = Project.objects.create( + name="Private Project", topic_description="Security" + ) + ProjectMembership.objects.create( + user=alice, project=shared_project, role=ProjectRole.ADMIN + ) + ProjectMembership.objects.create( + user=bob, project=shared_project, role=ProjectRole.MEMBER + ) + ProjectMembership.objects.create( + user=carol, project=private_project, role=ProjectRole.ADMIN + ) + return alice, bob, carol + + +def _create_thread(alice, bob) -> Thread: + """Create one direct-message thread between two fixture users.""" + + thread = Thread.objects.create() + ThreadParticipant.objects.create(thread=thread, user=alice) + ThreadParticipant.objects.create(thread=thread, user=bob) + return thread + + +def test_ninja_thread_list_is_scoped_to_the_current_participant(): + alice, bob, carol = _create_fixture_users_and_projects() + visible_thread = _create_thread(alice, bob) + hidden_thread = Thread.objects.create() + ThreadParticipant.objects.create(thread=hidden_thread, user=bob) + ThreadParticipant.objects.create(thread=hidden_thread, user=carol) + DirectMessage.objects.create( + thread=visible_thread, sender=bob, body="Draft is ready." + ) + + client = Client() + client.force_login(alice) + response = _response(client.get("/api/v1/messaging/threads/")) + + assert response.status_code == HTTPStatus.OK + assert len(response.json()) == 1 + payload = response.json()[0] + assert payload["id"] == int(visible_thread.pk) + assert payload["counterpart"]["username"] == "bob-ninja" + assert payload["last_message_preview"] == "Draft is ready." + assert payload["has_unread"] is True + + +def test_ninja_thread_create_requires_a_shared_project(): + alice, _, carol = _create_fixture_users_and_projects() + + client = Client() + client.force_login(alice) + response = _response( + client.post( + "/api/v1/messaging/threads/", + {"recipient_user_id": int(carol.pk)}, + content_type="application/json", + ) + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert ( + response.json()["recipient_user_id"][0] + == "You can only message users who share a project with you." + ) + + +def test_ninja_thread_create_rejects_messaging_yourself(): + alice, _, _ = _create_fixture_users_and_projects() + + client = Client() + client.force_login(alice) + response = _response( + client.post( + "/api/v1/messaging/threads/", + {"recipient_user_id": int(alice.pk)}, + content_type="application/json", + ) + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.json()["recipient_user_id"][0] == "You cannot message yourself." + + +def test_ninja_thread_create_creates_a_thread_and_optional_opening_message(): + alice, bob, _ = _create_fixture_users_and_projects() + + client = Client() + client.force_login(alice) + response = _response( + client.post( + "/api/v1/messaging/threads/", + { + "recipient_user_id": int(bob.pk), + "opening_message": "Want to review this draft together?", + }, + content_type="application/json", + ) + ) + + assert response.status_code == HTTPStatus.CREATED + thread = Thread.objects.get(pk=response.json()["id"]) + assert thread.participants.count() == 2 + assert thread.messages.count() == 1 + assert thread.messages.get().body == "Want to review this draft together?" + assert response.json()["counterpart"]["username"] == "bob-ninja" + + +def test_ninja_messages_endpoint_lists_and_creates_messages_for_a_participant(): + alice, bob, _ = _create_fixture_users_and_projects() + thread = _create_thread(alice, bob) + DirectMessage.objects.create(thread=thread, sender=bob, body="Initial note") + + client = Client() + client.force_login(alice) + list_response = _response( + client.get(f"/api/v1/messaging/threads/{thread.pk}/messages/") + ) + + assert list_response.status_code == HTTPStatus.OK + assert len(list_response.json()) == 1 + assert list_response.json()[0]["sender_username"] == "bob-ninja" + + create_response = _response( + client.post( + f"/api/v1/messaging/threads/{thread.pk}/messages/", + {"body": "Replying now."}, + content_type="application/json", + ) + ) + + assert create_response.status_code == HTTPStatus.CREATED + assert create_response.json()["sender_username"] == "alice-ninja" + thread.refresh_from_db() + assert thread.last_message_at is not None + + +def test_ninja_read_action_updates_the_current_users_last_read_cursor(): + alice, bob, _ = _create_fixture_users_and_projects() + thread = _create_thread(alice, bob) + message = DirectMessage.objects.create( + thread=thread, sender=bob, body="Unread message" + ) + thread.last_message_at = message.created_at + thread.save(update_fields=["last_message_at"]) + + client = Client() + client.force_login(alice) + response = _response( + client.post( + f"/api/v1/messaging/threads/{thread.pk}/read/", + content_type="application/json", + ) + ) + + assert response.status_code == HTTPStatus.OK + participant_state = ThreadParticipant.objects.get(thread=thread, user=alice) + assert response.json()["thread_id"] == int(thread.pk) + assert participant_state.last_read_at is not None diff --git a/newsletters/api.py b/newsletters/api.py deleted file mode 100644 index 7bbea7fc..00000000 --- a/newsletters/api.py +++ /dev/null @@ -1,383 +0,0 @@ -"""Newsletter-domain API viewsets kept under the existing nested project routes.""" - -from typing import Any, cast - -from django.conf import settings -from django.db.models import Prefetch -from django.utils import timezone -from drf_spectacular.utils import extend_schema, inline_serializer -from rest_framework import mixins, serializers, status, viewsets -from rest_framework.decorators import action -from rest_framework.filters import OrderingFilter -from rest_framework.response import Response - -from core.api import ( - AUTHENTICATION_REQUIRED_RESPONSE, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import IsProjectContributor, IsProjectMember -from newsletters.models import ( - IntakeAllowlist, - NewsletterDraft, - NewsletterDraftItem, - NewsletterDraftOriginalPiece, - NewsletterDraftSection, - NewsletterDraftStatus, - NewsletterIntake, -) -from newsletters.serializers import ( - IntakeAllowlistSerializer, - NewsletterDraftItemSerializer, - NewsletterDraftOriginalPieceSerializer, - NewsletterDraftRegenerateSectionSerializer, - NewsletterDraftSectionSerializer, - NewsletterDraftSerializer, - NewsletterIntakeSerializer, -) -from newsletters.tasks import ( - generate_newsletter_draft, - regenerate_newsletter_draft_section, -) - - -@document_project_owned_viewset( - resource_plural="intake allowlist entries", - resource_singular="intake allowlist entry", - create_description=( - "Create a new confirmed or pending sender allowlist entry for the selected " - "project's newsletter intake workflow." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - IntakeAllowlistSerializer, - resource_plural="intake allowlist entries for the selected project", - resource_singular="intake allowlist entry", - ), -) -class IntakeAllowlistViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage newsletter sender allowlist entries for a project.""" - - serializer_class = IntakeAllowlistSerializer - queryset = IntakeAllowlist.objects.select_related("project") - - def get_permissions(self): - """Restrict intake allowlist access to project contributors.""" - - return [IsProjectContributor()] - - -@document_project_owned_viewset( - resource_plural="newsletter intake entries", - resource_singular="newsletter intake entry", - create_description=( - "Newsletter intake entries are created by inbound email processing and are " - "exposed read-only for audit and troubleshooting." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - NewsletterIntakeSerializer, - resource_plural="newsletter intake entries for the selected project", - resource_singular="newsletter intake entry", - ), -) -class NewsletterIntakeViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect inbound newsletter history for a project.""" - - serializer_class = NewsletterIntakeSerializer - queryset = NewsletterIntake.objects.select_related("project") - - def get_permissions(self): - """Allow any project member to inspect newsletter intake history.""" - - return [IsProjectMember()] - - -class ProjectRelatedQuerysetMixin(ProjectOwnedQuerysetMixin): - """Scope nested resources to a project through an indirect relationship path.""" - - project_filter: str = "project" - - def get_queryset(self): - """Filter the configured queryset down to the current project path.""" - - queryset = self.queryset - if queryset is None: - raise AssertionError("queryset must be set on project-scoped viewsets") - return queryset.filter(**{self.project_filter: self.get_project()}) - - -class DraftEditedTimestampMixin: - """Touch the owning draft whenever an editor mutates a draft subtree.""" - - draft_lookup: str = "draft" - - def perform_update(self, serializer): - """Persist the mutation and mark the owning draft as editor-modified.""" - - instance = serializer.save() - draft = cast(NewsletterDraft, _resolve_nested_attr(instance, self.draft_lookup)) - draft.status = NewsletterDraftStatus.EDITED - draft.last_edited_at = timezone.now() - draft.save(update_fields=["status", "last_edited_at"]) - - def perform_destroy(self, instance): - """Delete the subtree node and mark the owning draft as editor-modified.""" - - draft = cast(NewsletterDraft, _resolve_nested_attr(instance, self.draft_lookup)) - instance.delete() - draft.status = NewsletterDraftStatus.EDITED - draft.last_edited_at = timezone.now() - draft.save(update_fields=["status", "last_edited_at"]) - - -class NewsletterDraftViewSet( - ProjectOwnedQuerysetMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet, -): - """Inspect, edit, and generate project-scoped newsletter drafts.""" - - serializer_class = NewsletterDraftSerializer - filter_backends = [OrderingFilter] - ordering_fields = ["generated_at", "target_publish_date", "status"] - ordering = ["-generated_at", "-id"] - http_method_names = ["get", "patch", "post", "head", "options"] - queryset = NewsletterDraft.objects.select_related("project").prefetch_related( - Prefetch( - "sections", - queryset=NewsletterDraftSection.objects.select_related( - "theme_suggestion" - ).prefetch_related( - Prefetch( - "items", - queryset=NewsletterDraftItem.objects.select_related("content"), - ) - ), - ), - Prefetch( - "original_pieces", - queryset=NewsletterDraftOriginalPiece.objects.select_related("idea"), - ), - ) - - def get_permissions(self): - """Allow members to read drafts and contributors to mutate or generate.""" - - if self.action in {"generate", "partial_update", "regenerate_section"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - def perform_update(self, serializer): - """Persist top-level draft edits and mark the draft as editor-modified.""" - - serializer.save( - status=NewsletterDraftStatus.EDITED, - last_edited_at=timezone.now(), - ) - - @extend_schema( - summary="Generate newsletter draft", - description=( - "Trigger newsletter draft generation for the selected project. When Celery " - "runs eagerly, the draft is composed before the response is returned. " - "Otherwise, the generation task is queued for background execution." - ), - request=None, - responses={ - 200: inline_serializer( - name="NewsletterDraftGenerateCompletedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - "result": inline_serializer( - name="NewsletterDraftGenerateResult", - fields={ - "project_id": serializers.IntegerField(), - "draft_id": serializers.IntegerField(allow_null=True), - "status": serializers.CharField(), - "reason": serializers.CharField(required=False), - "sections_created": serializers.IntegerField(), - "original_pieces_created": serializers.IntegerField(), - }, - ), - }, - ), - 202: inline_serializer( - name="NewsletterDraftGenerateQueuedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - }, - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Newsletter Composition"], - ) - @action(detail=False, methods=["post"], url_path="generate") - def generate(self, request, *args, **kwargs): - """Trigger draft composition for the current project.""" - - project = self.get_project() - project_id = int(project.pk) - if settings.CELERY_TASK_ALWAYS_EAGER: - result = generate_newsletter_draft(project_id) - return Response( - { - "status": "completed", - "project_id": project_id, - "result": result, - } - ) - generate_newsletter_draft.delay(project_id) - return Response( - {"status": "queued", "project_id": project_id}, - status=status.HTTP_202_ACCEPTED, - ) - - @extend_schema( - summary="Regenerate newsletter draft section", - description=( - "Recompose one section inside an existing draft without rebuilding the rest " - "of the draft tree." - ), - request=NewsletterDraftRegenerateSectionSerializer, - responses={ - 200: NewsletterDraftSerializer, - 202: inline_serializer( - name="NewsletterDraftRegenerateQueuedResponse", - fields={ - "status": serializers.CharField(), - "draft_id": serializers.IntegerField(), - "section_id": serializers.IntegerField(), - }, - ), - 400: NewsletterDraftRegenerateSectionSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Newsletter Composition"], - ) - @action(detail=True, methods=["post"], url_path="regenerate_section") - def regenerate_section(self, request, *args, **kwargs): - """Recompose one draft section for the selected draft.""" - - draft = self.get_object() - serializer = NewsletterDraftRegenerateSectionSerializer( - data=request.data, - context={**self.get_serializer_context(), "draft": draft}, - ) - serializer.is_valid(raise_exception=True) - section_id = serializer.validated_data["section_id"] - if settings.CELERY_TASK_ALWAYS_EAGER: - regenerate_newsletter_draft_section(section_id) - draft.refresh_from_db() - response_serializer = self.get_serializer(draft) - return Response(response_serializer.data) - regenerate_newsletter_draft_section.delay(section_id) - return Response( - { - "status": "queued", - "draft_id": int(draft.pk), - "section_id": section_id, - }, - status=status.HTTP_202_ACCEPTED, - ) - - -class NewsletterDraftSectionViewSet( - ProjectRelatedQuerysetMixin, - DraftEditedTimestampMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet, -): - """Edit or remove draft sections under one project.""" - - serializer_class = NewsletterDraftSectionSerializer - project_filter = "draft__project" - draft_lookup = "draft" - http_method_names = ["get", "patch", "delete", "head", "options"] - queryset = NewsletterDraftSection.objects.select_related( - "draft", - "theme_suggestion", - ).prefetch_related( - Prefetch( - "items", - queryset=NewsletterDraftItem.objects.select_related("content"), - ) - ) - - def get_permissions(self): - """Allow members to read sections and contributors to edit them.""" - - if self.action in {"partial_update", "destroy"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - -class NewsletterDraftItemViewSet( - ProjectRelatedQuerysetMixin, - DraftEditedTimestampMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet, -): - """Edit or remove draft items under one project.""" - - serializer_class = NewsletterDraftItemSerializer - project_filter = "section__draft__project" - draft_lookup = "section.draft" - http_method_names = ["get", "patch", "delete", "head", "options"] - queryset = NewsletterDraftItem.objects.select_related( - "section", - "section__draft", - "content", - ) - - def get_permissions(self): - """Allow members to read items and contributors to edit them.""" - - if self.action in {"partial_update", "destroy"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - -class NewsletterDraftOriginalPieceViewSet( - ProjectRelatedQuerysetMixin, - DraftEditedTimestampMixin, - mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - viewsets.GenericViewSet, -): - """Edit or remove draft original-content blocks under one project.""" - - serializer_class = NewsletterDraftOriginalPieceSerializer - project_filter = "draft__project" - draft_lookup = "draft" - http_method_names = ["get", "patch", "delete", "head", "options"] - queryset = NewsletterDraftOriginalPiece.objects.select_related("draft", "idea") - - def get_permissions(self): - """Allow members to read original pieces and contributors to edit them.""" - - if self.action in {"partial_update", "destroy"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - -def _resolve_nested_attr(instance: object, dotted_path: str) -> object: - """Resolve a dotted attribute path on a loaded model instance.""" - - current: Any = instance - for attribute in dotted_path.split("."): - current = getattr(current, attribute) - return current diff --git a/newsletters/api_urls.py b/newsletters/api_urls.py deleted file mode 100644 index 7c25e0a8..00000000 --- a/newsletters/api_urls.py +++ /dev/null @@ -1,47 +0,0 @@ -"""API route registration for newsletter intake resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from newsletters.api import ( - IntakeAllowlistViewSet, - NewsletterDraftItemViewSet, - NewsletterDraftOriginalPieceViewSet, - NewsletterDraftSectionViewSet, - NewsletterDraftViewSet, - NewsletterIntakeViewSet, -) - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested newsletter intake endpoints.""" - - project_router.register( - r"intake-allowlist", - IntakeAllowlistViewSet, - basename="project-intake-allowlist", - ) - project_router.register( - r"newsletter-intakes", - NewsletterIntakeViewSet, - basename="project-newsletter-intake", - ) - project_router.register( - r"drafts", - NewsletterDraftViewSet, - basename="project-newsletter-draft", - ) - project_router.register( - r"draft-sections", - NewsletterDraftSectionViewSet, - basename="project-newsletter-draft-section", - ) - project_router.register( - r"draft-items", - NewsletterDraftItemViewSet, - basename="project-newsletter-draft-item", - ) - project_router.register( - r"draft-original-pieces", - NewsletterDraftOriginalPieceViewSet, - basename="project-newsletter-draft-original-piece", - ) diff --git a/newsletters/intake.py b/newsletters/intake.py index 34193999..53b5f980 100644 --- a/newsletters/intake.py +++ b/newsletters/intake.py @@ -5,15 +5,15 @@ from email.utils import parseaddr from html import escape from html.parser import HTMLParser -from typing import Any, Iterable, Protocol, cast +from typing import Any, Iterable, cast -from celery import current_app from django.conf import settings as django_settings from django.core.mail import EmailMultiAlternatives from django.db.models import Model from django.urls import reverse from core.settings_types import CoreSettings +from digest_engine.taskiq import enqueue_task, run_task_inline, task_always_eager from newsletters.extraction import extract_newsletter_items from newsletters.models import IntakeAllowlist, NewsletterIntake from projects.models import Project @@ -21,16 +21,6 @@ settings = cast(CoreSettings, django_settings) -class QueuedTask(Protocol): - """Protocol for Celery tasks used by newsletter intake dispatch.""" - - def apply(self, *args: object, **kwargs: object) -> object: - pass - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - def _require_pk(instance: Model) -> int: """Return a saved model primary key for newsletter intake responses.""" @@ -40,13 +30,12 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -def _process_newsletter_intake_task() -> QueuedTask: - """Resolve the Celery task used to process stored intake rows.""" +def _process_newsletter_intake_task() -> object: + """Resolve the Taskiq task used to process stored intake rows.""" + + from newsletters.tasks import process_newsletter_intake - task = cast(Any, current_app.tasks).get("core.tasks.process_newsletter_intake") - if task is None: - raise RuntimeError("core.tasks.process_newsletter_intake is not registered") - return cast(QueuedTask, task) + return process_newsletter_intake __all__ = [ @@ -312,10 +301,10 @@ def queue_newsletter_intake(intake_id: int) -> None: """Dispatch newsletter extraction for a stored intake row.""" process_newsletter_intake = _process_newsletter_intake_task() - if settings.CELERY_TASK_ALWAYS_EAGER: - process_newsletter_intake.apply(args=(intake_id,), throw=True) + if task_always_eager(): + run_task_inline(process_newsletter_intake, intake_id) else: - process_newsletter_intake.delay(intake_id) + enqueue_task(process_newsletter_intake, intake_id) def _find_intake_project(recipients: Iterable[str]) -> Project | None: diff --git a/newsletters/ninja_api.py b/newsletters/ninja_api.py new file mode 100644 index 00000000..6eb2b65e --- /dev/null +++ b/newsletters/ninja_api.py @@ -0,0 +1,983 @@ +"""Django Ninja endpoints for project-scoped newsletter resources.""" + +from __future__ import annotations + +from typing import Any + +from django.core.exceptions import ValidationError +from django.db.models import Prefetch +from django.utils import timezone +from ninja import Body, Path, Router +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from digest_engine.taskiq import enqueue_task, task_always_eager +from newsletters.models import ( + IntakeAllowlist, + NewsletterDraft, + NewsletterDraftItem, + NewsletterDraftOriginalPiece, + NewsletterDraftSection, + NewsletterDraftStatus, + NewsletterIntake, +) +from newsletters.tasks import ( + generate_newsletter_draft, + regenerate_newsletter_draft_section, +) +from projects.ninja_helpers import _get_project_or_404, _require_project_writable + +intake_allowlist_router = Router(tags=["Ingestion"]) +newsletter_intakes_router = Router(tags=["Ingestion"]) +newsletter_drafts_router = Router(tags=["Newsletter Composition"]) +newsletter_draft_sections_router = Router(tags=["Newsletter Composition"]) +newsletter_draft_items_router = Router(tags=["Newsletter Composition"]) +newsletter_draft_original_pieces_router = Router(tags=["Newsletter Composition"]) + + +def _serialize_allowlist(entry: IntakeAllowlist) -> dict[str, Any]: + """Return one intake allowlist response body.""" + + return { + "id": int(entry.pk), + "project": entry.project_id, + "sender_email": entry.sender_email, + "is_confirmed": entry.is_confirmed, + "confirmed_at": entry.confirmed_at.isoformat() if entry.confirmed_at else None, + "confirmation_token": entry.confirmation_token, + "created_at": entry.created_at.isoformat(), + } + + +def _serialize_intake(intake: NewsletterIntake) -> dict[str, Any]: + """Return one newsletter intake response body.""" + + return { + "id": int(intake.pk), + "project": intake.project_id, + "sender_email": intake.sender_email, + "subject": intake.subject, + "received_at": intake.received_at.isoformat(), + "raw_html": intake.raw_html, + "raw_text": intake.raw_text, + "message_id": intake.message_id, + "status": intake.status, + "extraction_result": intake.extraction_result, + "error_message": intake.error_message, + } + + +def _serialize_draft_item_content(item: NewsletterDraftItem) -> dict[str, Any]: + """Return the embedded content summary for one draft item.""" + + content = item.content + return { + "id": content.id, + "url": content.url, + "title": content.title, + "source_plugin": content.source_plugin, + "published_date": ( + content.published_date.isoformat() if content.published_date else None + ), + } + + +def _serialize_draft(draft: NewsletterDraft) -> dict[str, Any]: + """Return one newsletter draft response body.""" + + return { + "id": int(draft.pk), + "project": draft.project_id, + "title": draft.title, + "intro": draft.intro, + "outro": draft.outro, + "target_publish_date": ( + draft.target_publish_date.isoformat() if draft.target_publish_date else None + ), + "status": draft.status, + "generated_at": draft.generated_at.isoformat(), + "last_edited_at": ( + draft.last_edited_at.isoformat() if draft.last_edited_at else None + ), + "generation_metadata": draft.generation_metadata, + "sections": [ + _serialize_draft_section(section) for section in draft.sections.all() + ], + "original_pieces": [ + _serialize_original_piece(original_piece) + for original_piece in draft.original_pieces.all() + ], + "rendered_markdown": draft.render_markdown(), + "rendered_html": draft.render_html(), + } + + +def _serialize_theme_suggestion( + section: NewsletterDraftSection, +) -> dict[str, Any] | None: + """Return the embedded accepted-theme summary for one draft section.""" + + theme = section.theme_suggestion + if theme is None: + return None + return { + "id": int(theme.pk), + "title": theme.title, + "pitch": theme.pitch, + "why_it_matters": theme.why_it_matters, + } + + +def _serialize_draft_section(section: NewsletterDraftSection) -> dict[str, Any]: + """Return one newsletter draft section response body.""" + + return { + "id": int(section.pk), + "draft": section.draft_id, + "theme_suggestion": section.theme_suggestion_id, + "theme_suggestion_detail": _serialize_theme_suggestion(section), + "title": section.title, + "lede": section.lede, + "order": section.order, + "items": [_serialize_draft_item(item) for item in section.items.all()], + } + + +def _serialize_draft_item(item: NewsletterDraftItem) -> dict[str, Any]: + """Return one newsletter draft item response body.""" + + return { + "id": int(item.pk), + "section": item.section_id, + "content": item.content_id, + "content_detail": _serialize_draft_item_content(item), + "summary_used": item.summary_used, + "why_it_matters": item.why_it_matters, + "order": item.order, + } + + +def _serialize_original_idea( + original_piece: NewsletterDraftOriginalPiece, +) -> dict[str, Any]: + """Return the embedded accepted-idea summary for one original piece.""" + + idea = original_piece.idea + return { + "id": int(idea.pk), + "angle_title": idea.angle_title, + "summary": idea.summary, + "suggested_outline": idea.suggested_outline, + } + + +def _serialize_original_piece( + original_piece: NewsletterDraftOriginalPiece, +) -> dict[str, Any]: + """Return one newsletter draft original-piece response body.""" + + return { + "id": int(original_piece.pk), + "draft": original_piece.draft_id, + "idea": original_piece.idea_id, + "idea_detail": _serialize_original_idea(original_piece), + "title": original_piece.title, + "pitch": original_piece.pitch, + "suggested_outline": original_piece.suggested_outline, + "order": original_piece.order, + } + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the native API error payload.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _unknown_field_errors( + payload: dict[str, Any], + *, + accepted_fields: set[str], +) -> dict[str, list[str]] | None: + """Reject fields that are not part of the supported payload contract.""" + + errors = { + field_name: ["This field is not allowed."] + for field_name in payload + if field_name not in accepted_fields + } + return errors or None + + +def _validated_model_payload( + instance: ( + IntakeAllowlist + | NewsletterDraft + | NewsletterDraftSection + | NewsletterDraftItem + | NewsletterDraftOriginalPiece + ), + payload: dict[str, Any], + *, + editable_fields: set[str], + accepted_fields: set[str], +) -> tuple[dict[str, list[str]] | None, list[str]]: + """Validate one partial model payload and return the touched field names.""" + + errors = _unknown_field_errors(payload, accepted_fields=accepted_fields) + if errors is not None: + return errors, [] + + updated_fields: list[str] = [] + for field_name in editable_fields: + if field_name in payload: + setattr(instance, field_name, payload[field_name]) + updated_fields.append(field_name) + try: + instance.full_clean() + except ValidationError as exc: + return _validation_error_payload(exc), updated_fields + return None, updated_fields + + +def _validated_regenerate_section_payload( + payload: dict[str, Any], + *, + draft: NewsletterDraft, +) -> tuple[int | None, dict[str, list[str]] | None]: + """Validate the draft-section regeneration payload.""" + + errors = _unknown_field_errors(payload, accepted_fields={"section_id"}) + if errors is not None: + return None, errors + if "section_id" not in payload: + return None, {"section_id": ["This field is required."]} + + raw_section_id = payload["section_id"] + try: + section_id = int(raw_section_id) + except (TypeError, ValueError): + return None, {"section_id": ["A valid integer is required."]} + + exists = NewsletterDraftSection.objects.filter( + pk=section_id, + draft=draft, + draft__project=draft.project, + ).exists() + if not exists: + return None, {"section_id": ["Draft section not found for this project."]} + return section_id, None + + +def _draft_queryset(): + """Return the canonical queryset for newsletter drafts.""" + + return NewsletterDraft.objects.select_related("project").prefetch_related( + Prefetch( + "sections", + queryset=NewsletterDraftSection.objects.select_related( + "theme_suggestion" + ).prefetch_related( + Prefetch( + "items", + queryset=NewsletterDraftItem.objects.select_related("content"), + ) + ), + ), + Prefetch( + "original_pieces", + queryset=NewsletterDraftOriginalPiece.objects.select_related("idea"), + ), + ) + + +def _get_allowlist_or_404(project_id: int, allowlist_id: int) -> IntakeAllowlist: + """Load one intake allowlist entry for the selected project.""" + + entry = IntakeAllowlist.objects.filter( + project_id=project_id, pk=allowlist_id + ).first() + if not entry: + raise HttpError(404, "Not found.") + return entry + + +def _get_newsletter_intake_or_404(project_id: int, intake_id: int) -> NewsletterIntake: + """Load one newsletter intake row for the selected project.""" + + intake = ( + NewsletterIntake.objects.select_related("project") + .filter( + project_id=project_id, + pk=intake_id, + ) + .first() + ) + if not intake: + raise HttpError(404, "Not found.") + return intake + + +def _get_draft_or_404(project_id: int, draft_id: int) -> NewsletterDraft: + """Load one newsletter draft for the selected project.""" + + draft = _draft_queryset().filter(project_id=project_id, pk=draft_id).first() + if not draft: + raise HttpError(404, "Not found.") + return draft + + +def _get_draft_section_or_404( + project_id: int, section_id: int +) -> NewsletterDraftSection: + """Load one newsletter draft section for the selected project.""" + + section = ( + NewsletterDraftSection.objects.select_related( + "draft", + "theme_suggestion", + ) + .prefetch_related( + Prefetch( + "items", + queryset=NewsletterDraftItem.objects.select_related("content"), + ) + ) + .filter( + draft__project_id=project_id, + pk=section_id, + ) + .first() + ) + if not section: + raise HttpError(404, "Not found.") + return section + + +def _get_draft_item_or_404(project_id: int, item_id: int) -> NewsletterDraftItem: + """Load one newsletter draft item for the selected project.""" + + item = ( + NewsletterDraftItem.objects.select_related( + "section", + "section__draft", + "content", + ) + .filter( + section__draft__project_id=project_id, + pk=item_id, + ) + .first() + ) + if not item: + raise HttpError(404, "Not found.") + return item + + +def _get_original_piece_or_404( + project_id: int, original_piece_id: int +) -> NewsletterDraftOriginalPiece: + """Load one newsletter draft original piece for the selected project.""" + + original_piece = ( + NewsletterDraftOriginalPiece.objects.select_related( + "draft", + "idea", + ) + .filter( + draft__project_id=project_id, + pk=original_piece_id, + ) + .first() + ) + if not original_piece: + raise HttpError(404, "Not found.") + return original_piece + + +def _mark_draft_edited(draft: NewsletterDraft) -> None: + """Mark one newsletter draft as editor-modified.""" + + draft.status = NewsletterDraftStatus.EDITED + draft.last_edited_at = timezone.now() + draft.save(update_fields=["status", "last_edited_at"]) + + +@intake_allowlist_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_intake_allowlist(request: Any, project_id: int = Path(...)): + """List intake allowlist entries for the selected project.""" + + _require_project_writable(request, project_id) + entries = IntakeAllowlist.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_allowlist(entry) for entry in entries] + + +@intake_allowlist_router.post( + "/", + response={201: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_intake_allowlist( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), +): + """Create one intake allowlist entry for the selected project.""" + + project = _require_project_writable(request, project_id) + entry = IntakeAllowlist(project=project) + errors, _ = _validated_model_payload( + entry, + payload, + editable_fields={"sender_email"}, + accepted_fields={ + "id", + "project", + "sender_email", + "is_confirmed", + "confirmed_at", + "confirmation_token", + "created_at", + }, + ) + if errors is not None: + return Status(400, errors) + entry.save() + return Status(201, _serialize_allowlist(entry)) + + +@intake_allowlist_router.get( + "/{allowlist_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_intake_allowlist( + request: Any, + project_id: int = Path(...), + allowlist_id: int = Path(...), +): + """Return one intake allowlist entry.""" + + _require_project_writable(request, project_id) + return _serialize_allowlist(_get_allowlist_or_404(project_id, allowlist_id)) + + +@intake_allowlist_router.patch( + "/{allowlist_id}/", + response={200: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_intake_allowlist( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + allowlist_id: int = Path(...), +): + """Update one intake allowlist entry.""" + + _require_project_writable(request, project_id) + entry = _get_allowlist_or_404(project_id, allowlist_id) + errors, updated_fields = _validated_model_payload( + entry, + payload, + editable_fields={"sender_email"}, + accepted_fields={ + "id", + "project", + "sender_email", + "is_confirmed", + "confirmed_at", + "confirmation_token", + "created_at", + }, + ) + if errors is not None: + return Status(400, errors) + if updated_fields: + entry.save(update_fields=updated_fields) + return _serialize_allowlist(entry) + + +@intake_allowlist_router.delete( + "/{allowlist_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_intake_allowlist( + request: Any, + project_id: int = Path(...), + allowlist_id: int = Path(...), +): + """Delete one intake allowlist entry.""" + + _require_project_writable(request, project_id) + entry = _get_allowlist_or_404(project_id, allowlist_id) + entry.delete() + return Status(204, None) + + +@newsletter_intakes_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_newsletter_intakes(request: Any, project_id: int = Path(...)): + """List newsletter intake rows for the selected project.""" + + _get_project_or_404(request, project_id) + intakes = NewsletterIntake.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_intake(intake) for intake in intakes] + + +@newsletter_intakes_router.get( + "/{intake_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_newsletter_intake( + request: Any, + project_id: int = Path(...), + intake_id: int = Path(...), +): + """Return one newsletter intake row.""" + + _get_project_or_404(request, project_id) + return _serialize_intake(_get_newsletter_intake_or_404(project_id, intake_id)) + + +@newsletter_drafts_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_newsletter_drafts(request: Any, project_id: int = Path(...)): + """List newsletter drafts for the selected project.""" + + _get_project_or_404(request, project_id) + drafts = _draft_queryset().filter(project_id=project_id) + return [_serialize_draft(draft) for draft in drafts] + + +@newsletter_drafts_router.post( + "/generate/", + response={200: dict[str, Any], 202: dict[str, Any]}, + auth=api_authenticate, +) +def generate_newsletter_draft_route(request: Any, project_id: int = Path(...)): + """Trigger newsletter draft generation for the selected project.""" + + _require_project_writable(request, project_id) + if task_always_eager(): + result = generate_newsletter_draft(project_id) + return Status( + 200, + { + "status": "completed", + "project_id": project_id, + "result": result, + }, + ) + enqueue_task(generate_newsletter_draft, project_id) + return Status(202, {"status": "queued", "project_id": project_id}) + + +@newsletter_drafts_router.get( + "/{draft_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_newsletter_draft( + request: Any, + project_id: int = Path(...), + draft_id: int = Path(...), +): + """Return one newsletter draft.""" + + _get_project_or_404(request, project_id) + return _serialize_draft(_get_draft_or_404(project_id, draft_id)) + + +@newsletter_drafts_router.patch( + "/{draft_id}/", + response={200: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_newsletter_draft( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + draft_id: int = Path(...), +): + """Update one newsletter draft and mark it as editor-modified.""" + + _require_project_writable(request, project_id) + draft = _get_draft_or_404(project_id, draft_id) + errors, updated_fields = _validated_model_payload( + draft, + payload, + editable_fields={"title", "intro", "outro", "target_publish_date", "status"}, + accepted_fields={ + "id", + "project", + "title", + "intro", + "outro", + "target_publish_date", + "status", + "generated_at", + "last_edited_at", + "generation_metadata", + "sections", + "original_pieces", + "rendered_markdown", + "rendered_html", + }, + ) + if errors is not None: + return Status(400, errors) + draft.status = NewsletterDraftStatus.EDITED + draft.last_edited_at = timezone.now() + draft.save(update_fields=sorted({*updated_fields, "status", "last_edited_at"})) + return _serialize_draft(_get_draft_or_404(project_id, draft_id)) + + +@newsletter_drafts_router.post( + "/{draft_id}/regenerate_section/", + response={200: dict[str, Any], 202: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def regenerate_newsletter_draft_section_route( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + draft_id: int = Path(...), +): + """Recompose one section inside the selected newsletter draft.""" + + _require_project_writable(request, project_id) + draft = _get_draft_or_404(project_id, draft_id) + section_id, errors = _validated_regenerate_section_payload(payload, draft=draft) + if errors is not None or section_id is None: + return Status(400, errors or {"section_id": ["This field is required."]}) + if task_always_eager(): + regenerate_newsletter_draft_section(section_id) + draft.refresh_from_db() + return _serialize_draft(_get_draft_or_404(project_id, draft_id)) + enqueue_task(regenerate_newsletter_draft_section, section_id) + return Status( + 202, + { + "status": "queued", + "draft_id": int(draft.pk), + "section_id": section_id, + }, + ) + + +@newsletter_draft_sections_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_newsletter_draft_sections(request: Any, project_id: int = Path(...)): + """List newsletter draft sections for the selected project.""" + + _get_project_or_404(request, project_id) + sections = ( + NewsletterDraftSection.objects.select_related( + "draft", + "theme_suggestion", + ) + .prefetch_related( + Prefetch( + "items", + queryset=NewsletterDraftItem.objects.select_related("content"), + ) + ) + .filter(draft__project_id=project_id) + ) + return [_serialize_draft_section(section) for section in sections] + + +@newsletter_draft_sections_router.get( + "/{section_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_newsletter_draft_section( + request: Any, + project_id: int = Path(...), + section_id: int = Path(...), +): + """Return one newsletter draft section.""" + + _get_project_or_404(request, project_id) + return _serialize_draft_section(_get_draft_section_or_404(project_id, section_id)) + + +@newsletter_draft_sections_router.patch( + "/{section_id}/", + response={200: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_newsletter_draft_section( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + section_id: int = Path(...), +): + """Update one newsletter draft section and mark its draft as edited.""" + + _require_project_writable(request, project_id) + section = _get_draft_section_or_404(project_id, section_id) + errors, updated_fields = _validated_model_payload( + section, + payload, + editable_fields={"title", "lede", "order"}, + accepted_fields={ + "id", + "draft", + "theme_suggestion", + "theme_suggestion_detail", + "title", + "lede", + "order", + "items", + }, + ) + if errors is not None: + return Status(400, errors) + if updated_fields: + section.save(update_fields=updated_fields) + _mark_draft_edited(section.draft) + return _serialize_draft_section(_get_draft_section_or_404(project_id, section_id)) + + +@newsletter_draft_sections_router.delete( + "/{section_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_newsletter_draft_section( + request: Any, + project_id: int = Path(...), + section_id: int = Path(...), +): + """Delete one newsletter draft section and mark its draft as edited.""" + + _require_project_writable(request, project_id) + section = _get_draft_section_or_404(project_id, section_id) + draft = section.draft + section.delete() + _mark_draft_edited(draft) + return Status(204, None) + + +@newsletter_draft_items_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_newsletter_draft_items(request: Any, project_id: int = Path(...)): + """List newsletter draft items for the selected project.""" + + _get_project_or_404(request, project_id) + items = NewsletterDraftItem.objects.select_related( + "section", + "section__draft", + "content", + ).filter(section__draft__project_id=project_id) + return [_serialize_draft_item(item) for item in items] + + +@newsletter_draft_items_router.get( + "/{item_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_newsletter_draft_item( + request: Any, + project_id: int = Path(...), + item_id: int = Path(...), +): + """Return one newsletter draft item.""" + + _get_project_or_404(request, project_id) + return _serialize_draft_item(_get_draft_item_or_404(project_id, item_id)) + + +@newsletter_draft_items_router.patch( + "/{item_id}/", + response={200: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_newsletter_draft_item( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + item_id: int = Path(...), +): + """Update one newsletter draft item and mark its draft as edited.""" + + _require_project_writable(request, project_id) + item = _get_draft_item_or_404(project_id, item_id) + errors, updated_fields = _validated_model_payload( + item, + payload, + editable_fields={"summary_used", "why_it_matters", "order"}, + accepted_fields={ + "id", + "section", + "content", + "content_detail", + "summary_used", + "why_it_matters", + "order", + }, + ) + if errors is not None: + return Status(400, errors) + if updated_fields: + item.save(update_fields=updated_fields) + _mark_draft_edited(item.section.draft) + return _serialize_draft_item(_get_draft_item_or_404(project_id, item_id)) + + +@newsletter_draft_items_router.delete( + "/{item_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_newsletter_draft_item( + request: Any, + project_id: int = Path(...), + item_id: int = Path(...), +): + """Delete one newsletter draft item and mark its draft as edited.""" + + _require_project_writable(request, project_id) + item = _get_draft_item_or_404(project_id, item_id) + draft = item.section.draft + item.delete() + _mark_draft_edited(draft) + return Status(204, None) + + +@newsletter_draft_original_pieces_router.get( + "/", + response=list[dict[str, Any]], + auth=api_authenticate, +) +def list_newsletter_draft_original_pieces(request: Any, project_id: int = Path(...)): + """List newsletter draft original pieces for the selected project.""" + + _get_project_or_404(request, project_id) + original_pieces = NewsletterDraftOriginalPiece.objects.select_related( + "draft", + "idea", + ).filter(draft__project_id=project_id) + return [ + _serialize_original_piece(original_piece) for original_piece in original_pieces + ] + + +@newsletter_draft_original_pieces_router.get( + "/{original_piece_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_newsletter_draft_original_piece( + request: Any, + project_id: int = Path(...), + original_piece_id: int = Path(...), +): + """Return one newsletter draft original piece.""" + + _get_project_or_404(request, project_id) + return _serialize_original_piece( + _get_original_piece_or_404(project_id, original_piece_id) + ) + + +@newsletter_draft_original_pieces_router.patch( + "/{original_piece_id}/", + response={200: dict[str, Any], 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_newsletter_draft_original_piece( + request: Any, + payload: dict[str, Any] = Body(...), + project_id: int = Path(...), + original_piece_id: int = Path(...), +): + """Update one newsletter draft original piece and mark its draft as edited.""" + + _require_project_writable(request, project_id) + original_piece = _get_original_piece_or_404(project_id, original_piece_id) + errors, updated_fields = _validated_model_payload( + original_piece, + payload, + editable_fields={"title", "pitch", "suggested_outline", "order"}, + accepted_fields={ + "id", + "draft", + "idea", + "idea_detail", + "title", + "pitch", + "suggested_outline", + "order", + }, + ) + if errors is not None: + return Status(400, errors) + if updated_fields: + original_piece.save(update_fields=updated_fields) + _mark_draft_edited(original_piece.draft) + return _serialize_original_piece( + _get_original_piece_or_404(project_id, original_piece_id) + ) + + +@newsletter_draft_original_pieces_router.delete( + "/{original_piece_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_newsletter_draft_original_piece( + request: Any, + project_id: int = Path(...), + original_piece_id: int = Path(...), +): + """Delete one newsletter draft original piece and mark its draft as edited.""" + + _require_project_writable(request, project_id) + original_piece = _get_original_piece_or_404(project_id, original_piece_id) + draft = original_piece.draft + original_piece.delete() + _mark_draft_edited(draft) + return Status(204, None) + + +__all__ = [ + "intake_allowlist_router", + "newsletter_intakes_router", + "newsletter_drafts_router", + "newsletter_draft_sections_router", + "newsletter_draft_items_router", + "newsletter_draft_original_pieces_router", +] diff --git a/newsletters/serializers.py b/newsletters/serializers.py deleted file mode 100644 index af70e19b..00000000 --- a/newsletters/serializers.py +++ /dev/null @@ -1,237 +0,0 @@ -"""DRF serializers for newsletter-domain models.""" - -from rest_framework import serializers - -from core.serializer_mixins import ProjectScopedSerializerMixin -from newsletters.models import ( - IntakeAllowlist, - NewsletterDraft, - NewsletterDraftItem, - NewsletterDraftOriginalPiece, - NewsletterDraftSection, - NewsletterIntake, -) - - -class IntakeAllowlistSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize confirmed and pending newsletter sender allowlist entries.""" - - is_confirmed = serializers.BooleanField(read_only=True) - - class Meta: - model = IntakeAllowlist - fields = [ - "id", - "project", - "sender_email", - "is_confirmed", - "confirmed_at", - "confirmation_token", - "created_at", - ] - read_only_fields = ["id", "project", "confirmation_token", "created_at"] - - -class NewsletterIntakeSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize raw inbound newsletter messages captured for a project.""" - - class Meta: - model = NewsletterIntake - fields = [ - "id", - "project", - "sender_email", - "subject", - "received_at", - "raw_html", - "raw_text", - "message_id", - "status", - "extraction_result", - "error_message", - ] - read_only_fields = [ - "id", - "project", - "received_at", - "status", - "extraction_result", - "error_message", - ] - - -class NewsletterDraftItemContentSerializer(serializers.Serializer): - """Serialize minimal content metadata embedded in draft items.""" - - id = serializers.IntegerField() - url = serializers.URLField() - title = serializers.CharField() - source_plugin = serializers.CharField() - published_date = serializers.DateTimeField() - - -class NewsletterDraftItemSerializer(serializers.ModelSerializer): - """Serialize one editable draft item under a newsletter section.""" - - content_detail = NewsletterDraftItemContentSerializer( - source="content", read_only=True - ) - - class Meta: - model = NewsletterDraftItem - fields = [ - "id", - "section", - "content", - "content_detail", - "summary_used", - "why_it_matters", - "order", - ] - read_only_fields = ["id", "section", "content", "content_detail"] - - -class NewsletterDraftSectionThemeSerializer(serializers.Serializer): - """Serialize the accepted theme linked to a draft section.""" - - id = serializers.IntegerField() - title = serializers.CharField() - pitch = serializers.CharField() - why_it_matters = serializers.CharField() - - -class NewsletterDraftSectionSerializer(serializers.ModelSerializer): - """Serialize one editable newsletter draft section.""" - - items = NewsletterDraftItemSerializer(many=True, read_only=True) - theme_suggestion_detail = NewsletterDraftSectionThemeSerializer( - source="theme_suggestion", - read_only=True, - allow_null=True, - ) - - class Meta: - model = NewsletterDraftSection - fields = [ - "id", - "draft", - "theme_suggestion", - "theme_suggestion_detail", - "title", - "lede", - "order", - "items", - ] - read_only_fields = [ - "id", - "draft", - "theme_suggestion", - "theme_suggestion_detail", - "items", - ] - - -class NewsletterDraftOriginalIdeaSerializer(serializers.Serializer): - """Serialize the accepted idea linked to a draft original piece.""" - - id = serializers.IntegerField() - angle_title = serializers.CharField() - summary = serializers.CharField() - suggested_outline = serializers.CharField() - - -class NewsletterDraftOriginalPieceSerializer(serializers.ModelSerializer): - """Serialize one editable original-content block in a draft.""" - - idea_detail = NewsletterDraftOriginalIdeaSerializer(source="idea", read_only=True) - - class Meta: - model = NewsletterDraftOriginalPiece - fields = [ - "id", - "draft", - "idea", - "idea_detail", - "title", - "pitch", - "suggested_outline", - "order", - ] - read_only_fields = ["id", "draft", "idea", "idea_detail"] - - -class NewsletterDraftSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize one newsletter draft and its nested editable tree.""" - - sections = NewsletterDraftSectionSerializer(many=True, read_only=True) - original_pieces = NewsletterDraftOriginalPieceSerializer(many=True, read_only=True) - rendered_markdown = serializers.SerializerMethodField() - rendered_html = serializers.SerializerMethodField() - - class Meta: - model = NewsletterDraft - fields = [ - "id", - "project", - "title", - "intro", - "outro", - "target_publish_date", - "status", - "generated_at", - "last_edited_at", - "generation_metadata", - "sections", - "original_pieces", - "rendered_markdown", - "rendered_html", - ] - read_only_fields = [ - "id", - "project", - "generated_at", - "last_edited_at", - "generation_metadata", - "sections", - "original_pieces", - "rendered_markdown", - "rendered_html", - ] - - def get_rendered_markdown(self, obj: NewsletterDraft) -> str: - """Render the draft tree into Markdown for API consumers.""" - - return obj.render_markdown() - - def get_rendered_html(self, obj: NewsletterDraft) -> str: - """Render the draft tree into HTML for API consumers.""" - - return obj.render_html() - - -class NewsletterDraftRegenerateSectionSerializer(serializers.Serializer): - """Validate per-section regeneration requests for an existing draft.""" - - section_id = serializers.IntegerField() - - def validate_section_id(self, value: int) -> int: - """Ensure the selected section belongs to the current project and draft.""" - - project = self.context["project"] - draft = self.context["draft"] - exists = NewsletterDraftSection.objects.filter( - pk=value, - draft=draft, - draft__project=project, - ).exists() - if not exists: - raise serializers.ValidationError( - "Draft section not found for this project." - ) - return value diff --git a/newsletters/tasks.py b/newsletters/tasks.py index badd2e82..df9cc921 100644 --- a/newsletters/tasks.py +++ b/newsletters/tasks.py @@ -1,16 +1,17 @@ -"""Celery tasks and helpers for newsletter intake processing.""" +"""Background tasks and helpers for newsletter intake processing.""" -from functools import lru_cache -from typing import Protocol, cast - -from celery import shared_task -from celery.schedules import crontab_parser -from django.conf import settings from django.db.models import Model, Q from django.utils import timezone from content.deduplication import canonicalize_url from content.models import Content +from core.cron import cron_matches_now +from digest_engine.taskiq import ( + broker, + enqueue_task, + run_task_inline, + task_always_eager, +) from newsletters.composition import ( generate_newsletter_draft as compose_newsletter_draft, ) @@ -31,19 +32,6 @@ from projects.models import Project, ProjectConfig -class DelayedTask(Protocol): - """Protocol for Celery tasks dispatched through ``delay``.""" - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - -def _enqueue_task(task: object, *args: object, **kwargs: object) -> None: - """Dispatch a Celery task through a typed ``delay`` seam.""" - - cast(DelayedTask, task).delay(*args, **kwargs) - - def _require_pk(instance: Model) -> int: """Return a saved model primary key for newsletter intake processing.""" @@ -53,7 +41,7 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -@shared_task(name="core.tasks.process_newsletter_intake") +@broker.task(task_name="core.tasks.process_newsletter_intake") def process_newsletter_intake(intake_id: int): """Convert a stored newsletter email into content rows.""" @@ -135,13 +123,13 @@ def _schedule_content_processing(content: Content) -> None: upsert_content_embedding(content) content_id = _require_pk(content) assign_content_to_topic_cluster(content_id) - if settings.CELERY_TASK_ALWAYS_EAGER: - process_content(content_id) + if task_always_eager(): + run_task_inline(process_content, content_id) else: - _enqueue_task(process_content, content_id) + enqueue_task(process_content, content_id) -@shared_task(name="core.tasks.generate_newsletter_draft") +@broker.task(task_name="core.tasks.generate_newsletter_draft") def generate_newsletter_draft( project_id: int, trigger_source: str = "manual", @@ -184,7 +172,7 @@ def generate_newsletter_draft( return result -@shared_task(name="core.tasks.run_all_scheduled_newsletter_drafts") +@broker.task(task_name="core.tasks.run_all_scheduled_newsletter_drafts") def run_all_scheduled_newsletter_drafts() -> dict[str, int]: """Queue scheduled newsletter drafts for projects whose cron matches now.""" @@ -200,16 +188,18 @@ def run_all_scheduled_newsletter_drafts() -> dict[str, int]: ): checked_count += 1 project_id = int(config.project_id) - if not _cron_matches_now(config.draft_schedule_cron, now=now): + if not cron_matches_now( + config.draft_schedule_cron, now=timezone.localtime(now) + ): skipped_not_due_count += 1 continue if _project_has_scheduled_draft_today(project_id, now=now): skipped_daily_cap_count += 1 continue - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): generate_newsletter_draft(project_id, trigger_source="scheduled") else: - _enqueue_task( + enqueue_task( generate_newsletter_draft, project_id, trigger_source="scheduled", @@ -224,7 +214,7 @@ def run_all_scheduled_newsletter_drafts() -> dict[str, int]: } -@shared_task(name="core.tasks.regenerate_newsletter_draft_section") +@broker.task(task_name="core.tasks.regenerate_newsletter_draft_section") def regenerate_newsletter_draft_section(section_id: int) -> dict[str, object]: """Recompose one newsletter draft section in isolation.""" @@ -274,45 +264,3 @@ def _project_has_scheduled_draft_today(project_id: int, *, now) -> bool: generated_at__date=timezone.localdate(now), generation_metadata__trigger_source="scheduled", ).exists() - - -def _cron_matches_now(cron_expression: str, *, now) -> bool: - """Return whether the current local minute satisfies the cron expression.""" - - try: - minute_set, hour_set, day_set, month_set, weekday_set = _parse_cron_fields( - cron_expression - ) - except ValueError: - return False - - current = timezone.localtime(now) - weekday = current.isoweekday() % 7 - return ( - current.minute in minute_set - and current.hour in hour_set - and current.day in day_set - and current.month in month_set - and weekday in weekday_set - ) - - -@lru_cache(maxsize=128) -def _parse_cron_fields(cron_expression: str) -> tuple[ - set[int], - set[int], - set[int], - set[int], - set[int], -]: - """Parse a normalized 5-part cron expression into comparable field sets.""" - - normalized = " ".join(cron_expression.split()) - minute, hour, day_of_month, month_of_year, day_of_week = normalized.split(" ") - return ( - crontab_parser(60).parse(minute), - crontab_parser(24).parse(hour), - crontab_parser(31, 1).parse(day_of_month), - crontab_parser(12, 1).parse(month_of_year), - crontab_parser(7).parse(day_of_week), - ) diff --git a/newsletters/tests/test_newsletters.py b/newsletters/tests/test_newsletters.py index 6affbd57..1bf072a9 100644 --- a/newsletters/tests/test_newsletters.py +++ b/newsletters/tests/test_newsletters.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from types import SimpleNamespace from typing import Any, cast +from unittest.mock import AsyncMock import pytest from django.core import mail @@ -155,9 +156,12 @@ def test_handle_anymail_inbound_creates_pending_intake_and_sends_confirmation( def test_handle_anymail_inbound_queues_confirmed_sender(settings, mocker, project): - settings.CELERY_TASK_ALWAYS_EAGER = False + settings.TASKIQ_ALWAYS_EAGER = False send_mock = mocker.patch("newsletters.intake.send_confirmation_email") - delay_mock = mocker.patch("newsletters.tasks.process_newsletter_intake.delay") + queue_mock = mocker.patch( + "newsletters.tasks.process_newsletter_intake.kiq", + new_callable=AsyncMock, + ) IntakeAllowlist.objects.create( project=project, sender_email="newsletter@example.com", @@ -178,7 +182,7 @@ def test_handle_anymail_inbound_queues_confirmed_sender(settings, mocker, projec handle_anymail_inbound(sender=object(), event=event, esp_name="Resend") intake = NewsletterIntake.objects.get(message_id="msg-456") - delay_mock.assert_called_once_with(_require_pk(intake)) + queue_mock.assert_awaited_once_with(_require_pk(intake)) send_mock.assert_not_called() @@ -337,7 +341,7 @@ def test_send_confirmation_email_uses_django_mail_backend(settings): def test_confirm_newsletter_sender_confirms_allowlist_and_queues_pending_intakes( client, settings, mocker, project ): - settings.CELERY_TASK_ALWAYS_EAGER = False + settings.TASKIQ_ALWAYS_EAGER = False allowlist = IntakeAllowlist.objects.create( project=project, sender_email="newsletter@example.com" ) @@ -348,7 +352,10 @@ def test_confirm_newsletter_sender_confirms_allowlist_and_queues_pending_intakes raw_text="Visit https://example.com/post", message_id="msg-456", ) - delay_mock = mocker.patch("newsletters.tasks.process_newsletter_intake.delay") + queue_mock = mocker.patch( + "newsletters.tasks.process_newsletter_intake.kiq", + new_callable=AsyncMock, + ) response = client.get( reverse( @@ -359,13 +366,13 @@ def test_confirm_newsletter_sender_confirms_allowlist_and_queues_pending_intakes assert response.status_code == 200 allowlist.refresh_from_db() assert allowlist.confirmed_at is not None - delay_mock.assert_called_once_with(_require_pk(intake)) + queue_mock.assert_awaited_once_with(_require_pk(intake)) def test_process_newsletter_intake_creates_content_for_confirmed_sender( settings, mocker, project ): - settings.CELERY_TASK_ALWAYS_EAGER = False + settings.TASKIQ_ALWAYS_EAGER = False allowlist = IntakeAllowlist.objects.create( project=project, sender_email="newsletter@example.com", @@ -380,7 +387,10 @@ def test_process_newsletter_intake_creates_content_for_confirmed_sender( message_id="msg-789", ) upsert_mock = mocker.patch("core.embeddings.upsert_content_embedding") - delay_mock = mocker.patch("core.tasks.process_content.delay") + queue_mock = mocker.patch( + "core.tasks.process_content.kiq", + new_callable=AsyncMock, + ) from newsletters.tasks import process_newsletter_intake @@ -397,4 +407,4 @@ def test_process_newsletter_intake_creates_content_for_confirmed_sender( source_metadata = cast(dict[str, Any], content.source_metadata) assert source_metadata["newsletter_intake_id"] == _require_pk(intake) upsert_mock.assert_called_once_with(content) - delay_mock.assert_called_once_with(_require_pk(content)) + queue_mock.assert_awaited_once_with(_require_pk(content)) diff --git a/newsletters/tests/test_api.py b/newsletters/tests/test_ninja_api.py similarity index 58% rename from newsletters/tests/test_api.py rename to newsletters/tests/test_ninja_api.py index 25160942..0c90dc30 100644 --- a/newsletters/tests/test_api.py +++ b/newsletters/tests/test_ninja_api.py @@ -1,10 +1,13 @@ +from __future__ import annotations + +from http import HTTPStatus from typing import Any, cast +from unittest.mock import AsyncMock, patch from django.contrib.auth import get_user_model from django.db.models import Model +from django.test import TestCase from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase from newsletters.models import ( IntakeAllowlist, @@ -13,6 +16,8 @@ NewsletterDraftOriginalPiece, NewsletterDraftSection, NewsletterDraftStatus, + NewsletterIntake, + NewsletterIntakeStatus, ) from projects.models import Project, ProjectMembership, ProjectRole from trends.models import ( @@ -32,26 +37,25 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - def _create_user(user_model: type[Any], **kwargs: object): """Create a user through the custom manager with a typed escape hatch.""" return cast(Any, user_model.objects).create_user(**kwargs) -class NewsletterApiTests(APITestCase): - """Exercise newsletter-owned project API endpoints.""" +class NewsletterNinjaApiTests(TestCase): + """Exercise newsletter-owned Ninja API endpoints.""" def setUp(self): user_model = get_user_model() self.owner = _create_user(user_model, username="owner", password="testpass123") + self.reader = _create_user( + user_model, username="reader", password="testpass123" + ) self.other_user = _create_user( - user_model, username="other", password="testpass123" + user_model, + username="other", + password="testpass123", ) self.owner_project = Project.objects.create( name="Owner Project", @@ -66,6 +70,11 @@ def setUp(self): project=self.owner_project, role=ProjectRole.ADMIN, ) + ProjectMembership.objects.create( + user=self.reader, + project=self.owner_project, + role=ProjectRole.READER, + ) ProjectMembership.objects.create( user=self.other_user, project=self.other_project, @@ -75,7 +84,16 @@ def setUp(self): project=self.owner_project, sender_email="sender@example.com", ) - _typed_client(self.client).force_authenticate(self.owner) + self.owner_intake = NewsletterIntake.objects.create( + project=self.owner_project, + sender_email="sender@example.com", + subject="Owner Digest", + raw_text="See https://example.com/post", + message_id="owner-intake-1", + status=NewsletterIntakeStatus.EXTRACTED, + extraction_result={"items": []}, + ) + self.client.force_login(self.owner) def test_intake_allowlist_list_is_scoped_to_request_user_project(self): other_allowlist = IntakeAllowlist.objects.create( @@ -85,12 +103,12 @@ def test_intake_allowlist_list_is_scoped_to_request_user_project(self): response = self.client.get( reverse( - "v1:project-intake-allowlist-list", + "ninja-api:list_intake_allowlist", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(len(response.json()), 1) self.assertEqual( response.json()[0]["id"], _require_pk(self.owner_intake_allowlist) @@ -101,14 +119,14 @@ def test_intake_allowlist_list_is_scoped_to_request_user_project(self): def test_intake_allowlist_create_and_delete_manage_project_senders(self): create_response = self.client.post( reverse( - "v1:project-intake-allowlist-list", + "ninja-api:create_intake_allowlist", kwargs={"project_id": _require_pk(self.owner_project)}, ), {"sender_email": "new-sender@example.com"}, - format="json", + content_type="application/json", ) - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) created_allowlist = IntakeAllowlist.objects.get( project=self.owner_project, sender_email="new-sender@example.com", @@ -116,23 +134,43 @@ def test_intake_allowlist_create_and_delete_manage_project_senders(self): self.assertEqual( create_response.json()["project"], _require_pk(self.owner_project) ) - self.assertFalse(create_response.json()["is_confirmed"]) delete_response = self.client.delete( reverse( - "v1:project-intake-allowlist-detail", + "ninja-api:delete_intake_allowlist", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(created_allowlist), + "allowlist_id": _require_pk(created_allowlist), }, ) ) - self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(delete_response.status_code, HTTPStatus.NO_CONTENT) self.assertFalse( IntakeAllowlist.objects.filter(pk=_require_pk(created_allowlist)).exists() ) + def test_newsletter_intake_list_is_scoped_to_request_project(self): + NewsletterIntake.objects.create( + project=self.other_project, + sender_email="other@example.com", + subject="Other Digest", + raw_text="Other raw text", + message_id="other-intake-1", + status=NewsletterIntakeStatus.PENDING, + ) + + response = self.client.get( + reverse( + "ninja-api:list_newsletter_intakes", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_intake)) + def test_newsletter_draft_list_is_scoped_to_request_project(self): owner_draft = NewsletterDraft.objects.create( project=self.owner_project, @@ -153,23 +191,21 @@ def test_newsletter_draft_list_is_scoped_to_request_project(self): response = self.client.get( reverse( - "v1:project-newsletter-draft-list", + "ninja-api:list_newsletter_drafts", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(len(response.json()), 1) self.assertEqual(response.json()[0]["id"], _require_pk(owner_draft)) self.assertIn("rendered_markdown", response.json()[0]) self.assertIn("rendered_html", response.json()[0]) def test_newsletter_draft_generate_action_runs_immediately_in_eager_mode(self): - with self.settings(CELERY_TASK_ALWAYS_EAGER=True): - from unittest.mock import patch - + with self.settings(TASKIQ_ALWAYS_EAGER=True): with patch( - "newsletters.api.generate_newsletter_draft", + "newsletters.ninja_api.generate_newsletter_draft", return_value={ "project_id": _require_pk(self.owner_project), "draft_id": 42, @@ -180,33 +216,32 @@ def test_newsletter_draft_generate_action_runs_immediately_in_eager_mode(self): ) as generate_mock: response = self.client.post( reverse( - "v1:project-newsletter-draft-generate", + "ninja-api:generate_newsletter_draft_route", kwargs={"project_id": _require_pk(self.owner_project)}, ), - format="json", ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.json()["status"], "completed") self.assertEqual(response.json()["result"]["draft_id"], 42) generate_mock.assert_called_once_with(_require_pk(self.owner_project)) def test_newsletter_draft_generate_action_queues_in_background_mode(self): - with self.settings(CELERY_TASK_ALWAYS_EAGER=False): - from unittest.mock import patch - - with patch("newsletters.api.generate_newsletter_draft.delay") as delay_mock: + with self.settings(TASKIQ_ALWAYS_EAGER=False): + with patch( + "newsletters.ninja_api.generate_newsletter_draft.kiq", + new_callable=AsyncMock, + ) as queue_mock: response = self.client.post( reverse( - "v1:project-newsletter-draft-generate", + "ninja-api:generate_newsletter_draft_route", kwargs={"project_id": _require_pk(self.owner_project)}, ), - format="json", ) - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) self.assertEqual(response.json()["status"], "queued") - delay_mock.assert_called_once_with(_require_pk(self.owner_project)) + queue_mock.assert_awaited_once_with(_require_pk(self.owner_project)) def test_newsletter_draft_regenerate_section_runs_immediately_in_eager_mode(self): draft = NewsletterDraft.objects.create( @@ -235,11 +270,9 @@ def test_newsletter_draft_regenerate_section_runs_immediately_in_eager_mode(self order=0, ) - with self.settings(CELERY_TASK_ALWAYS_EAGER=True): - from unittest.mock import patch - + with self.settings(TASKIQ_ALWAYS_EAGER=True): with patch( - "newsletters.api.regenerate_newsletter_draft_section", + "newsletters.ninja_api.regenerate_newsletter_draft_section", return_value={ "project_id": _require_pk(self.owner_project), "draft_id": _require_pk(draft), @@ -249,20 +282,123 @@ def test_newsletter_draft_regenerate_section_runs_immediately_in_eager_mode(self ) as regenerate_mock: response = self.client.post( reverse( - "v1:project-newsletter-draft-regenerate-section", + "ninja-api:regenerate_newsletter_draft_section_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(draft), + "draft_id": _require_pk(draft), }, ), {"section_id": _require_pk(section)}, - format="json", + content_type="application/json", ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) regenerate_mock.assert_called_once_with(_require_pk(section)) self.assertEqual(response.json()["id"], _require_pk(draft)) + def test_newsletter_draft_regenerate_section_queues_in_background_mode(self): + draft = NewsletterDraft.objects.create( + project=self.owner_project, + title="Draft", + intro="Intro", + outro="Outro", + status=NewsletterDraftStatus.READY, + generation_metadata={"source_theme_ids": [], "source_idea_ids": []}, + ) + theme = ThemeSuggestion.objects.create( + project=self.owner_project, + title="Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="", + velocity_at_creation=1.0, + novelty_score=0.7, + status=ThemeSuggestionStatus.ACCEPTED, + ) + section = NewsletterDraftSection.objects.create( + draft=draft, + theme_suggestion=theme, + title="Before", + lede="Before lede", + order=0, + ) + + with self.settings(TASKIQ_ALWAYS_EAGER=False): + with patch( + "newsletters.ninja_api.regenerate_newsletter_draft_section.kiq", + new_callable=AsyncMock, + ) as queue_mock: + response = self.client.post( + reverse( + "ninja-api:regenerate_newsletter_draft_section_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "draft_id": _require_pk(draft), + }, + ), + {"section_id": _require_pk(section)}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) + self.assertEqual(response.json()["status"], "queued") + self.assertEqual(response.json()["draft_id"], _require_pk(draft)) + self.assertEqual(response.json()["section_id"], _require_pk(section)) + queue_mock.assert_awaited_once_with(_require_pk(section)) + + def test_newsletter_draft_regenerate_section_rejects_section_from_other_draft(self): + draft = NewsletterDraft.objects.create( + project=self.owner_project, + title="Draft", + intro="Intro", + outro="Outro", + status=NewsletterDraftStatus.READY, + generation_metadata={"source_theme_ids": [], "source_idea_ids": []}, + ) + other_draft = NewsletterDraft.objects.create( + project=self.owner_project, + title="Other draft", + intro="Intro", + outro="Outro", + status=NewsletterDraftStatus.READY, + generation_metadata={"source_theme_ids": [], "source_idea_ids": []}, + ) + theme = ThemeSuggestion.objects.create( + project=self.owner_project, + title="Theme", + pitch="Pitch", + why_it_matters="Why", + suggested_angle="", + velocity_at_creation=1.0, + novelty_score=0.7, + status=ThemeSuggestionStatus.ACCEPTED, + ) + other_section = NewsletterDraftSection.objects.create( + draft=other_draft, + theme_suggestion=theme, + title="Other section", + lede="Other lede", + order=0, + ) + + response = self.client.post( + reverse( + "ninja-api:regenerate_newsletter_draft_section_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "draft_id": _require_pk(draft), + }, + ), + {"section_id": _require_pk(other_section)}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["section_id"][0], + "Draft section not found for this project.", + ) + def test_newsletter_draft_item_patch_marks_draft_as_edited(self): draft = NewsletterDraft.objects.create( project=self.owner_project, @@ -307,19 +443,19 @@ def test_newsletter_draft_item_patch_marks_draft_as_edited(self): response = self.client.patch( reverse( - "v1:project-newsletter-draft-item-detail", + "ninja-api:update_newsletter_draft_item", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(item), + "item_id": _require_pk(item), }, ), {"summary_used": "Updated summary"}, - format="json", + content_type="application/json", ) draft.refresh_from_db() item.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(item.summary_used, "Updated summary") self.assertEqual(draft.status, NewsletterDraftStatus.EDITED) self.assertIsNotNone(draft.last_edited_at) @@ -354,16 +490,16 @@ def test_newsletter_draft_original_piece_delete_marks_draft_as_edited(self): response = self.client.delete( reverse( - "v1:project-newsletter-draft-original-piece-detail", + "ninja-api:delete_newsletter_draft_original_piece", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(original_piece), + "original_piece_id": _require_pk(original_piece), }, ) ) draft.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) self.assertEqual(draft.status, NewsletterDraftStatus.EDITED) self.assertFalse( NewsletterDraftOriginalPiece.objects.filter( diff --git a/newsletters/tests/test_tasks.py b/newsletters/tests/test_tasks.py index b7f3f952..acb6aa94 100644 --- a/newsletters/tests/test_tasks.py +++ b/newsletters/tests/test_tasks.py @@ -1,5 +1,6 @@ from datetime import timedelta from typing import cast +from unittest.mock import AsyncMock import pytest from django.contrib.auth import get_user_model @@ -422,7 +423,7 @@ def test_task_regenerate_newsletter_draft_section_notifies_project_admins_on_fai def test_run_all_scheduled_newsletter_drafts_executes_due_projects_inline( settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True fixed_now = timezone.now().replace(hour=9, minute=17, second=0, microsecond=0) due_expression = f"{fixed_now.minute} {fixed_now.hour} * * *" due_project = Project.objects.create( @@ -461,7 +462,7 @@ def test_run_all_scheduled_newsletter_drafts_executes_due_projects_inline( def test_run_all_scheduled_newsletter_drafts_enqueues_due_projects_when_not_eager( settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = False + settings.TASKIQ_ALWAYS_EAGER = False fixed_now = timezone.now().replace(hour=9, minute=17, second=0, microsecond=0) due_expression = f"{fixed_now.minute} {fixed_now.hour} * * *" due_project = Project.objects.create( @@ -472,7 +473,10 @@ def test_run_all_scheduled_newsletter_drafts_enqueues_due_projects_when_not_eage project=due_project, draft_schedule_cron=due_expression, ) - delay_mock = mocker.patch("newsletters.tasks.generate_newsletter_draft.delay") + queue_mock = mocker.patch( + "newsletters.tasks.generate_newsletter_draft.kiq", + new_callable=AsyncMock, + ) mocker.patch("newsletters.tasks.timezone.now", return_value=fixed_now) result = run_all_scheduled_newsletter_drafts() @@ -483,14 +487,14 @@ def test_run_all_scheduled_newsletter_drafts_enqueues_due_projects_when_not_eage "skipped_not_due": 0, "skipped_daily_cap": 0, } - delay_mock.assert_called_once_with( + queue_mock.assert_awaited_once_with( _require_pk(due_project), trigger_source="scheduled", ) def test_run_all_scheduled_newsletter_drafts_skips_daily_cap(settings, mocker): - settings.CELERY_TASK_ALWAYS_EAGER = False + settings.TASKIQ_ALWAYS_EAGER = False fixed_now = timezone.now().replace(hour=9, minute=17, second=0, microsecond=0) due_expression = f"{fixed_now.minute} {fixed_now.hour} * * *" capped_project = Project.objects.create( @@ -513,7 +517,10 @@ def test_run_all_scheduled_newsletter_drafts_skips_daily_cap(settings, mocker): "trigger_source": "scheduled", }, ) - delay_mock = mocker.patch("newsletters.tasks.generate_newsletter_draft.delay") + queue_mock = mocker.patch( + "newsletters.tasks.generate_newsletter_draft.kiq", + new_callable=AsyncMock, + ) mocker.patch("newsletters.tasks.timezone.now", return_value=fixed_now) result = run_all_scheduled_newsletter_drafts() @@ -524,4 +531,4 @@ def test_run_all_scheduled_newsletter_drafts_skips_daily_cap(settings, mocker): "skipped_not_due": 0, "skipped_daily_cap": 1, } - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() diff --git a/node_modules/.bin/turbo b/node_modules/.bin/turbo index a22af52a..f0d84e90 100755 --- a/node_modules/.bin/turbo +++ b/node_modules/.bin/turbo @@ -10,9 +10,9 @@ case `uname` in esac if [ -z "$NODE_PATH" ]; then - export NODE_PATH="/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.14/node_modules/turbo/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.14/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/node_modules" + export NODE_PATH="/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.16/node_modules/turbo/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.16/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/node_modules" else - export NODE_PATH="/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.14/node_modules/turbo/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.14/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/node_modules:$NODE_PATH" + export NODE_PATH="/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.16/node_modules/turbo/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/turbo@2.9.16/node_modules:/home/kevin/Repos/digest-engine/node_modules/.pnpm/node_modules:$NODE_PATH" fi if [ -x "$basedir/node" ]; then exec "$basedir/node" "$basedir/../turbo/bin/turbo" "$@" diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json deleted file mode 100644 index 57ac142b..00000000 --- a/node_modules/.package-lock.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "digest-engine", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "node_modules/@turbo/linux-64": { - "version": "2.9.12", - "resolved": "https://registry.npmjs.org/@turbo/linux-64/-/linux-64-2.9.12.tgz", - "integrity": "sha512-InIUtH7cw/vqXNX1Gr7QgWfmw3ct08pV5CpfdEOR48z2u2rzdmpIuk00B/Q2xCb0PMWtKgiMQynfuphmEuUyTQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/turbo": { - "version": "2.9.12", - "resolved": "https://registry.npmjs.org/turbo/-/turbo-2.9.12.tgz", - "integrity": "sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw==", - "dev": true, - "license": "MIT", - "bin": { - "turbo": "bin/turbo" - }, - "optionalDependencies": { - "@turbo/darwin-64": "2.9.12", - "@turbo/darwin-arm64": "2.9.12", - "@turbo/linux-64": "2.9.12", - "@turbo/linux-arm64": "2.9.12", - "@turbo/windows-64": "2.9.12", - "@turbo/windows-arm64": "2.9.12" - } - } - } -} diff --git a/notifications/api.py b/notifications/api.py deleted file mode 100644 index 019c3edc..00000000 --- a/notifications/api.py +++ /dev/null @@ -1,100 +0,0 @@ -"""User-scoped REST endpoints for persistent notifications.""" - -from __future__ import annotations - -from django.utils import timezone -from drf_spectacular.utils import ( - OpenApiResponse, - extend_schema, - extend_schema_view, - inline_serializer, -) -from rest_framework import mixins, serializers, status, viewsets -from rest_framework.decorators import action -from rest_framework.response import Response - -from core.api import AUTHENTICATION_REQUIRED_RESPONSE -from notifications.models import Notification -from notifications.serializers import NotificationSerializer - -READ_ALL_RESPONSE = inline_serializer( - name="NotificationsReadAllResponse", - fields={ - "updated_count": serializers.IntegerField(), - }, -) - - -@extend_schema_view( - list=extend_schema( - summary="List notifications", - description="Return the current user's persistent notifications, newest first.", - tags=["Notifications"], - responses={ - 200: NotificationSerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ), - destroy=extend_schema( - summary="Delete notification", - description="Delete one notification from the current user's inbox.", - tags=["Notifications"], - responses={ - 204: OpenApiResponse(description="Notification deleted."), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ), -) -class NotificationViewSet( - mixins.ListModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet -): - """List and manage the authenticated user's notifications.""" - - serializer_class = NotificationSerializer - http_method_names = ["get", "post", "delete", "head", "options"] - queryset = Notification.objects.select_related("project") - - def get_queryset(self): - """Scope notifications to the authenticated user and optional unread filter.""" - - queryset = self.queryset.filter(user=self.request.user) - unread_value = self.request.query_params.get("unread", "") - if unread_value.lower() in {"1", "true", "yes", "on"}: - queryset = queryset.filter(read_at__isnull=True) - return queryset - - @extend_schema( - summary="Mark notification as read", - description="Set `read_at` for one notification owned by the current user.", - tags=["Notifications"], - request=None, - responses={200: NotificationSerializer, 403: AUTHENTICATION_REQUIRED_RESPONSE}, - ) - @action(detail=True, methods=["post"], url_path="read") - def read(self, request, *args, **kwargs): - """Mark one notification as read and return the updated payload.""" - - notification = self.get_object() - if notification.read_at is None: - notification.read_at = timezone.now() - notification.save(update_fields=["read_at"]) - serializer = self.get_serializer(notification) - return Response(serializer.data) - - @extend_schema( - summary="Mark all notifications as read", - description="Set `read_at` for every unread notification owned by the current user.", - tags=["Notifications"], - request=None, - responses={200: READ_ALL_RESPONSE, 403: AUTHENTICATION_REQUIRED_RESPONSE}, - ) - @action(detail=False, methods=["post"], url_path="read-all") - def read_all(self, request, *args, **kwargs): - """Mark every unread notification as read for the current user.""" - - updated_count = ( - self.get_queryset() - .filter(read_at__isnull=True) - .update(read_at=timezone.now()) - ) - return Response({"updated_count": updated_count}, status=status.HTTP_200_OK) diff --git a/notifications/api_urls.py b/notifications/api_urls.py deleted file mode 100644 index 7862692a..00000000 --- a/notifications/api_urls.py +++ /dev/null @@ -1,11 +0,0 @@ -"""API route registration for top-level notification resources.""" - -from rest_framework.routers import DefaultRouter - -from notifications.api import NotificationViewSet - - -def register_root_routes(router: DefaultRouter) -> None: - """Register top-level current-user notification endpoints.""" - - router.register("notifications", NotificationViewSet, basename="notification") diff --git a/notifications/ninja_api.py b/notifications/ninja_api.py new file mode 100644 index 00000000..edd445a1 --- /dev/null +++ b/notifications/ninja_api.py @@ -0,0 +1,129 @@ +"""Django Ninja endpoints for the notifications API.""" + +from __future__ import annotations + +from typing import Any + +from django.shortcuts import get_object_or_404 +from django.utils import timezone +from ninja import Query, Router, Schema +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from notifications.models import Notification + +router = Router(tags=["Notifications"]) + + +class NotificationSchema(Schema): + """Serialized notification payload.""" + + id: int + project: int | None = None + level: str + body: str + link_path: str + metadata: dict[str, Any] + created_at: str + read_at: str | None = None + is_read: bool + + +class NotificationListFilters(Schema): + """Supported filters for listing notifications.""" + + unread: str | None = None + + +class NotificationsReadAllResponseSchema(Schema): + """Response body for the read-all action.""" + + updated_count: int + + +def _serialize_notification(notification: Notification) -> dict[str, Any]: + """Return one serialized notification payload.""" + + return { + "id": int(notification.pk), + "project": notification.project_id, + "level": notification.level, + "body": notification.body, + "link_path": notification.link_path, + "metadata": notification.metadata, + "created_at": notification.created_at.isoformat(), + "read_at": notification.read_at.isoformat() if notification.read_at else None, + "is_read": notification.is_read, + } + + +def _serialize_notifications(notifications: list[Notification]) -> list[dict[str, Any]]: + """Return serialized notification payloads for a list response.""" + + return [_serialize_notification(notification) for notification in notifications] + + +def _notifications_queryset(request) -> Any: + """Return the current user's notification queryset.""" + + return Notification.objects.select_related("project").filter(user=request.user) + + +@router.get("/notifications/", response=list[NotificationSchema], auth=api_authenticate) +def list_notifications(request, filters: Query[NotificationListFilters]): + """Return the current user's notifications, newest first.""" + + queryset = _notifications_queryset(request) + unread_value = (filters.unread or "").lower() + if unread_value in {"1", "true", "yes", "on"}: + queryset = queryset.filter(read_at__isnull=True) + return _serialize_notifications(list(queryset)) + + +@router.post( + "/notifications/read-all/", + response=NotificationsReadAllResponseSchema, + auth=api_authenticate, +) +def read_all_notifications(request): + """Mark every unread notification as read for the current user.""" + + updated_count = ( + _notifications_queryset(request) + .filter(read_at__isnull=True) + .update(read_at=timezone.now()) + ) + return {"updated_count": updated_count} + + +@router.delete( + "/notifications/{notification_id}/", auth=api_authenticate, response={204: None} +) +def delete_notification(request, notification_id: int): + """Delete one notification from the current user's inbox.""" + + notification = get_object_or_404( + _notifications_queryset(request), pk=notification_id + ) + notification.delete() + return Status(204, None) + + +@router.post( + "/notifications/{notification_id}/read/", + response=NotificationSchema, + auth=api_authenticate, +) +def read_notification(request, notification_id: int): + """Mark one notification as read and return the updated payload.""" + + notification = get_object_or_404( + _notifications_queryset(request), pk=notification_id + ) + if notification.read_at is None: + notification.read_at = timezone.now() + notification.save(update_fields=["read_at"]) + return _serialize_notification(notification) + + +__all__ = ["router"] diff --git a/notifications/realtime.py b/notifications/realtime.py index 5d933de7..d9e9ac90 100644 --- a/notifications/realtime.py +++ b/notifications/realtime.py @@ -1,7 +1,6 @@ """Realtime notification helpers shared by signals and consumers.""" from notifications.models import Notification -from notifications.serializers import NotificationSerializer def notification_group_name(user_id: int) -> str: @@ -13,4 +12,14 @@ def notification_group_name(user_id: int) -> str: def serialize_notification(notification: Notification) -> dict[str, object]: """Return the websocket payload for a persisted notification row.""" - return NotificationSerializer(notification).data + return { + "id": int(notification.pk), + "project": notification.project_id, + "level": notification.level, + "body": notification.body, + "link_path": notification.link_path, + "metadata": notification.metadata, + "created_at": notification.created_at, + "read_at": notification.read_at, + "is_read": notification.is_read, + } diff --git a/notifications/serializers.py b/notifications/serializers.py deleted file mode 100644 index aef86f79..00000000 --- a/notifications/serializers.py +++ /dev/null @@ -1,31 +0,0 @@ -"""DRF serializers for persistent user notifications.""" - -from rest_framework import serializers - -from notifications.models import Notification - - -class NotificationSerializer(serializers.ModelSerializer): - """Serialize notification rows for the current authenticated user.""" - - is_read = serializers.SerializerMethodField() - - class Meta: - model = Notification - fields = [ - "id", - "project", - "level", - "body", - "link_path", - "metadata", - "created_at", - "read_at", - "is_read", - ] - read_only_fields = fields - - def get_is_read(self, obj: Notification) -> bool: - """Return whether the serialized notification has been read.""" - - return obj.is_read diff --git a/notifications/tests/test_api.py b/notifications/tests/test_api.py deleted file mode 100644 index b4f4638b..00000000 --- a/notifications/tests/test_api.py +++ /dev/null @@ -1,126 +0,0 @@ -"""API tests for the notifications app.""" - -from __future__ import annotations - -from typing import Any, cast - -from django.contrib.auth import get_user_model -from django.db.models import Model -from django.urls import reverse -from django.utils import timezone -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from notifications.models import Notification, NotificationLevel -from projects.models import Project - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed API test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create a user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -class NotificationApiTests(APITestCase): - """Exercise the current-user notification endpoints.""" - - def setUp(self): - user_model = get_user_model() - self.owner = _create_user(user_model, username="owner", password="testpass123") - self.other_user = _create_user( - user_model, - username="other", - password="testpass123", - ) - self.project = Project.objects.create( - name="Owner Project", - topic_description="Platform engineering", - ) - self.read_notification = Notification.objects.create( - user=self.owner, - project=self.project, - level=NotificationLevel.INFO, - body="Already read", - read_at=timezone.now(), - ) - self.unread_notification = Notification.objects.create( - user=self.owner, - project=self.project, - level=NotificationLevel.ERROR, - body="Needs attention", - ) - Notification.objects.create( - user=self.other_user, - level=NotificationLevel.SUCCESS, - body="Other user notification", - ) - _typed_client(self.client).force_authenticate(self.owner) - - def test_list_returns_only_current_user_notifications(self): - response = self.client.get(reverse("v1:notification-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - payload = response.json() - self.assertEqual(len(payload), 2) - self.assertEqual(payload[0]["body"], "Needs attention") - self.assertEqual(payload[1]["body"], "Already read") - - def test_list_can_filter_to_unread_notifications(self): - response = self.client.get(reverse("v1:notification-list"), {"unread": "true"}) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - payload = response.json() - self.assertEqual(len(payload), 1) - self.assertEqual(payload[0]["body"], "Needs attention") - self.assertFalse(payload[0]["is_read"]) - - def test_read_action_sets_read_at(self): - response = self.client.post( - reverse( - "v1:notification-read", - kwargs={"pk": _require_pk(self.unread_notification)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.unread_notification.refresh_from_db() - self.assertIsNotNone(self.unread_notification.read_at) - self.assertTrue(response.json()["is_read"]) - - def test_read_all_marks_only_current_users_unread_rows(self): - response = self.client.post(reverse("v1:notification-read-all")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["updated_count"], 1) - self.unread_notification.refresh_from_db() - self.assertIsNotNone(self.unread_notification.read_at) - - def test_destroy_deletes_owned_notification(self): - response = self.client.delete( - reverse( - "v1:notification-detail", - kwargs={"pk": _require_pk(self.unread_notification)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse( - Notification.objects.filter( - pk=_require_pk(self.unread_notification) - ).exists() - ) diff --git a/notifications/tests/test_ninja_api.py b/notifications/tests/test_ninja_api.py new file mode 100644 index 00000000..8dcec015 --- /dev/null +++ b/notifications/tests/test_ninja_api.py @@ -0,0 +1,165 @@ +"""Tests for the Django Ninja notifications API surface.""" + +from __future__ import annotations + +from http import HTTPStatus +from typing import Any, cast + +import pytest +from django.contrib.auth import get_user_model +from django.test import Client +from django.utils import timezone + +from notifications.models import Notification, NotificationLevel +from projects.models import Project + +pytestmark = pytest.mark.django_db + + +def _response(value: object) -> Any: + """Return a typed response object for assertions.""" + + return cast(Any, value) + + +def test_ninja_notification_list_returns_only_current_user_notifications(): + user_model = get_user_model() + owner = cast(Any, user_model.objects).create_user( + username="owner", password="testpass123" + ) + other_user = cast(Any, user_model.objects).create_user( + username="other", password="testpass123" + ) + project = Project.objects.create( + name="Owner Project", topic_description="Platform engineering" + ) + Notification.objects.create( + user=owner, + project=project, + level=NotificationLevel.INFO, + body="Already read", + read_at=timezone.now(), + ) + Notification.objects.create( + user=owner, + project=project, + level=NotificationLevel.ERROR, + body="Needs attention", + ) + Notification.objects.create( + user=other_user, + level=NotificationLevel.SUCCESS, + body="Other user notification", + ) + + client = Client() + client.force_login(owner) + response = _response(client.get("/api/v1/notifications/")) + + assert response.status_code == HTTPStatus.OK + payload = response.json() + assert len(payload) == 2 + assert payload[0]["body"] == "Needs attention" + assert payload[1]["body"] == "Already read" + + +def test_ninja_notification_list_can_filter_to_unread_notifications(): + user_model = get_user_model() + owner = cast(Any, user_model.objects).create_user( + username="owner2", password="testpass123" + ) + project = Project.objects.create( + name="Unread Project", topic_description="Platform engineering" + ) + Notification.objects.create( + user=owner, + project=project, + level=NotificationLevel.INFO, + body="Already read", + read_at=timezone.now(), + ) + Notification.objects.create( + user=owner, + project=project, + level=NotificationLevel.ERROR, + body="Needs attention", + ) + + client = Client() + client.force_login(owner) + response = _response(client.get("/api/v1/notifications/", {"unread": "true"})) + + assert response.status_code == HTTPStatus.OK + payload = response.json() + assert len(payload) == 1 + assert payload[0]["body"] == "Needs attention" + assert payload[0]["is_read"] is False + + +def test_ninja_notification_read_action_sets_read_at(): + user_model = get_user_model() + owner = cast(Any, user_model.objects).create_user( + username="owner3", password="testpass123" + ) + notification = Notification.objects.create( + user=owner, + level=NotificationLevel.ERROR, + body="Needs attention", + ) + + client = Client() + client.force_login(owner) + response = _response(client.post(f"/api/v1/notifications/{notification.pk}/read/")) + + assert response.status_code == HTTPStatus.OK + notification.refresh_from_db() + assert notification.read_at is not None + assert response.json()["is_read"] is True + + +def test_ninja_notification_read_all_marks_only_current_users_unread_rows(): + user_model = get_user_model() + owner = cast(Any, user_model.objects).create_user( + username="owner4", password="testpass123" + ) + other_user = cast(Any, user_model.objects).create_user( + username="other4", password="testpass123" + ) + unread_notification = Notification.objects.create( + user=owner, + level=NotificationLevel.ERROR, + body="Needs attention", + ) + Notification.objects.create( + user=other_user, + level=NotificationLevel.SUCCESS, + body="Other user notification", + ) + + client = Client() + client.force_login(owner) + response = _response(client.post("/api/v1/notifications/read-all/")) + + assert response.status_code == HTTPStatus.OK + assert response.json()["updated_count"] == 1 + unread_notification.refresh_from_db() + assert unread_notification.read_at is not None + + +def test_ninja_notification_destroy_deletes_owned_notification(): + user_model = get_user_model() + owner = cast(Any, user_model.objects).create_user( + username="owner5", password="testpass123" + ) + notification = Notification.objects.create( + user=owner, + level=NotificationLevel.ERROR, + body="Needs attention", + ) + + client = Client() + client.force_login(owner) + response = _response(client.delete(f"/api/v1/notifications/{notification.pk}/")) + + assert response.status_code == HTTPStatus.NO_CONTENT + assert Notification.objects.filter(pk=notification.pk).exists() is False diff --git a/package.json b/package.json index 601e9987..6c14dac7 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "@tailwindcss/typography": "^0.5.19", "stylelint": "^17.11.0", "stylelint-config-standard": "^40.0.0", - "turbo": "^2.9.14" + "turbo": "^2.9.16" } } diff --git a/pants.toml b/pants.toml index 73bb6f10..f94f95da 100644 --- a/pants.toml +++ b/pants.toml @@ -38,8 +38,6 @@ version = "0.13.0" [test] extra_env_vars = [ "ALLOWED_HOSTS", - "CELERY_BROKER_URL", - "CELERY_RESULT_BACKEND", "DATABASE_URL", "NEWSLETTER_API_INTERNAL_URL", "NEWSLETTER_PUBLIC_URL", @@ -50,4 +48,7 @@ extra_env_vars = [ "REDDIT_CLIENT_ID", "REDDIT_CLIENT_SECRET", "REDDIT_USER_AGENT", + "RABBITMQ_URL", + "TASKIQ_ALWAYS_EAGER", + "TASKIQ_RESULT_BACKEND_URL", ] diff --git a/pipeline/admin.py b/pipeline/admin.py index 6f267727..277272c6 100644 --- a/pipeline/admin.py +++ b/pipeline/admin.py @@ -11,6 +11,7 @@ from unfold.admin import ModelAdmin from core.tasks import retry_pipeline_review_item +from digest_engine.taskiq import enqueue_task from pipeline.models import ReviewQueue, ReviewResolution, SkillResult @@ -257,7 +258,7 @@ def retry_selected_items(self, request, queryset): """Retry selected review items from their recorded failed node.""" for review_item in queryset: - retry_pipeline_review_item.delay(review_item.pk) + enqueue_task(retry_pipeline_review_item, review_item.pk) self.message_user(request, "Selected items queued for retry.", messages.SUCCESS) def changelist_view(self, request, extra_context=None): diff --git a/pipeline/api.py b/pipeline/api.py deleted file mode 100644 index daddd56b..00000000 --- a/pipeline/api.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Pipeline-domain API viewsets kept under the existing nested project routes.""" - -from django.conf import settings -from django.utils import timezone -from rest_framework import status, viewsets -from rest_framework.decorators import action -from rest_framework.permissions import BasePermission -from rest_framework.response import Response - -from core.api import ( - SKILL_RESULT_RESPONSE_EXAMPLE, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import ( - IsProjectContributor, - IsProjectMember, - IsProjectMemberWritable, -) -from core.tasks import retry_pipeline_review_item -from pipeline.models import ReviewQueue, ReviewResolution, SkillResult -from pipeline.serializers import ReviewQueueSerializer, SkillResultSerializer - - -@document_project_owned_viewset( - resource_plural="skill results", - resource_singular="skill result", - create_description="Create a new skill result for project content. The referenced content must belong to the selected project.", - tag="AI Processing", - action_overrides=build_crud_action_overrides( - SkillResultSerializer, - resource_plural="skill results for the selected project", - resource_singular="skill result", - retrieve_examples=[SKILL_RESULT_RESPONSE_EXAMPLE], - ), -) -class SkillResultViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Inspect persisted AI skill outputs for project content.""" - - serializer_class = SkillResultSerializer - queryset = SkillResult.objects.select_related("content", "project", "superseded_by") - - def get_permissions(self): - """Allow all members to read skill results and contributors to modify them.""" - - permission_classes: list[type[BasePermission]] - if self.action in {"create", "update", "partial_update", "destroy"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - -@document_project_owned_viewset( - resource_plural="review queue entries", - resource_singular="review queue entry", - create_description="Create a new review queue entry for the selected project. The referenced content must belong to the same project.", - tag="Review Queue", - action_overrides=build_crud_action_overrides( - ReviewQueueSerializer, - resource_plural="review queue entries for the selected project", - resource_singular="review queue entry", - ), -) -class ReviewQueueViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Inspect and manage content awaiting manual review.""" - - serializer_class = ReviewQueueSerializer - queryset = ReviewQueue.objects.select_related("content", "project") - - def get_permissions(self): - """Restrict review-queue access to project contributors.""" - - return [IsProjectContributor()] - - @action(detail=True, methods=["post"]) - def retry(self, request, *args, **kwargs): - """Retry the selected review item from its failed node.""" - - review_item = self.get_object() - if settings.CELERY_TASK_ALWAYS_EAGER: - payload = retry_pipeline_review_item(review_item.pk) - return Response(payload, status=status.HTTP_200_OK) - retry_pipeline_review_item.delay(review_item.pk) - return Response( - { - "review_item_id": review_item.pk, - "status": "queued", - }, - status=status.HTTP_202_ACCEPTED, - ) - - @action(detail=True, methods=["post"]) - def resolve(self, request, *args, **kwargs): - """Resolve the selected review item without retrying it.""" - - review_item = self.get_object() - review_item.resolved = True - review_item.resolution = ReviewResolution.MANUALLY_RESOLVED - review_item.resolved_at = timezone.now() - review_item.save(update_fields=["resolved", "resolution", "resolved_at"]) - serializer = self.get_serializer(review_item) - return Response(serializer.data, status=status.HTTP_200_OK) - - @action(detail=True, methods=["post"]) - def archive(self, request, *args, **kwargs): - """Archive the selected review item.""" - - review_item = self.get_object() - review_item.resolved = True - review_item.resolution = ReviewResolution.ARCHIVED - review_item.resolved_at = timezone.now() - review_item.save(update_fields=["resolved", "resolution", "resolved_at"]) - serializer = self.get_serializer(review_item) - return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/pipeline/api_urls.py b/pipeline/api_urls.py deleted file mode 100644 index 8048e5ec..00000000 --- a/pipeline/api_urls.py +++ /dev/null @@ -1,16 +0,0 @@ -"""API route registration for pipeline resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from pipeline.api import ReviewQueueViewSet, SkillResultViewSet - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested skill-result and review endpoints.""" - - project_router.register( - r"skill-results", SkillResultViewSet, basename="project-skill-result" - ) - project_router.register( - r"review-queue", ReviewQueueViewSet, basename="project-review-queue" - ) diff --git a/pipeline/ninja_api.py b/pipeline/ninja_api.py new file mode 100644 index 00000000..603d1636 --- /dev/null +++ b/pipeline/ninja_api.py @@ -0,0 +1,632 @@ +"""Django Ninja endpoints for project-scoped pipeline resources.""" + +from __future__ import annotations + +import datetime +from typing import Any + +from django.utils import timezone +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from content.models import Content +from core.ninja_api import api_authenticate +from core.tasks import retry_pipeline_review_item +from digest_engine.taskiq import enqueue_task, task_always_eager +from pipeline.models import ( + ReviewQueue, + ReviewResolution, + SkillResult, +) +from projects.models import Project, ProjectMembership, ProjectRole +from projects.ninja_helpers import _get_project_or_404 + +router = Router(tags=["AI Processing"]) + + +class SkillResultSchema(Schema): + """Serialized pipeline skill result payload.""" + + id: int + content: int + project: int + skill_name: str + status: str + result_data: dict[str, Any] | list[Any] | None = None + error_message: str + model_used: str + latency_ms: int | None = None + confidence: float | None = None + invocation_id: str + created_at: datetime.datetime + superseded_by: int | None = None + + +class SkillResultCreateInput(Schema): + """Writable fields for creating one skill result.""" + + content: int + skill_name: str + status: str + result_data: dict[str, Any] | list[Any] | None = None + error_message: str = "" + model_used: str = "" + latency_ms: int | None = None + confidence: float | None = None + superseded_by: int | None = None + + +class SkillResultUpdateInput(Schema): + """Writable fields for partially updating one skill result.""" + + content: int | None = None + skill_name: str | None = None + status: str | None = None + result_data: dict[str, Any] | list[Any] | None = None + error_message: str | None = None + model_used: str | None = None + latency_ms: int | None = None + confidence: float | None = None + superseded_by: int | None = None + + +class ReviewQueueItemSchema(Schema): + """Serialized pipeline review-queue payload.""" + + id: int + project: int + content: int + reason: str + confidence: float + failed_node: str + failure_detail: str + skill_invocation_id: str | None = None + created_at: datetime.datetime + resolved: bool + resolved_at: datetime.datetime | None = None + resolution: str + + +class ReviewQueueCreateInput(Schema): + """Writable fields for creating one review-queue item.""" + + content: int + reason: str + confidence: float + failed_node: str = "" + failure_detail: str = "" + skill_invocation_id: str | None = None + resolved: bool = False + resolved_at: datetime.datetime | None = None + resolution: str = "" + + +class ReviewQueueUpdateInput(Schema): + """Writable fields for partially updating one review-queue item.""" + + content: int | None = None + reason: str | None = None + confidence: float | None = None + failed_node: str | None = None + failure_detail: str | None = None + skill_invocation_id: str | None = None + resolved: bool | None = None + resolved_at: datetime.datetime | None = None + resolution: str | None = None + + +class ReviewQueueRetryQueuedSchema(Schema): + """Queued retry response for one review item.""" + + review_item_id: int + status: str + + +def _require_pk(instance: Any) -> int: + """Return a saved model primary key as an integer.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _require_project_member(request: Any, project_id: int) -> Project: + """Load one project if the current user can read project-scoped pipeline data.""" + + return _get_project_or_404(request, project_id) + + +def _require_project_contributor(request: Any, project_id: int) -> Project: + """Load one project if the current user can mutate project-scoped pipeline data.""" + + project = _get_project_or_404(request, project_id) + membership = ProjectMembership.objects.filter( + project=project, + user=request.user, + ).first() + if not membership or membership.role not in {ProjectRole.ADMIN, ProjectRole.MEMBER}: + raise HttpError(403, "You do not have permission to perform this action.") + return project + + +def _get_skill_result_or_404(project_id: int, skill_result_id: int) -> SkillResult: + """Return one project-scoped skill result or raise 404.""" + + skill_result = ( + SkillResult.objects.select_related("content", "project", "superseded_by") + .filter(project_id=project_id, pk=skill_result_id) + .first() + ) + if skill_result is None: + raise HttpError(404, "Not found.") + return skill_result + + +def _get_review_item_or_404(project_id: int, review_item_id: int) -> ReviewQueue: + """Return one project-scoped review item or raise 404.""" + + review_item = ( + ReviewQueue.objects.select_related("content", "project") + .filter(project_id=project_id, pk=review_item_id) + .first() + ) + if review_item is None: + raise HttpError(404, "Not found.") + return review_item + + +def _serialize_skill_result(skill_result: SkillResult) -> dict[str, Any]: + """Return one serialized skill result payload.""" + + return { + "id": _require_pk(skill_result), + "content": skill_result.content_id, + "project": skill_result.project_id, + "skill_name": skill_result.skill_name, + "status": skill_result.status, + "result_data": skill_result.result_data, + "error_message": skill_result.error_message, + "model_used": skill_result.model_used, + "latency_ms": skill_result.latency_ms, + "confidence": skill_result.confidence, + "invocation_id": str(skill_result.invocation_id), + "created_at": skill_result.created_at, + "superseded_by": skill_result.superseded_by_id, + } + + +def _serialize_review_item(review_item: ReviewQueue) -> dict[str, Any]: + """Return one serialized review-queue payload.""" + + return { + "id": _require_pk(review_item), + "project": review_item.project_id, + "content": review_item.content_id, + "reason": review_item.reason, + "confidence": review_item.confidence, + "failed_node": review_item.failed_node, + "failure_detail": review_item.failure_detail, + "skill_invocation_id": ( + str(review_item.skill_invocation_id) + if review_item.skill_invocation_id is not None + else None + ), + "created_at": review_item.created_at, + "resolved": review_item.resolved, + "resolved_at": review_item.resolved_at, + "resolution": review_item.resolution, + } + + +def _project_content_or_error(project: Project, content_id: int): + """Return one project-owned content row or a validation payload.""" + + content = Content.objects.filter(pk=content_id).first() + if content is None or content.project_id != _require_pk(project): + return None, {"content": ["Content must belong to the selected project."]} + return content, None + + +def _project_skill_result_reference_or_error(project: Project, skill_result_id: int): + """Return one project-owned superseded-by target or a validation payload.""" + + skill_result = SkillResult.objects.filter(pk=skill_result_id).first() + if skill_result is None or skill_result.project_id != _require_pk(project): + return None, { + "superseded_by": [ + "Superseded skill result must belong to the selected project." + ] + } + return skill_result, None + + +def _validate_skill_status(status_value: str): + """Return a validation payload when the supplied skill status is invalid.""" + + choices = SkillResult._meta.get_field("status").choices or () + valid_statuses = {choice for choice, _ in choices} + if status_value not in valid_statuses: + return {"status": ["Select a valid choice."]} + return None + + +def _validate_review_reason(reason: str): + """Return a validation payload when the supplied review reason is invalid.""" + + choices = ReviewQueue._meta.get_field("reason").choices or () + valid_reasons = {choice for choice, _ in choices} + if reason not in valid_reasons: + return {"reason": ["Select a valid choice."]} + return None + + +def _validate_review_resolution(resolution: str): + """Return a validation payload when the supplied review resolution is invalid.""" + + if resolution == "": + return None + choices = ReviewQueue._meta.get_field("resolution").choices or () + valid_resolutions = {choice for choice, _ in choices} + if resolution not in valid_resolutions: + return {"resolution": ["Select a valid choice."]} + return None + + +@router.get( + "/skill-results/", + response=list[SkillResultSchema], + auth=api_authenticate, +) +def list_skill_results(request: Any, project_id: int = Path(...)): + """Return persisted pipeline skill results for one project.""" + + _require_project_member(request, project_id) + skill_results = SkillResult.objects.select_related( + "content", "project", "superseded_by" + ).filter(project_id=project_id) + return [_serialize_skill_result(skill_result) for skill_result in skill_results] + + +@router.post( + "/skill-results/", + response={201: SkillResultSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_skill_result( + request: Any, + payload: SkillResultCreateInput, + project_id: int = Path(...), +): + """Create one skill result under the selected project.""" + + project = _require_project_contributor(request, project_id) + content, content_error = _project_content_or_error(project, payload.content) + if content_error is not None: + return Status(400, content_error) + status_error = _validate_skill_status(payload.status) + if status_error is not None: + return Status(400, status_error) + + superseded_by = None + if payload.superseded_by is not None: + superseded_by, superseded_error = _project_skill_result_reference_or_error( + project, + payload.superseded_by, + ) + if superseded_error is not None: + return Status(400, superseded_error) + + skill_result = SkillResult.objects.create( + project=project, + content=content, + skill_name=payload.skill_name, + status=payload.status, + result_data=payload.result_data, + error_message=payload.error_message, + model_used=payload.model_used, + latency_ms=payload.latency_ms, + confidence=payload.confidence, + superseded_by=superseded_by, + ) + return Status(201, _serialize_skill_result(skill_result)) + + +@router.get( + "/skill-results/{skill_result_id}/", + response=SkillResultSchema, + auth=api_authenticate, +) +def get_skill_result( + request: Any, + project_id: int = Path(...), + skill_result_id: int = Path(...), +): + """Return one persisted skill result.""" + + _require_project_member(request, project_id) + return _serialize_skill_result( + _get_skill_result_or_404(project_id, skill_result_id) + ) + + +@router.patch( + "/skill-results/{skill_result_id}/", + response={200: SkillResultSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_skill_result( + request: Any, + payload: SkillResultUpdateInput, + project_id: int = Path(...), + skill_result_id: int = Path(...), +): + """Partially update one persisted skill result.""" + + project = _require_project_contributor(request, project_id) + skill_result = _get_skill_result_or_404(project_id, skill_result_id) + updates = payload.model_dump(exclude_unset=True) + + if "content" in updates: + content, content_error = _project_content_or_error(project, updates["content"]) + if content_error is not None: + return Status(400, content_error) + skill_result.content = content + if "status" in updates: + status_error = _validate_skill_status(updates["status"]) + if status_error is not None: + return Status(400, status_error) + skill_result.status = updates["status"] + if "skill_name" in updates: + skill_result.skill_name = updates["skill_name"] + if "result_data" in updates: + skill_result.result_data = updates["result_data"] + if "error_message" in updates: + skill_result.error_message = updates["error_message"] + if "model_used" in updates: + skill_result.model_used = updates["model_used"] + if "latency_ms" in updates: + skill_result.latency_ms = updates["latency_ms"] + if "confidence" in updates: + skill_result.confidence = updates["confidence"] + if "superseded_by" in updates: + superseded_by_id = updates["superseded_by"] + if superseded_by_id is None: + skill_result.superseded_by = None + else: + superseded_by, superseded_error = _project_skill_result_reference_or_error( + project, + superseded_by_id, + ) + if superseded_error is not None: + return Status(400, superseded_error) + skill_result.superseded_by = superseded_by + + skill_result.save() + return _serialize_skill_result(skill_result) + + +@router.delete( + "/skill-results/{skill_result_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_skill_result( + request: Any, + project_id: int = Path(...), + skill_result_id: int = Path(...), +): + """Delete one persisted skill result.""" + + _require_project_contributor(request, project_id) + skill_result = _get_skill_result_or_404(project_id, skill_result_id) + skill_result.delete() + return Status(204) + + +@router.get( + "/review-queue/", + response=list[ReviewQueueItemSchema], + auth=api_authenticate, +) +def list_review_queue_items(request: Any, project_id: int = Path(...)): + """Return review-queue items for one project.""" + + _require_project_contributor(request, project_id) + review_items = ReviewQueue.objects.select_related("content", "project").filter( + project_id=project_id + ) + return [_serialize_review_item(review_item) for review_item in review_items] + + +@router.post( + "/review-queue/", + response={201: ReviewQueueItemSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_review_queue_item( + request: Any, + payload: ReviewQueueCreateInput, + project_id: int = Path(...), +): + """Create one review-queue item under the selected project.""" + + project = _require_project_contributor(request, project_id) + content, content_error = _project_content_or_error(project, payload.content) + if content_error is not None: + return Status(400, content_error) + reason_error = _validate_review_reason(payload.reason) + if reason_error is not None: + return Status(400, reason_error) + resolution_error = _validate_review_resolution(payload.resolution) + if resolution_error is not None: + return Status(400, resolution_error) + + review_item = ReviewQueue.objects.create( + project=project, + content=content, + reason=payload.reason, + confidence=payload.confidence, + failed_node=payload.failed_node, + failure_detail=payload.failure_detail, + skill_invocation_id=payload.skill_invocation_id, + resolved=payload.resolved, + resolved_at=payload.resolved_at, + resolution=payload.resolution, + ) + return Status(201, _serialize_review_item(review_item)) + + +@router.get( + "/review-queue/{review_item_id}/", + response=ReviewQueueItemSchema, + auth=api_authenticate, +) +def get_review_queue_item( + request: Any, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Return one review-queue item.""" + + _require_project_contributor(request, project_id) + return _serialize_review_item(_get_review_item_or_404(project_id, review_item_id)) + + +@router.patch( + "/review-queue/{review_item_id}/", + response={200: ReviewQueueItemSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_review_queue_item( + request: Any, + payload: ReviewQueueUpdateInput, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Partially update one review-queue item.""" + + project = _require_project_contributor(request, project_id) + review_item = _get_review_item_or_404(project_id, review_item_id) + updates = payload.model_dump(exclude_unset=True) + + if "content" in updates: + content, content_error = _project_content_or_error(project, updates["content"]) + if content_error is not None: + return Status(400, content_error) + review_item.content = content + if "reason" in updates: + reason_error = _validate_review_reason(updates["reason"]) + if reason_error is not None: + return Status(400, reason_error) + review_item.reason = updates["reason"] + if "confidence" in updates: + review_item.confidence = updates["confidence"] + if "failed_node" in updates: + review_item.failed_node = updates["failed_node"] + if "failure_detail" in updates: + review_item.failure_detail = updates["failure_detail"] + if "skill_invocation_id" in updates: + review_item.skill_invocation_id = updates["skill_invocation_id"] + if "resolved" in updates: + review_item.resolved = updates["resolved"] + if "resolved_at" in updates: + review_item.resolved_at = updates["resolved_at"] + if "resolution" in updates: + resolution_error = _validate_review_resolution(updates["resolution"]) + if resolution_error is not None: + return Status(400, resolution_error) + review_item.resolution = updates["resolution"] + + review_item.save() + return _serialize_review_item(review_item) + + +@router.delete( + "/review-queue/{review_item_id}/", + response={204: None}, + auth=api_authenticate, +) +def delete_review_queue_item( + request: Any, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Delete one review-queue item.""" + + _require_project_contributor(request, project_id) + review_item = _get_review_item_or_404(project_id, review_item_id) + review_item.delete() + return Status(204) + + +@router.post( + "/review-queue/{review_item_id}/retry/", + response={200: dict[str, Any], 202: ReviewQueueRetryQueuedSchema}, + auth=api_authenticate, +) +def retry_review_queue_item_route( + request: Any, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Retry one review-queue item from its failed pipeline node.""" + + _require_project_contributor(request, project_id) + review_item = _get_review_item_or_404(project_id, review_item_id) + if task_always_eager(): + return retry_pipeline_review_item(review_item.pk) + enqueue_task(retry_pipeline_review_item, review_item.pk) + return Status( + 202, + { + "review_item_id": _require_pk(review_item), + "status": "queued", + }, + ) + + +@router.post( + "/review-queue/{review_item_id}/resolve/", + response=ReviewQueueItemSchema, + auth=api_authenticate, +) +def resolve_review_queue_item_route( + request: Any, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Resolve one review-queue item without retrying it.""" + + _require_project_contributor(request, project_id) + review_item = _get_review_item_or_404(project_id, review_item_id) + review_item.resolved = True + review_item.resolution = ReviewResolution.MANUALLY_RESOLVED + review_item.resolved_at = timezone.now() + review_item.save(update_fields=["resolved", "resolution", "resolved_at"]) + return _serialize_review_item(review_item) + + +@router.post( + "/review-queue/{review_item_id}/archive/", + response=ReviewQueueItemSchema, + auth=api_authenticate, +) +def archive_review_queue_item_route( + request: Any, + project_id: int = Path(...), + review_item_id: int = Path(...), +): + """Archive one review-queue item.""" + + _require_project_contributor(request, project_id) + review_item = _get_review_item_or_404(project_id, review_item_id) + review_item.resolved = True + review_item.resolution = ReviewResolution.ARCHIVED + review_item.resolved_at = timezone.now() + review_item.save(update_fields=["resolved", "resolution", "resolved_at"]) + return _serialize_review_item(review_item) + + +__all__ = ["router"] diff --git a/pipeline/serializers.py b/pipeline/serializers.py deleted file mode 100644 index 9bff8ddc..00000000 --- a/pipeline/serializers.py +++ /dev/null @@ -1,81 +0,0 @@ -"""DRF serializers for pipeline-domain models.""" - -from rest_framework import serializers - -from core.serializer_mixins import ProjectScopedSerializerMixin -from pipeline.models import ReviewQueue, SkillResult - - -class SkillResultSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize persisted AI skill executions for content.""" - - class Meta: - model = SkillResult - fields = [ - "id", - "content", - "project", - "skill_name", - "status", - "result_data", - "error_message", - "model_used", - "latency_ms", - "confidence", - "invocation_id", - "created_at", - "superseded_by", - ] - read_only_fields = ["id", "project", "created_at"] - - def validate(self, attrs): - """Reject skill results whose content does not belong to the active project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - content = attrs.get("content") or getattr(self.instance, "content", None) - if project and content and content.project_id != project.id: - raise serializers.ValidationError( - {"content": "Content must belong to the selected project."} - ) - return attrs - - -class ReviewQueueSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize manual-review queue items for project content.""" - - class Meta: - model = ReviewQueue - fields = [ - "id", - "project", - "content", - "reason", - "confidence", - "failed_node", - "failure_detail", - "skill_invocation_id", - "created_at", - "resolved", - "resolved_at", - "resolution", - ] - read_only_fields = ["id", "project", "created_at"] - - def validate(self, attrs): - """Reject review items whose content does not belong to the active project.""" - - project = ( - self.context.get("project") - or attrs.get("project") - or getattr(self.instance, "project", None) - ) - content = attrs.get("content") or getattr(self.instance, "content", None) - if project and content and content.project_id != project.id: - raise serializers.ValidationError( - {"content": "Content must belong to the selected project."} - ) - return attrs diff --git a/pipeline/tests/test_admin.py b/pipeline/tests/test_admin.py index 4b60f0ec..dd21c55e 100644 --- a/pipeline/tests/test_admin.py +++ b/pipeline/tests/test_admin.py @@ -1,6 +1,6 @@ from types import SimpleNamespace from typing import Any, cast -from unittest.mock import ANY, Mock +from unittest.mock import ANY, AsyncMock, Mock import pytest from django.contrib import messages @@ -246,6 +246,42 @@ def test_review_queue_actions_update_resolution_and_emit_message( assert message_user_mock.call_count == 2 +def test_review_queue_retry_action_queues_taskiq_retry(source_admin_context, mocker): + content = Content.objects.create( + project=source_admin_context.project, + url="https://example.com/review-retry-action", + title="Review Retry Action", + author="Reviewer", + source_plugin=SourcePluginName.RSS, + published_date=timezone.now(), + content_text="Review retry action content.", + ) + review_item = ReviewQueue.objects.create( + project=source_admin_context.project, + content=content, + reason=ReviewReason.RETRY_EXHAUSTED, + confidence=0.25, + resolved=False, + ) + admin_instance = ReviewQueueAdmin(ReviewQueue, AdminSite()) + message_user_mock = _message_user_mock(admin_instance, mocker) + queue_mock = mocker.patch( + "pipeline.admin.retry_pipeline_review_item.kiq", + new_callable=AsyncMock, + ) + + admin_instance.retry_selected_items( + _request(), ReviewQueue.objects.filter(pk=review_item.pk) + ) + + queue_mock.assert_awaited_once_with(_require_pk(review_item)) + message_user_mock.assert_called_once_with( + ANY, + "Selected items queued for retry.", + messages.SUCCESS, + ) + + def test_skill_result_admin_handles_unknown_status_and_empty_performance( source_admin_context, ): diff --git a/pipeline/tests/test_ninja_api.py b/pipeline/tests/test_ninja_api.py new file mode 100644 index 00000000..6bb8ec1c --- /dev/null +++ b/pipeline/tests/test_ninja_api.py @@ -0,0 +1,459 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Any, cast +from unittest.mock import AsyncMock, patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from content.models import Content +from entities.models import Entity +from pipeline.models import ( + ReviewQueue, + ReviewReason, + ReviewResolution, + SkillResult, + SkillStatus, +) +from projects.models import Project, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + + return cast(Any, user_model.objects).create_user(**kwargs) + + +class PipelineNinjaApiTests(TestCase): + """Exercise project-scoped Ninja pipeline endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member = _create_user( + user_model, username="member", password="testpass123" + ) + self.reader = _create_user( + user_model, username="reader", password="testpass123" + ) + self.other_user = _create_user( + user_model, + username="other", + password="testpass123", + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.member, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) + ProjectMembership.objects.create( + user=self.reader, + project=self.owner_project, + role=ProjectRole.READER, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.owner_entity = Entity.objects.create( + project=self.owner_project, + name="Owner Entity", + type="individual", + ) + self.other_entity = Entity.objects.create( + project=self.other_project, + name="Other Entity", + type="vendor", + ) + self.owner_content = Content.objects.create( + project=self.owner_project, + url="https://example.com/owner", + title="Owner Content", + author="Owner Author", + entity=self.owner_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Owner content text", + ) + self.other_content = Content.objects.create( + project=self.other_project, + url="https://example.com/other", + title="Other Content", + author="Other Author", + entity=self.other_entity, + source_plugin="rss", + published_date="2026-04-21T00:00:00Z", + content_text="Other content text", + ) + self.owner_skill_result = SkillResult.objects.create( + project=self.owner_project, + content=self.owner_content, + skill_name="summarization", + status=SkillStatus.COMPLETED, + result_data={"summary": "Owner summary"}, + error_message="", + model_used="gpt-test", + latency_ms=125, + confidence=0.87, + ) + self.other_skill_result = SkillResult.objects.create( + project=self.other_project, + content=self.other_content, + skill_name="summarization", + status=SkillStatus.FAILED, + result_data=None, + error_message="boom", + model_used="gpt-test", + ) + self.owner_review_item = ReviewQueue.objects.create( + project=self.owner_project, + content=self.owner_content, + reason=ReviewReason.BORDERLINE_RELEVANCE, + confidence=0.42, + failed_node="classify", + failure_detail="Need a human decision.", + resolved=False, + resolution="", + ) + self.other_review_item = ReviewQueue.objects.create( + project=self.other_project, + content=self.other_content, + reason=ReviewReason.RETRY_EXHAUSTED, + confidence=0.11, + failed_node="summarize", + failure_detail="Other project detail.", + resolved=False, + resolution="", + ) + self.client.force_login(self.owner) + + def test_skill_result_routes_cover_list_create_update_and_validation(self): + list_response = self.client.get( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + detail_response = self.client.get( + reverse( + "ninja-api:get_skill_result", + kwargs={ + "project_id": _require_pk(self.owner_project), + "skill_result_id": _require_pk(self.owner_skill_result), + }, + ) + ) + create_response = self.client.post( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.owner_content), + "project": _require_pk(self.other_project), + "skill_name": "relevance_scoring", + "status": SkillStatus.RUNNING, + "result_data": {"queued": True}, + "model_used": "gpt-5.4", + "superseded_by": _require_pk(self.owner_skill_result), + }, + content_type="application/json", + ) + + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 1) + self.assertEqual( + list_response.json()[0]["id"], _require_pk(self.owner_skill_result) + ) + self.assertEqual(detail_response.status_code, HTTPStatus.OK) + self.assertEqual( + detail_response.json()["content"], _require_pk(self.owner_content) + ) + + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + created_id = create_response.json()["id"] + created_skill_result = SkillResult.objects.get(pk=created_id) + self.assertEqual(created_skill_result.project, self.owner_project) + self.assertEqual(created_skill_result.superseded_by, self.owner_skill_result) + self.assertEqual( + create_response.json()["invocation_id"], + str(created_skill_result.invocation_id), + ) + + update_response = self.client.patch( + reverse( + "ninja-api:get_skill_result", + kwargs={ + "project_id": _require_pk(self.owner_project), + "skill_result_id": created_id, + }, + ), + { + "status": SkillStatus.COMPLETED, + "confidence": 0.91, + "superseded_by": None, + }, + content_type="application/json", + ) + invalid_response = self.client.post( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.other_content), + "skill_name": "summarization", + "status": SkillStatus.COMPLETED, + }, + content_type="application/json", + ) + + self.assertEqual(update_response.status_code, HTTPStatus.OK) + self.assertEqual(update_response.json()["status"], SkillStatus.COMPLETED) + self.assertAlmostEqual(update_response.json()["confidence"], 0.91) + self.assertIsNone(update_response.json()["superseded_by"]) + self.assertEqual(invalid_response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + invalid_response.json()["content"][0], + "Content must belong to the selected project.", + ) + + def test_review_queue_routes_cover_list_patch_and_workflow_actions(self): + list_response = self.client.get( + reverse( + "ninja-api:list_review_queue_items", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + detail_response = self.client.get( + reverse( + "ninja-api:get_review_queue_item", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": _require_pk(self.owner_review_item), + }, + ) + ) + create_response = self.client.post( + reverse( + "ninja-api:list_review_queue_items", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.owner_content), + "reason": ReviewReason.LOW_CONFIDENCE_CLASSIFICATION, + "confidence": 0.33, + "failed_node": "classify", + "failure_detail": "Low confidence.", + }, + content_type="application/json", + ) + + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 1) + self.assertEqual( + list_response.json()[0]["id"], _require_pk(self.owner_review_item) + ) + self.assertEqual(detail_response.status_code, HTTPStatus.OK) + self.assertEqual( + detail_response.json()["content"], _require_pk(self.owner_content) + ) + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + + created_review_id = create_response.json()["id"] + patch_response = self.client.patch( + reverse( + "ninja-api:get_review_queue_item", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": created_review_id, + }, + ), + { + "resolved": True, + "resolution": ReviewResolution.HUMAN_APPROVED, + }, + content_type="application/json", + ) + resolve_response = self.client.post( + reverse( + "ninja-api:resolve_review_queue_item_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": _require_pk(self.owner_review_item), + }, + ), + ) + archive_target = ReviewQueue.objects.create( + project=self.owner_project, + content=self.owner_content, + reason=ReviewReason.CIRCUIT_BREAKER_OPEN, + confidence=0.14, + failed_node="summarize", + failure_detail="Paused for inspection.", + ) + archive_response = self.client.post( + reverse( + "ninja-api:archive_review_queue_item_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": _require_pk(archive_target), + }, + ), + ) + invalid_response = self.client.post( + reverse( + "ninja-api:list_review_queue_items", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.other_content), + "reason": ReviewReason.BORDERLINE_RELEVANCE, + "confidence": 0.5, + }, + content_type="application/json", + ) + + self.assertEqual(patch_response.status_code, HTTPStatus.OK) + self.assertTrue(patch_response.json()["resolved"]) + self.assertEqual( + patch_response.json()["resolution"], + ReviewResolution.HUMAN_APPROVED, + ) + self.assertEqual(resolve_response.status_code, HTTPStatus.OK) + self.assertEqual( + resolve_response.json()["resolution"], + ReviewResolution.MANUALLY_RESOLVED, + ) + self.assertTrue(resolve_response.json()["resolved"]) + self.assertIsNotNone(resolve_response.json()["resolved_at"]) + self.assertEqual(archive_response.status_code, HTTPStatus.OK) + self.assertEqual( + archive_response.json()["resolution"], ReviewResolution.ARCHIVED + ) + self.assertEqual(invalid_response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + invalid_response.json()["content"][0], + "Content must belong to the selected project.", + ) + + def test_review_queue_retry_handles_eager_and_queued_modes(self): + with self.settings(TASKIQ_ALWAYS_EAGER=True): + with patch( + "pipeline.ninja_api.retry_pipeline_review_item", + return_value={ + "review_item_id": _require_pk(self.owner_review_item), + "status": "retried", + }, + ) as retry_mock: + eager_response = self.client.post( + reverse( + "ninja-api:retry_review_queue_item_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": _require_pk(self.owner_review_item), + }, + ), + ) + + with self.settings(TASKIQ_ALWAYS_EAGER=False): + with patch( + "pipeline.ninja_api.retry_pipeline_review_item.kiq", + new_callable=AsyncMock, + ) as queue_mock: + queued_response = self.client.post( + reverse( + "ninja-api:retry_review_queue_item_route", + kwargs={ + "project_id": _require_pk(self.owner_project), + "review_item_id": _require_pk(self.owner_review_item), + }, + ), + ) + + retry_mock.assert_called_once_with(_require_pk(self.owner_review_item)) + self.assertEqual(eager_response.status_code, HTTPStatus.OK) + self.assertEqual(eager_response.json()["status"], "retried") + queue_mock.assert_awaited_once_with(_require_pk(self.owner_review_item)) + self.assertEqual(queued_response.status_code, HTTPStatus.ACCEPTED) + self.assertEqual(queued_response.json()["status"], "queued") + + def test_pipeline_ninja_permissions_match_existing_roles(self): + self.client.force_login(self.reader) + + reader_skill_results = self.client.get( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + reader_review_queue = self.client.get( + reverse( + "ninja-api:list_review_queue_items", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + reader_skill_create = self.client.post( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content": _require_pk(self.owner_content), + "skill_name": "summarization", + "status": SkillStatus.PENDING, + }, + content_type="application/json", + ) + + self.assertEqual(reader_skill_results.status_code, HTTPStatus.OK) + self.assertEqual(reader_review_queue.status_code, HTTPStatus.FORBIDDEN) + self.assertEqual(reader_skill_create.status_code, HTTPStatus.FORBIDDEN) + + self.client.force_login(self.member) + member_review_queue = self.client.get( + reverse( + "ninja-api:list_review_queue_items", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + self.assertEqual(member_review_queue.status_code, HTTPStatus.OK) + + def test_pipeline_ninja_requires_authentication(self): + self.client.logout() + + response = self.client.get( + reverse( + "ninja-api:list_skill_results", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED) + self.assertEqual(response.json(), {"detail": "Unauthorized"}) diff --git a/pipeline/tests/test_pipeline.py b/pipeline/tests/test_pipeline.py index f18178ec..bdc7aa1c 100644 --- a/pipeline/tests/test_pipeline.py +++ b/pipeline/tests/test_pipeline.py @@ -128,7 +128,7 @@ def test_process_content_runs_full_pipeline_for_relevant_content( result = process_content(_require_pk(pipeline_context.content)) pipeline_context.content.refresh_from_db() - assert result["status"] == "completed" + assert result.get("status") == "completed" assert pipeline_context.content.content_type == "release_notes" assert pipeline_context.content.relevance_score == pytest.approx(0.92) assert pipeline_context.content.authority_adjusted_score == pytest.approx(1.0) @@ -273,7 +273,7 @@ def test_process_content_queues_borderline_items_for_review(pipeline_context, mo result = process_content(pipeline_context.content.id) pipeline_context.content.refresh_from_db() - assert result["status"] == "review" + assert result.get("status") == "review" assert pipeline_context.content.is_active is True summarize_mock.assert_not_called() review_item = ReviewQueue.objects.get( @@ -320,7 +320,7 @@ def test_process_content_archives_irrelevant_items(pipeline_context, mocker): result = process_content(pipeline_context.content.id) pipeline_context.content.refresh_from_db() - assert result["status"] == "archived" + assert result.get("status") == "archived" assert pipeline_context.content.is_active is False summarize_mock.assert_not_called() assert ( @@ -377,7 +377,7 @@ def test_process_content_adds_review_item_for_low_confidence_classification( result = process_content(pipeline_context.content.id) - assert result["status"] == "completed" + assert result.get("status") == "completed" review_item = ReviewQueue.objects.get( content=pipeline_context.content, reason=ReviewReason.LOW_CONFIDENCE_CLASSIFICATION, @@ -415,7 +415,7 @@ def test_process_content_marks_exact_duplicates_and_skips_downstream_skills( duplicate.refresh_from_db() existing.refresh_from_db() - assert result["status"] == "duplicate" + assert result.get("status") == "duplicate" assert duplicate.duplicate_of == existing assert duplicate.is_active is False assert existing.duplicate_signal_count == 1 @@ -468,7 +468,7 @@ def test_process_content_marks_semantic_duplicates_with_high_similarity( candidate.refresh_from_db() existing.refresh_from_db() - assert result["status"] == "duplicate" + assert result.get("status") == "duplicate" assert candidate.duplicate_of == existing assert candidate.is_active is False assert existing.duplicate_signal_count == 1 @@ -1343,7 +1343,7 @@ def test_process_content_records_entity_extraction_skill_result( skill_name=ENTITY_EXTRACTION_SKILL_NAME, ) - assert result["status"] == "completed" + assert result.get("status") == "completed" assert skill_result.status == SkillStatus.COMPLETED assert skill_result.confidence == pytest.approx(0.88) assert skill_result.result_data is not None diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f6f4a9f..967fc42f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,8 +21,8 @@ importers: specifier: ^40.0.0 version: 40.0.0(stylelint@17.11.0(typescript@5.9.3)) turbo: - specifier: ^2.9.14 - version: 2.9.14 + specifier: ^2.9.16 + version: 2.9.16 frontend: dependencies: @@ -1559,33 +1559,33 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@turbo/darwin-64@2.9.14': - resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} + '@turbo/darwin-64@2.9.16': + resolution: {integrity: sha512-jLjApWTSNd7JZ5JaLYfelW1ytnGQOvB7ivl+2RD1xQvJTbi8I9gBjzcga7tDZVPyaxpl10YTfJt3BrYXR18KDw==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.14': - resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} + '@turbo/darwin-arm64@2.9.16': + resolution: {integrity: sha512-YPgrn+5HIGzrx0O2a631SV4MBQUe4W/DafMFUuBVgaU32PW9/OTT0ehviF0QSxTXuRJlHvW2eUTemddF5/spmw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.14': - resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} + '@turbo/linux-64@2.9.16': + resolution: {integrity: sha512-vAEf1H6l26lTpl9FJ/peQo1NUB8RC0sbEJJz5mPcUhHA2bPDup2x3CZPgo/bH8S4cUcBLm4FN3UHd5iUO2RAew==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.14': - resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} + '@turbo/linux-arm64@2.9.16': + resolution: {integrity: sha512-xDBLR2PZg4BrQOchfG6svgpv5FCNJ2TOtT2psLdEJcdKo1BH+pnPs9Xj6pvUjgfkHbuvBOfeE4R6tvxMoQKDHQ==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.14': - resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} + '@turbo/windows-64@2.9.16': + resolution: {integrity: sha512-NBAJnaUiGdgkSzQwUIdOvkCkcpTSu58G/sBGa0mvBtzfvFOOgrQwepKOOQ8cp6sWM6OcKDNFj2p1dsZA1OWjPg==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.14': - resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} + '@turbo/windows-arm64@2.9.16': + resolution: {integrity: sha512-Y7SJppD0Z8wjO3Ec0ZGd9KQ4Yv0BMnA8CIowj5Vp+OEVsosXDG2weK6/t1RRLfJmc2Ozrnd6y4DOgQys+mn3WQ==} cpu: [arm64] os: [win32] @@ -3871,8 +3871,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - turbo@2.9.14: - resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} + turbo@2.9.16: + resolution: {integrity: sha512-NqgRQy6j6dPYcdSdv0q1g9QsZg7SWg87RERM8otw/1AtKU2yTFVClOM7cbwKzOonZr/Ek1blTBucw64L9H0Bwg==} hasBin: true tw-animate-css@1.4.0: @@ -5241,22 +5241,22 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@turbo/darwin-64@2.9.14': + '@turbo/darwin-64@2.9.16': optional: true - '@turbo/darwin-arm64@2.9.14': + '@turbo/darwin-arm64@2.9.16': optional: true - '@turbo/linux-64@2.9.14': + '@turbo/linux-64@2.9.16': optional: true - '@turbo/linux-arm64@2.9.14': + '@turbo/linux-arm64@2.9.16': optional: true - '@turbo/windows-64@2.9.14': + '@turbo/windows-64@2.9.16': optional: true - '@turbo/windows-arm64@2.9.14': + '@turbo/windows-arm64@2.9.16': optional: true '@tybys/wasm-util@0.10.2': @@ -7848,14 +7848,14 @@ snapshots: tslib@2.8.1: {} - turbo@2.9.14: + turbo@2.9.16: optionalDependencies: - '@turbo/darwin-64': 2.9.14 - '@turbo/darwin-arm64': 2.9.14 - '@turbo/linux-64': 2.9.14 - '@turbo/linux-arm64': 2.9.14 - '@turbo/windows-64': 2.9.14 - '@turbo/windows-arm64': 2.9.14 + '@turbo/darwin-64': 2.9.16 + '@turbo/darwin-arm64': 2.9.16 + '@turbo/linux-64': 2.9.16 + '@turbo/linux-arm64': 2.9.16 + '@turbo/windows-64': 2.9.16 + '@turbo/windows-arm64': 2.9.16 tw-animate-css@1.4.0: {} diff --git a/projects/admin.py b/projects/admin.py index 1d4940c6..f0886cf8 100644 --- a/projects/admin.py +++ b/projects/admin.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any, cast from django import forms -from django.conf import settings from django.contrib import admin, messages from django.utils import timezone from django.utils.html import format_html @@ -12,6 +11,7 @@ from import_export.admin import ExportActionMixin from unfold.admin import ModelAdmin +from digest_engine.taskiq import enqueue_task, task_always_eager from ingestion.plugins import get_plugin_for_source_config, validate_plugin_config from projects.models import ( BlueskyCredentials, @@ -502,12 +502,12 @@ def recompute_selected_authority_models(self, request, queryset): selected_count = 0 for config in queryset.select_related("project"): project_id = int(config.project_id) - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_source_quality(project_id) recompute_authority_scores(project_id) else: - recompute_source_quality.delay(project_id) - recompute_authority_scores.delay(project_id) + enqueue_task(recompute_source_quality, project_id) + enqueue_task(recompute_authority_scores, project_id) selected_count += 1 if selected_count: self.message_user( diff --git a/projects/api.py b/projects/api.py deleted file mode 100644 index ced39cf0..00000000 --- a/projects/api.py +++ /dev/null @@ -1,755 +0,0 @@ -"""REST API viewsets for project-owned models.""" - -from typing import Any, cast - -from django.conf import settings -from django.core.mail import send_mail -from django.utils import timezone -from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer -from rest_framework import mixins, serializers, status, viewsets -from rest_framework.decorators import action -from rest_framework.permissions import BasePermission -from rest_framework.response import Response - -from core.api import ( - AUTHENTICATION_REQUIRED_RESPONSE, - BLUESKY_CREDENTIALS_RESPONSE_EXAMPLE, - BLUESKY_CREDENTIALS_VERIFY_RESPONSE, - LINKEDIN_CREDENTIALS_RESPONSE_EXAMPLE, - LINKEDIN_CREDENTIALS_VERIFY_RESPONSE, - LINKEDIN_OAUTH_AUTHORIZE_RESPONSE, - LINKEDIN_OAUTH_AUTHORIZE_RESPONSE_EXAMPLE, - MASTODON_CREDENTIALS_RESPONSE_EXAMPLE, - MASTODON_CREDENTIALS_VERIFY_RESPONSE, - PROJECT_CREATE_REQUEST_EXAMPLE, - PROJECT_RESPONSE_EXAMPLE, - SOURCE_CONFIG_BLUESKY_REQUEST_EXAMPLE, - SOURCE_CONFIG_CREATE_REQUEST_EXAMPLE, - SOURCE_CONFIG_LINKEDIN_REQUEST_EXAMPLE, - SOURCE_CONFIG_MASTODON_REQUEST_EXAMPLE, - SOURCE_CONFIG_REDDIT_REQUEST_EXAMPLE, - SOURCE_CONFIG_RESPONSE_EXAMPLE, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - build_success_response, - document_group_access_viewset, - document_project_owned_viewset, - logger, -) -from core.permissions import ( - IsProjectAdmin, - IsProjectMember, - IsProjectMemberWritable, - get_visible_projects_queryset, -) -from ingestion.plugins.bluesky import BlueskySourcePlugin -from ingestion.plugins.linkedin import LinkedInSourcePlugin -from ingestion.plugins.mastodon import MastodonSourcePlugin -from projects.linkedin_oauth import build_linkedin_authorize_url -from projects.models import ( - BlueskyCredentials, - LinkedInCredentials, - MastodonCredentials, - Project, - ProjectConfig, - ProjectMembership, - ProjectRole, - SourceConfig, - generate_project_intake_token, -) -from projects.serializers import ( - BlueskyCredentialsSerializer, - LinkedInCredentialsSerializer, - MastodonCredentialsSerializer, - ProjectConfigSerializer, - ProjectMembershipSerializer, - ProjectSerializer, - SourceConfigSerializer, -) -from users.models import MembershipInvitation -from users.serializers import MembershipInvitationSerializer - - -def _assert_project_keeps_admin( - project: Project, - membership: ProjectMembership, - *, - next_role: str | None = None, -) -> None: - """Reject changes that would leave a project without an admin.""" - - resulting_role = next_role - if membership.role != ProjectRole.ADMIN or resulting_role == ProjectRole.ADMIN: - return - - has_other_admin = ( - ProjectMembership.objects.filter(project=project) - .exclude(pk=membership.pk) - .filter(role=ProjectRole.ADMIN) - .exists() - ) - if not has_other_admin: - raise serializers.ValidationError( - {"role": "Projects must keep at least one admin."} - ) - - -def _send_membership_invitation_email(invitation: MembershipInvitation) -> None: - """Send the one-time membership invitation email.""" - - invite_url = f"{settings.FRONTEND_BASE_URL.rstrip('/')}/invite/{invitation.token}" - send_mail( - subject=f"You're invited to join {invitation.project.name}", - message=( - f"You have been invited to join {invitation.project.name} as a " - f"{invitation.role}.\n\nOpen this link to accept the invitation:\n{invite_url}" - ), - from_email=settings.DEFAULT_FROM_EMAIL, - recipient_list=[invitation.email], - fail_silently=False, - ) - - -@document_group_access_viewset( - resource_plural="projects", - resource_singular="project", - create_description="Create a new project for the authenticated user.", - tag="Project Management", - action_overrides=build_crud_action_overrides( - ProjectSerializer, - resource_plural="projects available to the authenticated user", - resource_singular="project", - create_examples=[PROJECT_CREATE_REQUEST_EXAMPLE, PROJECT_RESPONSE_EXAMPLE], - create_response_examples=[PROJECT_RESPONSE_EXAMPLE], - retrieve_examples=[PROJECT_RESPONSE_EXAMPLE], - ), -) -class ProjectViewSet(viewsets.ModelViewSet): - """Manage projects accessible through the current user's project memberships.""" - - serializer_class = ProjectSerializer - queryset: Any = Project.objects.select_related("bluesky_credentials") - lookup_url_kwarg = "id" - - def get_permissions(self): - """Apply role-aware permissions by action for project-level operations.""" - - permission_classes: list[type[BasePermission]] - if self.action in { - "update", - "partial_update", - "destroy", - "rotate_intake_token", - "start_linkedin_oauth", - "verify_bluesky_credentials", - "verify_linkedin_credentials", - "verify_mastodon_credentials", - }: - permission_classes = [IsProjectAdmin] - elif self.action in {"list", "retrieve"}: - permission_classes = [IsProjectMember] - else: - permission_classes = cast( - list[type[BasePermission]], - getattr(self, "permission_classes", []) or [], - ) - return [permission() for permission in permission_classes] - - def get_queryset(self) -> Any: - """Limit projects to those visible through the authenticated user.""" - - return get_visible_projects_queryset(self.request.user).select_related( - "bluesky_credentials" - ) - - def perform_create(self, serializer): - """Create the project and make the creator its first admin.""" - - project = serializer.save() - ProjectMembership.objects.create( - user=self.request.user, - project=project, - role=ProjectRole.ADMIN, - ) - - @extend_schema( - summary="Rotate newsletter intake token", - description=( - "Generate a fresh project-specific newsletter intake token and return the " - "updated project payload." - ), - tags=["Project Management"], - request=None, - responses={200: ProjectSerializer, 403: AUTHENTICATION_REQUIRED_RESPONSE}, - ) - @action(detail=True, methods=["post"], url_path="rotate-intake-token") - def rotate_intake_token(self, request, *args, **kwargs): - """Generate a fresh intake token for the selected project.""" - - project = self.get_object() - project.intake_token = generate_project_intake_token() - project.save(update_fields=["intake_token"]) - serializer = self.get_serializer(project) - return Response(serializer.data) - - @extend_schema( - summary="Verify Bluesky credentials", - description=( - "Verify the selected project's stored Bluesky credentials by authenticating " - "the account and checking the current session." - ), - tags=["Ingestion"], - request=None, - responses={ - 200: build_success_response( - BLUESKY_CREDENTIALS_VERIFY_RESPONSE, - "The project's Bluesky credentials were verified successfully.", - ), - 400: OpenApiResponse( - response=inline_serializer( - name="BlueskyCredentialsVerifyErrorResponse", - fields={ - "type": serializers.CharField(), - "errors": inline_serializer( - name="BlueskyCredentialsVerifyError", - fields={ - "code": serializers.CharField(), - "detail": serializers.CharField(), - "attr": serializers.CharField(allow_null=True), - }, - many=True, - ), - }, - ), - description="The project is missing Bluesky credentials or verification failed.", - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path="verify-bluesky-credentials") - def verify_bluesky_credentials(self, request, *args, **kwargs): - """Verify the Bluesky credentials stored for the selected project.""" - - project = self.get_object() - try: - credentials = project.bluesky_credentials - except BlueskyCredentials.DoesNotExist as exc: - raise serializers.ValidationError( - { - "bluesky_credentials": "No Bluesky credentials are configured for this project." - } - ) from exc - - try: - BlueskySourcePlugin.verify_credentials(credentials) - except Exception as exc: - logger.exception( - "Bluesky credential verification failed for project id=%s", - project.id, - ) - raise serializers.ValidationError( - { - "bluesky_credentials": ( - "Credential verification failed. Please re-check the credentials " - "and try again." - ) - } - ) from exc - - credentials.refresh_from_db() - return Response( - { - "status": "verified", - "handle": credentials.handle, - "last_verified_at": credentials.last_verified_at, - "last_error": "", - } - ) - - @extend_schema( - summary="Start LinkedIn OAuth", - description=( - "Build the LinkedIn authorization URL for the selected project so the " - "current admin can connect or re-authorize project-scoped LinkedIn credentials." - ), - tags=["Ingestion"], - request=inline_serializer( - name="LinkedInOAuthStartRequest", - fields={ - "redirect_to": serializers.CharField(required=False), - }, - ), - responses={ - 200: build_success_response( - LINKEDIN_OAUTH_AUTHORIZE_RESPONSE, - "The LinkedIn authorization URL was generated successfully.", - examples=[LINKEDIN_OAUTH_AUTHORIZE_RESPONSE_EXAMPLE], - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path="start-linkedin-oauth") - def start_linkedin_oauth(self, request, *args, **kwargs): - """Return the LinkedIn OAuth authorization URL for the selected project.""" - - project = self.get_object() - authorize_url = build_linkedin_authorize_url( - project, - request.data.get("redirect_to"), - ) - return Response({"authorize_url": authorize_url}) - - @extend_schema( - summary="Verify LinkedIn credentials", - description=( - "Verify the selected project's stored LinkedIn credentials by calling " - "the LinkedIn member identity endpoint with the configured access token." - ), - tags=["Ingestion"], - request=None, - responses={ - 200: build_success_response( - LINKEDIN_CREDENTIALS_VERIFY_RESPONSE, - "The project's LinkedIn credentials were verified successfully.", - ), - 400: OpenApiResponse( - response=inline_serializer( - name="LinkedInCredentialsVerifyErrorResponse", - fields={ - "type": serializers.CharField(), - "errors": inline_serializer( - name="LinkedInCredentialsVerifyError", - fields={ - "code": serializers.CharField(), - "detail": serializers.CharField(), - "attr": serializers.CharField(allow_null=True), - }, - many=True, - ), - }, - ), - description="The project is missing LinkedIn credentials or verification failed.", - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path="verify-linkedin-credentials") - def verify_linkedin_credentials(self, request, *args, **kwargs): - """Verify the LinkedIn credentials stored for the selected project.""" - - project = self.get_object() - try: - credentials = project.linkedin_credentials - except LinkedInCredentials.DoesNotExist as exc: - raise serializers.ValidationError( - { - "linkedin_credentials": "No LinkedIn credentials are configured for this project." - } - ) from exc - - try: - LinkedInSourcePlugin.verify_credentials(credentials) - except Exception as exc: - logger.exception( - "LinkedIn credential verification failed for project id=%s", - project.id, - ) - raise serializers.ValidationError( - { - "linkedin_credentials": ( - "Credential verification failed. Please re-check the credentials " - "and try again." - ) - } - ) from exc - - credentials.refresh_from_db() - return Response( - { - "status": "verified", - "member_urn": credentials.member_urn, - "expires_at": credentials.expires_at, - "last_verified_at": credentials.last_verified_at, - "last_error": "", - } - ) - - @extend_schema( - summary="Verify Mastodon credentials", - description=( - "Verify the selected project's stored Mastodon credentials by " - "authenticating the configured instance token and checking the bound " - "account." - ), - tags=["Ingestion"], - request=None, - responses={ - 200: build_success_response( - MASTODON_CREDENTIALS_VERIFY_RESPONSE, - "The project's Mastodon credentials were verified successfully.", - ), - 400: OpenApiResponse( - response=inline_serializer( - name="MastodonCredentialsVerifyErrorResponse", - fields={ - "type": serializers.CharField(), - "errors": inline_serializer( - name="MastodonCredentialsVerifyError", - fields={ - "code": serializers.CharField(), - "detail": serializers.CharField(), - "attr": serializers.CharField(allow_null=True), - }, - many=True, - ), - }, - ), - description="The project is missing Mastodon credentials or verification failed.", - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - ) - @action(detail=True, methods=["post"], url_path="verify-mastodon-credentials") - def verify_mastodon_credentials(self, request, *args, **kwargs): - """Verify the Mastodon credentials stored for the selected project.""" - - project = self.get_object() - try: - credentials = project.mastodon_credentials - except MastodonCredentials.DoesNotExist as exc: - raise serializers.ValidationError( - { - "mastodon_credentials": "No Mastodon credentials are configured for this project." - } - ) from exc - - try: - MastodonSourcePlugin.verify_credentials(credentials) - except Exception as exc: - logger.exception( - "Mastodon credential verification failed for project id=%s", - project.id, - ) - raise serializers.ValidationError( - { - "mastodon_credentials": ( - "Credential verification failed. Please re-check the credentials " - "and try again." - ) - } - ) from exc - - credentials.refresh_from_db() - return Response( - { - "status": "verified", - "account_acct": credentials.account_acct, - "instance_url": credentials.instance_url, - "last_verified_at": credentials.last_verified_at, - "last_error": "", - } - ) - - -@document_project_owned_viewset( - resource_plural="project configurations", - resource_singular="project configuration", - create_description="Create a new project configuration record for the selected project, including authority weighting and decay settings.", - tag="Project Management", - action_overrides=build_crud_action_overrides( - ProjectConfigSerializer, - resource_plural="project configurations for the selected project", - resource_singular="project configuration", - ), -) -class ProjectConfigViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage per-project scoring and authority configuration.""" - - serializer_class = ProjectConfigSerializer - queryset = ProjectConfig.objects.select_related("project") - - def get_permissions(self): - """Allow all members to read project config, but only admins to modify it.""" - - permission_classes: list[type[BasePermission]] - if self.action in { - "update", - "partial_update", - "create", - "destroy", - "recompute_authority", - }: - permission_classes = [IsProjectAdmin] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - @extend_schema( - summary="Recompute project authority scores", - description=( - "Recompute source-quality inputs and authority-score snapshots for the " - "selected project configuration. When Celery runs eagerly, the " - "recomputation finishes before the response is returned. Otherwise, the " - "source-quality and authority tasks are queued for background execution." - ), - request=None, - responses={ - 200: inline_serializer( - name="ProjectConfigRecomputeCompletedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - "config_id": serializers.IntegerField(), - }, - ), - 202: inline_serializer( - name="ProjectConfigRecomputeQueuedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - "config_id": serializers.IntegerField(), - }, - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Project Management"], - ) - @action(detail=True, methods=["post"], url_path="recompute_authority") - def recompute_authority(self, request, *args, **kwargs): - """Recompute source quality and entity authority for the selected project.""" - - from core.tasks import recompute_authority_scores, recompute_source_quality - - config = self.get_object() - project_id = int(config.project_id) - payload = { - "status": "queued", - "project_id": project_id, - "config_id": int(config.pk), - } - - if settings.CELERY_TASK_ALWAYS_EAGER: - recompute_source_quality(project_id) - recompute_authority_scores(project_id) - payload["status"] = "completed" - return Response(payload) - - recompute_source_quality.delay(project_id) - recompute_authority_scores.delay(project_id) - return Response(payload, status=status.HTTP_202_ACCEPTED) - - -@document_project_owned_viewset( - resource_plural="Bluesky credentials", - resource_singular="Bluesky credentials", - create_description=( - "Create Bluesky credentials for the selected project. The app password is " - "accepted write-only and is never returned in API responses." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - BlueskyCredentialsSerializer, - resource_plural="Bluesky credentials for the selected project", - resource_singular="Bluesky credentials", - retrieve_examples=[BLUESKY_CREDENTIALS_RESPONSE_EXAMPLE], - ), -) -class BlueskyCredentialsViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage project-scoped Bluesky credentials.""" - - serializer_class = BlueskyCredentialsSerializer - queryset = BlueskyCredentials.objects.select_related("project") - - def get_permissions(self): - """Restrict Bluesky credential access to project admins.""" - - return [IsProjectAdmin()] - - def get_queryset(self): - """Restrict credentials to the selected project and current user.""" - - return super().get_queryset().order_by("-updated_at") - - -@document_project_owned_viewset( - resource_plural="Mastodon credentials", - resource_singular="Mastodon credentials", - create_description=( - "Create Mastodon credentials for the selected project. The access token is " - "accepted write-only and is never returned in API responses." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - MastodonCredentialsSerializer, - resource_plural="Mastodon credentials for the selected project", - resource_singular="Mastodon credentials", - retrieve_examples=[MASTODON_CREDENTIALS_RESPONSE_EXAMPLE], - ), -) -class MastodonCredentialsViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage project-scoped Mastodon credentials.""" - - serializer_class = MastodonCredentialsSerializer - queryset = MastodonCredentials.objects.select_related("project") - - def get_permissions(self): - """Restrict Mastodon credential access to project admins.""" - - return [IsProjectAdmin()] - - def get_queryset(self): - """Restrict credentials to the selected project and current user.""" - - return super().get_queryset().order_by("-updated_at") - - -@document_project_owned_viewset( - resource_plural="LinkedIn credentials", - resource_singular="LinkedIn credentials", - create_description=( - "Create LinkedIn credentials for the selected project. The access token " - "and refresh token are accepted write-only and are never returned in API responses." - ), - tag="Ingestion", - action_overrides=build_crud_action_overrides( - LinkedInCredentialsSerializer, - resource_plural="LinkedIn credentials for the selected project", - resource_singular="LinkedIn credentials", - retrieve_examples=[LINKEDIN_CREDENTIALS_RESPONSE_EXAMPLE], - ), -) -class LinkedInCredentialsViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage project-scoped LinkedIn credentials.""" - - serializer_class = LinkedInCredentialsSerializer - queryset = LinkedInCredentials.objects.select_related("project") - - def get_permissions(self): - """Restrict LinkedIn credential access to project admins.""" - - return [IsProjectAdmin()] - - def get_queryset(self): - """Restrict credentials to the selected project and current user.""" - - return super().get_queryset().order_by("-updated_at") - - -@document_project_owned_viewset( - resource_plural="source configurations", - resource_singular="source configuration", - create_description="Create a new source configuration for the selected project. Plugin-specific configuration is validated before the record is saved.", - tag="Ingestion", - action_overrides=build_crud_action_overrides( - SourceConfigSerializer, - resource_plural="source configurations for the selected project", - resource_singular="source configuration", - create_examples=[ - SOURCE_CONFIG_CREATE_REQUEST_EXAMPLE, - SOURCE_CONFIG_REDDIT_REQUEST_EXAMPLE, - SOURCE_CONFIG_BLUESKY_REQUEST_EXAMPLE, - SOURCE_CONFIG_LINKEDIN_REQUEST_EXAMPLE, - SOURCE_CONFIG_MASTODON_REQUEST_EXAMPLE, - SOURCE_CONFIG_RESPONSE_EXAMPLE, - ], - create_response_examples=[SOURCE_CONFIG_RESPONSE_EXAMPLE], - retrieve_examples=[SOURCE_CONFIG_RESPONSE_EXAMPLE], - ), -) -class SourceConfigViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage source-plugin configuration for a project.""" - - serializer_class = SourceConfigSerializer - queryset = SourceConfig.objects.select_related("project") - - def get_permissions(self): - """Allow all members to read source configs, but only contributors to modify them.""" - - permission_classes: list[type[BasePermission]] - if self.action == "destroy": - permission_classes = [IsProjectMemberWritable] - elif self.action in {"create", "update", "partial_update"}: - permission_classes = [IsProjectMemberWritable] - else: - permission_classes = [IsProjectMember] - return [permission() for permission in permission_classes] - - -class ProjectMembershipViewSet(ProjectOwnedQuerysetMixin, viewsets.ModelViewSet): - """Manage the roster of members attached to one project.""" - - serializer_class = ProjectMembershipSerializer - queryset = ProjectMembership.objects.select_related("project", "user", "invited_by") - - def get_permissions(self): - """Restrict member-roster management to project admins.""" - - return [IsProjectAdmin()] - - def get_queryset(self): - """Return roster rows for the selected project ordered by join time.""" - - return super().get_queryset().order_by("joined_at", "user__username") - - def perform_update(self, serializer): - """Persist role changes without allowing the last admin to disappear.""" - - membership = self.get_object() - next_role = serializer.validated_data.get("role", membership.role) - _assert_project_keeps_admin(membership.project, membership, next_role=next_role) - serializer.save() - - def destroy(self, request, *args, **kwargs): - """Remove one membership while preserving at least one project admin.""" - - membership = self.get_object() - _assert_project_keeps_admin(membership.project, membership, next_role=None) - membership.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class ProjectInvitationViewSet( - ProjectOwnedQuerysetMixin, - mixins.CreateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet, -): - """Create, list, and revoke project invitation tokens.""" - - serializer_class = MembershipInvitationSerializer - queryset = MembershipInvitation.objects.select_related("project", "invited_by") - - def get_permissions(self): - """Restrict invitation management to project admins.""" - - return [IsProjectAdmin()] - - def get_queryset(self): - """Return invitations for the selected project ordered newest first.""" - - return super().get_queryset().order_by("-created_at") - - def get_serializer_context(self): - """Pass the selected project into invitation validation and URL generation.""" - - context = super().get_serializer_context() - context["project"] = self.get_project() - return context - - def perform_create(self, serializer): - """Persist the invitation row and dispatch its email.""" - - invitation = serializer.save( - project=self.get_project(), - invited_by=self.request.user, - ) - _send_membership_invitation_email(invitation) - - def destroy(self, request, *args, **kwargs): - """Mark one invitation as revoked instead of deleting it outright.""" - - invitation = self.get_object() - invitation.revoked_at = timezone.now() - invitation.save(update_fields=["revoked_at"]) - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/projects/api_urls.py b/projects/api_urls.py deleted file mode 100644 index a599263d..00000000 --- a/projects/api_urls.py +++ /dev/null @@ -1,53 +0,0 @@ -"""API route registration for project-owned resources.""" - -from rest_framework.routers import DefaultRouter -from rest_framework_nested.routers import NestedSimpleRouter - -from projects.api import ( - BlueskyCredentialsViewSet, - LinkedInCredentialsViewSet, - MastodonCredentialsViewSet, - ProjectConfigViewSet, - ProjectInvitationViewSet, - ProjectMembershipViewSet, - ProjectViewSet, - SourceConfigViewSet, -) - - -def register_root_routes(router: DefaultRouter) -> None: - """Register top-level project endpoints.""" - - router.register("projects", ProjectViewSet, basename="project") - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested project-management endpoints.""" - - project_router.register( - r"project-configs", ProjectConfigViewSet, basename="project-config" - ) - project_router.register( - r"memberships", ProjectMembershipViewSet, basename="project-membership" - ) - project_router.register( - r"invitations", ProjectInvitationViewSet, basename="project-invitation" - ) - project_router.register( - r"bluesky-credentials", - BlueskyCredentialsViewSet, - basename="project-bluesky-credentials", - ) - project_router.register( - r"mastodon-credentials", - MastodonCredentialsViewSet, - basename="project-mastodon-credentials", - ) - project_router.register( - r"linkedin-credentials", - LinkedInCredentialsViewSet, - basename="project-linkedin-credentials", - ) - project_router.register( - r"source-configs", SourceConfigViewSet, basename="project-source-config" - ) diff --git a/projects/ninja_api.py b/projects/ninja_api.py new file mode 100644 index 00000000..0a492c2e --- /dev/null +++ b/projects/ninja_api.py @@ -0,0 +1,469 @@ +"""Django Ninja endpoints for project-owned resources.""" + +from __future__ import annotations + +import datetime +import logging +from typing import Any + +from ninja import Router, Schema +from ninja.responses import Status + +from content.ninja_api import content_router, feedback_router +from core.ninja_api import api_authenticate +from core.permissions import get_user_role, get_visible_projects_queryset +from entities.ninja_api import entity_candidate_router, entity_router +from ingestion.ninja_api import router as ingestion_runs_router +from ingestion.plugins.bluesky import BlueskySourcePlugin +from ingestion.plugins.linkedin import LinkedInSourcePlugin +from ingestion.plugins.mastodon import MastodonSourcePlugin +from newsletters.ninja_api import ( + intake_allowlist_router, + newsletter_draft_items_router, + newsletter_draft_original_pieces_router, + newsletter_draft_sections_router, + newsletter_drafts_router, + newsletter_intakes_router, +) +from pipeline.ninja_api import router as pipeline_router +from projects.linkedin_oauth import build_linkedin_authorize_url +from projects.models import ( + BlueskyCredentials, + LinkedInCredentials, + MastodonCredentials, + Project, + ProjectMembership, + ProjectRole, + generate_project_intake_token, +) +from projects.ninja_credentials_api import ( + bluesky_router, + linkedin_router, + mastodon_router, +) +from projects.ninja_helpers import ( + _get_project_or_404, + _require_project_admin, +) +from projects.ninja_invitations_api import router as project_invitations_router +from projects.ninja_memberships_api import router as project_memberships_router +from projects.ninja_project_configs_api import router as project_configs_router +from projects.ninja_source_configs_api import router as source_configs_router +from trends.ninja_api import ( + clusters_router, + ideas_router, + source_diversity_snapshots_router, + themes_router, + topic_centroid_snapshots_router, + trend_task_runs_router, +) + +logger = logging.getLogger(__name__) + +router = Router(tags=["Project Management"]) + + +class ProjectSchema(Schema): + """Serialized project payload.""" + + id: int + name: str + topic_description: str + content_retention_days: int + intake_token: str + intake_enabled: bool + user_role: str | None = None + has_bluesky_credentials: bool + bluesky_handle: str | None = None + bluesky_is_active: bool + bluesky_last_verified_at: datetime.datetime | None = None + bluesky_last_error: str | None = None + created_at: datetime.datetime + + +class ProjectCreateInput(Schema): + """Editable fields accepted by the project create endpoint.""" + + name: str + topic_description: str + content_retention_days: int = 90 + intake_enabled: bool = True + + +class ProjectUpdateInput(Schema): + """Editable fields accepted by the project update endpoint.""" + + name: str | None = None + topic_description: str | None = None + content_retention_days: int | None = None + intake_enabled: bool | None = None + + +class BlueskyCredentialsVerifyResponse(Schema): + status: str + handle: str + last_verified_at: datetime.datetime | None + last_error: str + + +class LinkedInOAuthStartRequest(Schema): + redirect_to: str | None = None + + +class LinkedInOAuthStartResponse(Schema): + authorize_url: str + + +class LinkedInCredentialsVerifyResponse(Schema): + status: str + member_urn: str + expires_at: datetime.datetime | None + last_verified_at: datetime.datetime | None + last_error: str + + +class MastodonCredentialsVerifyResponse(Schema): + status: str + account_acct: str + instance_url: str + last_verified_at: datetime.datetime | None + last_error: str + + +class ValidationErrorSchema(Schema): + """Simple field-to-errors response payload.""" + + pass + + +def _serialize_project(project: Project, request: Any = None) -> dict[str, Any]: + """Return the project response body.""" + + credentials = BlueskyCredentials.objects.filter(project=project).first() + request_user = request.user if request is not None else None + return { + "id": int(project.pk), + "name": project.name, + "topic_description": project.topic_description, + "content_retention_days": project.content_retention_days, + "intake_token": project.intake_token, + "intake_enabled": project.intake_enabled, + "user_role": ( + get_user_role(request_user, project) + if request_user is not None and request_user.is_authenticated + else None + ), + "has_bluesky_credentials": credentials is not None, + "bluesky_handle": credentials.handle if credentials else "", + "bluesky_is_active": credentials.is_active if credentials else False, + "bluesky_last_verified_at": ( + credentials.last_verified_at if credentials else None + ), + "bluesky_last_error": credentials.last_error if credentials else "", + "created_at": project.created_at, + } + + +def _validate_project_payload(payload: dict[str, Any]) -> dict[str, list[str]] | None: + """Return one validation payload when project fields are invalid.""" + + errors: dict[str, list[str]] = {} + name = payload.get("name") + if name is not None and not str(name).strip(): + errors["name"] = ["This field may not be blank."] + topic_description = payload.get("topic_description") + if topic_description is not None and not str(topic_description).strip(): + errors["topic_description"] = ["This field may not be blank."] + content_retention_days = payload.get("content_retention_days") + if content_retention_days is not None and int(content_retention_days) < 0: + errors["content_retention_days"] = [ + "Ensure this value is greater than or equal to 0." + ] + return errors or None + + +def _project_update_fields(payload: dict[str, Any]) -> list[str]: + """Return the subset of project model fields changed by the payload.""" + + allowed_fields = { + "name", + "topic_description", + "content_retention_days", + "intake_enabled", + } + return [field_name for field_name in payload if field_name in allowed_fields] + + +def _credential_validation_error(field_name: str, message: str): + """Return one 400 response payload for credential verification failures.""" + + return Status(400, {field_name: [message]}) + + +@router.get("/projects/", response=list[ProjectSchema], auth=api_authenticate) +def list_projects(request): + """Return accessible projects for the authenticated user.""" + projects = get_visible_projects_queryset(request.user).select_related( + "bluesky_credentials" + ) + return [_serialize_project(project, request) for project in projects] + + +@router.post( + "/projects/", + response={201: ProjectSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_project(request, payload: ProjectCreateInput): + """Create a new project for the authenticated user.""" + + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validate_project_payload(validated_payload) + if errors is not None: + return Status(400, errors) + project = Project.objects.create(**validated_payload) + ProjectMembership.objects.create( + user=request.user, + project=project, + role=ProjectRole.ADMIN, + ) + return Status(201, _serialize_project(project, request)) + + +@router.get("/projects/{project_id}/", response=ProjectSchema, auth=api_authenticate) +def get_project(request, project_id: int): + """Return project details.""" + project = _get_project_or_404(request, project_id) + return _serialize_project(project, request) + + +@router.patch( + "/projects/{project_id}/", + response={200: ProjectSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_project(request, project_id: int, payload: ProjectUpdateInput): + """Update project details.""" + project = _require_project_admin(request, project_id) + + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validate_project_payload(validated_payload) + if errors is not None: + return Status(400, errors) + for field_name in _project_update_fields(validated_payload): + setattr(project, field_name, validated_payload[field_name]) + update_fields = _project_update_fields(validated_payload) + if update_fields: + project.save(update_fields=update_fields) + return _serialize_project(project, request) + + +@router.delete("/projects/{project_id}/", response={204: None}, auth=api_authenticate) +def delete_project(request, project_id: int): + """Delete a project.""" + project = _require_project_admin(request, project_id) + project.delete() + return Status(204) + + +@router.post( + "/projects/{project_id}/rotate-intake-token/", + response=ProjectSchema, + auth=api_authenticate, +) +def rotate_intake_token(request, project_id: int): + """Generate a fresh intake token for the selected project.""" + project = _require_project_admin(request, project_id) + project.intake_token = generate_project_intake_token() + project.save(update_fields=["intake_token"]) + return _serialize_project(project, request) + + +@router.post( + "/projects/{project_id}/verify-bluesky-credentials/", + response={200: BlueskyCredentialsVerifyResponse, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def verify_bluesky_credentials(request, project_id: int): + """Verify the Bluesky credentials stored for the selected project.""" + project = _require_project_admin(request, project_id) + try: + credentials = project.bluesky_credentials + except BlueskyCredentials.DoesNotExist: + return _credential_validation_error( + "bluesky_credentials", + "No Bluesky credentials are configured for this project.", + ) + + try: + BlueskySourcePlugin.verify_credentials(credentials) + except Exception: + logger.exception( + "Bluesky credential verification failed for project id=%s", project.id + ) + return _credential_validation_error( + "bluesky_credentials", + "Credential verification failed. Please re-check the credentials and try again.", + ) + + credentials.refresh_from_db() + return { + "status": "verified", + "handle": credentials.handle, + "last_verified_at": credentials.last_verified_at, + "last_error": credentials.last_error or "", + } + + +@router.post( + "/projects/{project_id}/start-linkedin-oauth/", + response=LinkedInOAuthStartResponse, + auth=api_authenticate, +) +def start_linkedin_oauth(request, project_id: int, payload: LinkedInOAuthStartRequest): + """Return the LinkedIn OAuth authorization URL for the selected project.""" + project = _require_project_admin(request, project_id) + authorize_url = build_linkedin_authorize_url( + project, + payload.redirect_to, + ) + return {"authorize_url": authorize_url} + + +@router.post( + "/projects/{project_id}/verify-linkedin-credentials/", + response={200: LinkedInCredentialsVerifyResponse, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def verify_linkedin_credentials(request, project_id: int): + """Verify the LinkedIn credentials stored for the selected project.""" + project = _require_project_admin(request, project_id) + try: + credentials = project.linkedin_credentials + except LinkedInCredentials.DoesNotExist: + return _credential_validation_error( + "linkedin_credentials", + "No LinkedIn credentials are configured for this project.", + ) + + try: + LinkedInSourcePlugin.verify_credentials(credentials) + except Exception: + logger.exception( + "LinkedIn credential verification failed for project id=%s", project.id + ) + return _credential_validation_error( + "linkedin_credentials", + "Credential verification failed. Please re-check the credentials and try again.", + ) + + credentials.refresh_from_db() + return { + "status": "verified", + "member_urn": credentials.member_urn, + "expires_at": credentials.expires_at, + "last_verified_at": credentials.last_verified_at, + "last_error": credentials.last_error or "", + } + + +@router.post( + "/projects/{project_id}/verify-mastodon-credentials/", + response={200: MastodonCredentialsVerifyResponse, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def verify_mastodon_credentials(request, project_id: int): + """Verify the Mastodon credentials stored for the selected project.""" + project = _require_project_admin(request, project_id) + try: + credentials = project.mastodon_credentials + except MastodonCredentials.DoesNotExist: + return _credential_validation_error( + "mastodon_credentials", + "No Mastodon credentials are configured for this project.", + ) + + try: + MastodonSourcePlugin.verify_credentials(credentials) + except Exception: + logger.exception( + "Mastodon credential verification failed for project id=%s", project.id + ) + return _credential_validation_error( + "mastodon_credentials", + "Credential verification failed. Please re-check the credentials and try again.", + ) + + credentials.refresh_from_db() + return { + "status": "verified", + "account_acct": credentials.account_acct, + "instance_url": credentials.instance_url, + "last_verified_at": credentials.last_verified_at, + "last_error": credentials.last_error or "", + } + + +__all__ = ["router"] + +# ============================================================================== +# Nested /projects/{project_id}/... Router Pattern +# ============================================================================== +# Ninja handles nested routes natively by injecting Path parameters (e.g. `project_id`) +# into sub-router endpoints. Modules owning nested project resources (like +# memberships or source_configs) should define their own Router instances. +# +# Pattern usage: +# ```python +# from ninja import Router +# from projects.ninja_helpers import _require_project_admin, _get_project_or_404 +# +# project_configs_router = Router(tags=["Project Configurations"]) +# +# @project_configs_router.get("/", auth=api_authenticate) +# def list_configs(request, project_id: int): +# project = _get_project_or_404(request, project_id) +# ... +# ``` +# +# Then attach it here to the main project_router: +# `router.add_router("/{project_id}/project-configs", project_configs_router)` + +router.add_router("/projects/{project_id}/project-configs", project_configs_router) +router.add_router("/projects/{project_id}/memberships", project_memberships_router) +router.add_router("/projects/{project_id}/invitations", project_invitations_router) +router.add_router("/projects/{project_id}/source-configs", source_configs_router) +router.add_router("/projects/{project_id}/bluesky-credentials", bluesky_router) +router.add_router("/projects/{project_id}/mastodon-credentials", mastodon_router) +router.add_router("/projects/{project_id}/linkedin-credentials", linkedin_router) +router.add_router("/projects/{project_id}/contents", content_router) +router.add_router("/projects/{project_id}/feedback", feedback_router) +router.add_router("/projects/{project_id}/ingestion-runs", ingestion_runs_router) +router.add_router("/projects/{project_id}/entities", entity_router) +router.add_router("/projects/{project_id}/entity-candidates", entity_candidate_router) +router.add_router("/projects/{project_id}/intake-allowlist", intake_allowlist_router) +router.add_router( + "/projects/{project_id}/newsletter-intakes", newsletter_intakes_router +) +router.add_router("/projects/{project_id}/drafts", newsletter_drafts_router) +router.add_router( + "/projects/{project_id}/draft-sections", newsletter_draft_sections_router +) +router.add_router("/projects/{project_id}/draft-items", newsletter_draft_items_router) +router.add_router( + "/projects/{project_id}/draft-original-pieces", + newsletter_draft_original_pieces_router, +) +router.add_router("/projects/{project_id}", pipeline_router) +router.add_router("/projects/{project_id}/clusters", clusters_router) +router.add_router("/projects/{project_id}/themes", themes_router) +router.add_router("/projects/{project_id}/ideas", ideas_router) +router.add_router( + "/projects/{project_id}/topic-centroid-snapshots", + topic_centroid_snapshots_router, +) +router.add_router( + "/projects/{project_id}/source-diversity-snapshots", + source_diversity_snapshots_router, +) +router.add_router("/projects/{project_id}/trend-task-runs", trend_task_runs_router) diff --git a/projects/ninja_api_pattern_test.py b/projects/ninja_api_pattern_test.py new file mode 100644 index 00000000..cc6100d9 --- /dev/null +++ b/projects/ninja_api_pattern_test.py @@ -0,0 +1,7 @@ +from ninja import Router + +project_router = Router() + +@project_router.get("/{project_id}/hello") +def hello(request, project_id: int): + return {"project_id": project_id} diff --git a/projects/ninja_credentials_api.py b/projects/ninja_credentials_api.py new file mode 100644 index 00000000..864e07ee --- /dev/null +++ b/projects/ninja_credentials_api.py @@ -0,0 +1,403 @@ +import datetime +from typing import Any + +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from projects.models import BlueskyCredentials, LinkedInCredentials, MastodonCredentials +from projects.ninja_helpers import _get_project_or_404, _require_project_admin + +bluesky_router = Router(tags=["Bluesky Credentials"]) + + +class BlueskyCredentialsSchema(Schema): + id: int + project: int + handle: str + pds_url: str + is_active: bool + has_stored_credential: bool + last_verified_at: datetime.datetime | str | None = None + last_error: str | None = None + created_at: datetime.datetime | str | None = None + updated_at: datetime.datetime | str | None = None + + +class BlueskyCredentialsCreateInput(Schema): + handle: str + pds_url: str = "https://bsky.social" + is_active: bool = True + app_password: str | None = None + + +class BlueskyCredentialsUpdateInput(Schema): + handle: str | None = None + pds_url: str | None = None + is_active: bool | None = None + app_password: str | None = None + + +def _serialize_bluesky(cred: BlueskyCredentials) -> dict[str, Any]: + return { + "id": int(cred.pk), + "project": cred.project_id, + "handle": cred.handle, + "pds_url": cred.pds_url, + "is_active": cred.is_active, + "has_stored_credential": cred.has_stored_credential(), + "last_verified_at": cred.last_verified_at, + "last_error": cred.last_error, + "created_at": cred.created_at, + "updated_at": cred.updated_at, + } + + +def _get_bluesky_or_404(project_id: int, cred_id: int) -> BlueskyCredentials: + cred = BlueskyCredentials.objects.filter(project_id=project_id, pk=cred_id).first() + if not cred: + raise HttpError(404, "Not found.") + return cred + + +@bluesky_router.get("/", response=list[BlueskyCredentialsSchema], auth=api_authenticate) +def list_bluesky(request, project_id: int = Path(...)): + _get_project_or_404(request, project_id) + creds = BlueskyCredentials.objects.filter(project_id=project_id).order_by( + "-updated_at" + ) + return [_serialize_bluesky(c) for c in creds] + + +@bluesky_router.post( + "/", + response={201: BlueskyCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_bluesky( + request, payload: BlueskyCredentialsCreateInput, project_id: int = Path(...) +): + project = _require_project_admin(request, project_id) + app_password = payload.app_password or "" + if not app_password: + return Status(400, {"app_password": ["A Bluesky app credential is required."]}) + cred = BlueskyCredentials.objects.create( + project=project, + handle=payload.handle, + pds_url=payload.pds_url, + is_active=payload.is_active, + ) + cred.set_app_password(app_password) + cred.save(update_fields=["app_password_encrypted", "updated_at"]) + return Status(201, _serialize_bluesky(cred)) + + +@bluesky_router.get( + "/{cred_id}/", response=BlueskyCredentialsSchema, auth=api_authenticate +) +def get_bluesky(request, project_id: int = Path(...), cred_id: int = Path(...)): + _get_project_or_404(request, project_id) + cred = _get_bluesky_or_404(project_id, cred_id) + return _serialize_bluesky(cred) + + +@bluesky_router.patch( + "/{cred_id}/", + response={200: BlueskyCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_bluesky( + request, + payload: BlueskyCredentialsUpdateInput, + project_id: int = Path(...), + cred_id: int = Path(...), +): + _require_project_admin(request, project_id) + cred = _get_bluesky_or_404(project_id, cred_id) + updates = payload.model_dump(exclude_unset=True, exclude_none=True) + if "handle" in updates: + cred.handle = updates["handle"] + if "pds_url" in updates: + cred.pds_url = updates["pds_url"] + if "is_active" in updates: + cred.is_active = updates["is_active"] + if "app_password" in updates and updates["app_password"]: + cred.set_app_password(updates["app_password"]) + cred.save() + return _serialize_bluesky(cred) + + +@bluesky_router.delete("/{cred_id}/", response={204: None}, auth=api_authenticate) +def delete_bluesky(request, project_id: int = Path(...), cred_id: int = Path(...)): + _require_project_admin(request, project_id) + cred = _get_bluesky_or_404(project_id, cred_id) + cred.delete() + return Status(204, None) + + +mastodon_router = Router(tags=["Mastodon Credentials"]) + + +class MastodonCredentialsSchema(Schema): + id: int + project: int + instance_url: str + account_acct: str + is_active: bool + has_stored_credential: bool + last_verified_at: datetime.datetime | str | None = None + last_error: str | None = None + created_at: datetime.datetime | str | None = None + updated_at: datetime.datetime | str | None = None + + +class MastodonCredentialsCreateInput(Schema): + instance_url: str + account_acct: str + is_active: bool = True + access_token: str | None = None + + +class MastodonCredentialsUpdateInput(Schema): + instance_url: str | None = None + account_acct: str | None = None + is_active: bool | None = None + access_token: str | None = None + + +def _serialize_mastodon(cred: MastodonCredentials) -> dict[str, Any]: + return { + "id": int(cred.pk), + "project": cred.project_id, + "instance_url": cred.instance_url, + "account_acct": cred.account_acct, + "is_active": cred.is_active, + "has_stored_credential": cred.has_stored_credential(), + "last_verified_at": cred.last_verified_at, + "last_error": cred.last_error, + "created_at": cred.created_at, + "updated_at": cred.updated_at, + } + + +def _get_mastodon_or_404(project_id: int, cred_id: int) -> MastodonCredentials: + cred = MastodonCredentials.objects.filter(project_id=project_id, pk=cred_id).first() + if not cred: + raise HttpError(404, "Not found.") + return cred + + +@mastodon_router.get( + "/", response=list[MastodonCredentialsSchema], auth=api_authenticate +) +def list_mastodon(request, project_id: int = Path(...)): + _get_project_or_404(request, project_id) + creds = MastodonCredentials.objects.filter(project_id=project_id).order_by( + "-updated_at" + ) + return [_serialize_mastodon(c) for c in creds] + + +@mastodon_router.post( + "/", + response={201: MastodonCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_mastodon( + request, payload: MastodonCredentialsCreateInput, project_id: int = Path(...) +): + project = _require_project_admin(request, project_id) + access_token = payload.access_token or "" + if not access_token: + return Status(400, {"access_token": ["A Mastodon access token is required."]}) + cred = MastodonCredentials.objects.create( + project=project, + instance_url=payload.instance_url, + account_acct=payload.account_acct, + is_active=payload.is_active, + ) + cred.set_access_token(access_token) + cred.save(update_fields=["access_token_encrypted", "updated_at"]) + return Status(201, _serialize_mastodon(cred)) + + +@mastodon_router.get( + "/{cred_id}/", response=MastodonCredentialsSchema, auth=api_authenticate +) +def get_mastodon(request, project_id: int = Path(...), cred_id: int = Path(...)): + _get_project_or_404(request, project_id) + cred = _get_mastodon_or_404(project_id, cred_id) + return _serialize_mastodon(cred) + + +@mastodon_router.patch( + "/{cred_id}/", + response={200: MastodonCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_mastodon( + request, + payload: MastodonCredentialsUpdateInput, + project_id: int = Path(...), + cred_id: int = Path(...), +): + _require_project_admin(request, project_id) + cred = _get_mastodon_or_404(project_id, cred_id) + updates = payload.model_dump(exclude_unset=True, exclude_none=True) + if "instance_url" in updates: + cred.instance_url = updates["instance_url"] + if "account_acct" in updates: + cred.account_acct = updates["account_acct"] + if "is_active" in updates: + cred.is_active = updates["is_active"] + if "access_token" in updates and updates["access_token"]: + cred.set_access_token(updates["access_token"]) + cred.save() + return _serialize_mastodon(cred) + + +@mastodon_router.delete("/{cred_id}/", response={204: None}, auth=api_authenticate) +def delete_mastodon(request, project_id: int = Path(...), cred_id: int = Path(...)): + _require_project_admin(request, project_id) + cred = _get_mastodon_or_404(project_id, cred_id) + cred.delete() + return Status(204, None) + + +linkedin_router = Router(tags=["LinkedIn Credentials"]) + + +class LinkedInCredentialsSchema(Schema): + id: int + project: int + member_urn: str + expires_at: datetime.datetime | str | None = None + is_active: bool + has_stored_credential: bool + last_verified_at: datetime.datetime | str | None = None + last_error: str | None = None + created_at: datetime.datetime | str | None = None + updated_at: datetime.datetime | str | None = None + + +class LinkedInCredentialsCreateInput(Schema): + member_urn: str + is_active: bool = True + access_token: str | None = None + refresh_token: str | None = None + + +class LinkedInCredentialsUpdateInput(Schema): + member_urn: str | None = None + is_active: bool | None = None + access_token: str | None = None + refresh_token: str | None = None + + +def _serialize_linkedin(cred: LinkedInCredentials) -> dict[str, Any]: + return { + "id": int(cred.pk), + "project": cred.project_id, + "member_urn": cred.member_urn, + "expires_at": cred.expires_at, + "is_active": cred.is_active, + "has_stored_credential": cred.has_stored_credential(), + "last_verified_at": cred.last_verified_at, + "last_error": cred.last_error, + "created_at": cred.created_at, + "updated_at": cred.updated_at, + } + + +def _get_linkedin_or_404(project_id: int, cred_id: int) -> LinkedInCredentials: + cred = LinkedInCredentials.objects.filter(project_id=project_id, pk=cred_id).first() + if not cred: + raise HttpError(404, "Not found.") + return cred + + +@linkedin_router.get( + "/", response=list[LinkedInCredentialsSchema], auth=api_authenticate +) +def list_linkedin(request, project_id: int = Path(...)): + _get_project_or_404(request, project_id) + creds = LinkedInCredentials.objects.filter(project_id=project_id).order_by( + "-updated_at" + ) + return [_serialize_linkedin(c) for c in creds] + + +@linkedin_router.post( + "/", + response={201: LinkedInCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_linkedin( + request, payload: LinkedInCredentialsCreateInput, project_id: int = Path(...) +): + project = _require_project_admin(request, project_id) + access_token = payload.access_token or "" + refresh_token = payload.refresh_token or "" + if not access_token: + return Status(400, {"access_token": ["A LinkedIn access token is required."]}) + if not refresh_token: + return Status(400, {"refresh_token": ["A LinkedIn refresh token is required."]}) + cred = LinkedInCredentials.objects.create( + project=project, + member_urn=payload.member_urn, + is_active=payload.is_active, + ) + cred.set_access_token(access_token) + cred.set_refresh_token(refresh_token) + cred.save( + update_fields=[ + "access_token_encrypted", + "refresh_token_encrypted", + "updated_at", + ] + ) + return Status(201, _serialize_linkedin(cred)) + + +@linkedin_router.get( + "/{cred_id}/", response=LinkedInCredentialsSchema, auth=api_authenticate +) +def get_linkedin(request, project_id: int = Path(...), cred_id: int = Path(...)): + _get_project_or_404(request, project_id) + cred = _get_linkedin_or_404(project_id, cred_id) + return _serialize_linkedin(cred) + + +@linkedin_router.patch( + "/{cred_id}/", + response={200: LinkedInCredentialsSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_linkedin( + request, + payload: LinkedInCredentialsUpdateInput, + project_id: int = Path(...), + cred_id: int = Path(...), +): + _require_project_admin(request, project_id) + cred = _get_linkedin_or_404(project_id, cred_id) + updates = payload.model_dump(exclude_unset=True, exclude_none=True) + if "member_urn" in updates: + cred.member_urn = updates["member_urn"] + if "is_active" in updates: + cred.is_active = updates["is_active"] + if "access_token" in updates and updates["access_token"]: + cred.set_access_token(updates["access_token"]) + if "refresh_token" in updates and updates["refresh_token"]: + cred.set_refresh_token(updates["refresh_token"]) + cred.save() + return _serialize_linkedin(cred) + + +@linkedin_router.delete("/{cred_id}/", response={204: None}, auth=api_authenticate) +def delete_linkedin(request, project_id: int = Path(...), cred_id: int = Path(...)): + _require_project_admin(request, project_id) + cred = _get_linkedin_or_404(project_id, cred_id) + cred.delete() + return Status(204, None) diff --git a/projects/ninja_helpers.py b/projects/ninja_helpers.py new file mode 100644 index 00000000..36190102 --- /dev/null +++ b/projects/ninja_helpers.py @@ -0,0 +1,55 @@ +"""Shared helpers for project-scoped Django Ninja routes.""" + +from __future__ import annotations + +from typing import Any + +from ninja.errors import HttpError + +from core.permissions import get_visible_projects_queryset +from projects.models import Project, ProjectMembership, ProjectRole + + +def _get_project_or_404(request: Any, project_id: int) -> Project: + """Load one project if the authenticated user has access.""" + + project = ( + get_visible_projects_queryset(request.user) + .filter(id=project_id) + .select_related("bluesky_credentials") + .first() + ) + if not project: + raise HttpError(404, "Not found.") + return project + + +def _require_project_writable(request: Any, project_id: int) -> Project: + """Load one project, requiring admin or member write access.""" + + project = _get_project_or_404(request, project_id) + membership = ProjectMembership.objects.filter( + project=project, user=request.user + ).first() + if not membership or membership.role not in {ProjectRole.ADMIN, ProjectRole.MEMBER}: + raise HttpError(403, "You do not have permission to perform this action.") + return project + + +def _require_project_admin(request: Any, project_id: int) -> Project: + """Load one project, requiring admin access.""" + + project = _get_project_or_404(request, project_id) + membership = ProjectMembership.objects.filter( + project=project, user=request.user + ).first() + if not membership or membership.role != ProjectRole.ADMIN: + raise HttpError(403, "You do not have permission to perform this action.") + return project + + +__all__ = [ + "_get_project_or_404", + "_require_project_admin", + "_require_project_writable", +] diff --git a/projects/ninja_invitations_api.py b/projects/ninja_invitations_api.py new file mode 100644 index 00000000..4349d1ae --- /dev/null +++ b/projects/ninja_invitations_api.py @@ -0,0 +1,191 @@ +import datetime +from typing import Any + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.mail import send_mail +from django.utils import timezone +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from projects.models import ProjectRole +from projects.ninja_helpers import _require_project_admin +from users.models import MembershipInvitation + +router = Router(tags=["Project Invitations"]) + + +class MembershipInvitationSchema(Schema): + id: int + project: int + email: str + role: str + token: str + invited_by: int | None = None + invited_by_email: str + invite_url: str + created_at: datetime.datetime | str + accepted_at: datetime.datetime | str | None = None + revoked_at: datetime.datetime | str | None = None + + +class MembershipInvitationCreateInput(Schema): + email: str + role: str + + +def _serialize_invitation(invitation: MembershipInvitation, project) -> dict[str, Any]: + return { + "id": int(invitation.pk), + "project": project.id, + "email": invitation.email, + "role": invitation.role, + "token": invitation.token, + "invited_by": invitation.invited_by_id, + "invited_by_email": ( + invitation.invited_by.email if invitation.invited_by else "" + ), + "invite_url": f"{settings.FRONTEND_BASE_URL.rstrip('/')}/invite/{invitation.token}", + "created_at": invitation.created_at, + "accepted_at": invitation.accepted_at, + "revoked_at": invitation.revoked_at, + } + + +def _error_payload(field: str, message: str) -> dict[str, list[str]]: + """Return the native Ninja validation payload shape.""" + + return {field: [message]} + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _validated_invitation_payload( + payload: dict[str, Any], + *, + project, +) -> tuple[dict[str, Any], dict[str, list[str]] | None]: + """Normalize and validate an invitation payload for one project.""" + + normalized_payload = dict(payload) + normalized_payload["email"] = ( + str(normalized_payload.get("email", "")).strip().lower() + ) + + if normalized_payload.get("role") not in ProjectRole.values: + return normalized_payload, _error_payload( + "role", "Select a valid project role." + ) + + invitation = MembershipInvitation(project=project, **normalized_payload) + try: + invitation.full_clean( + exclude=["token", "invited_by", "accepted_at", "revoked_at"] + ) + except ValidationError as exc: + return normalized_payload, _validation_error_payload(exc) + + email = normalized_payload["email"] + if project.memberships.filter(user__email__iexact=email).exists(): + return normalized_payload, _error_payload( + "email", "That user is already a project member." + ) + + active_invitation_exists = MembershipInvitation.objects.filter( + project=project, + email__iexact=email, + accepted_at__isnull=True, + revoked_at__isnull=True, + ).exists() + if active_invitation_exists: + return normalized_payload, _error_payload( + "email", "An active invitation already exists for this email." + ) + + return normalized_payload, None + + +def _send_membership_invitation_email(invitation: MembershipInvitation) -> None: + """Send the one-time membership invitation email.""" + + invite_url = f"{settings.FRONTEND_BASE_URL.rstrip('/')}/invite/{invitation.token}" + send_mail( + subject=f"You're invited to join {invitation.project.name}", + message=( + f"You have been invited to join {invitation.project.name} as a " + f"{invitation.role}.\n\nOpen this link to accept the invitation:\n{invite_url}" + ), + from_email=settings.DEFAULT_FROM_EMAIL, + recipient_list=[invitation.email], + fail_silently=False, + ) + + +def _get_invitation_or_404(project_id: int, invitation_id: int) -> MembershipInvitation: + invitation = MembershipInvitation.objects.filter( + project_id=project_id, pk=invitation_id + ).first() + if not invitation: + raise HttpError(404, "Not found.") + return invitation + + +@router.get("/", response=list[MembershipInvitationSchema], auth=api_authenticate) +def list_invitations(request, project_id: int = Path(...)): + project = _require_project_admin(request, project_id) + invitations = ( + MembershipInvitation.objects.select_related("project", "invited_by") + .filter(project_id=project_id) + .order_by("-created_at") + ) + return [_serialize_invitation(inv, project) for inv in invitations] + + +@router.post( + "/", + response={201: MembershipInvitationSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_invitation( + request, payload: MembershipInvitationCreateInput, project_id: int = Path(...) +): + project = _require_project_admin(request, project_id) + + validated_payload, errors = _validated_invitation_payload( + payload.model_dump(exclude_unset=True), + project=project, + ) + if errors is not None: + return Status(400, errors) + + invitation = MembershipInvitation.objects.create( + project=project, + invited_by=request.user, + **validated_payload, + ) + _send_membership_invitation_email(invitation) + + return Status(201, _serialize_invitation(invitation, project)) + + +@router.delete("/{invitation_id}/", response={204: None}, auth=api_authenticate) +def revoke_invitation( + request, project_id: int = Path(...), invitation_id: int = Path(...) +): + _require_project_admin(request, project_id) + invitation = _get_invitation_or_404(project_id, invitation_id) + + invitation.revoked_at = timezone.now() + invitation.save(update_fields=["revoked_at"]) + return Status(204, None) diff --git a/projects/ninja_memberships_api.py b/projects/ninja_memberships_api.py new file mode 100644 index 00000000..28b9efd3 --- /dev/null +++ b/projects/ninja_memberships_api.py @@ -0,0 +1,147 @@ +import datetime +from typing import Any + +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from projects.models import ProjectMembership, ProjectRole +from projects.ninja_helpers import _require_project_admin + +router = Router(tags=["Project Memberships"]) + + +class ProjectMembershipSchema(Schema): + id: int + project: int + user: int + username: str + email: str + display_name: str + role: str + invited_by: int | None = None + joined_at: datetime.datetime | str + + +class ProjectMembershipUpdateInput(Schema): + role: str + + +def _serialize_membership(membership: ProjectMembership) -> dict[str, Any]: + return { + "id": int(membership.pk), + "project": membership.project_id, + "user": membership.user_id, + "username": membership.user.username, + "email": membership.user.email, + "display_name": membership.user.display_name, + "role": membership.role, + "invited_by": membership.invited_by_id, + "joined_at": membership.joined_at, + } + + +def _role_error(message: str) -> dict[str, list[str]]: + """Return the native Ninja error payload shape for role validation failures.""" + + return {"role": [message]} + + +def _validate_role(role: str) -> dict[str, list[str]] | None: + """Restrict membership role updates to the supported project roles.""" + + if role not in ProjectRole.values: + return _role_error("Select a valid project role.") + return None + + +def _validate_project_keeps_admin( + membership: ProjectMembership, + *, + next_role: str | None = None, +) -> dict[str, list[str]] | None: + """Reject changes that would leave the project without an admin.""" + + resulting_role = next_role + if membership.role != ProjectRole.ADMIN or resulting_role == ProjectRole.ADMIN: + return None + + has_other_admin = ( + ProjectMembership.objects.filter(project=membership.project) + .exclude(pk=membership.pk) + .filter(role=ProjectRole.ADMIN) + .exists() + ) + if not has_other_admin: + return _role_error("Projects must keep at least one admin.") + return None + + +def _get_membership_or_404(project_id: int, membership_id: int) -> ProjectMembership: + membership = ( + ProjectMembership.objects.select_related("project", "user", "invited_by") + .filter(project_id=project_id, pk=membership_id) + .first() + ) + if not membership: + raise HttpError(404, "Not found.") + return membership + + +@router.get("/", response=list[ProjectMembershipSchema], auth=api_authenticate) +def list_memberships(request, project_id: int = Path(...)): + _require_project_admin(request, project_id) + # The original query uses select_related and orders by joined_at, user__username + memberships = ( + ProjectMembership.objects.select_related("project", "user", "invited_by") + .filter(project_id=project_id) + .order_by("joined_at", "user__username") + ) + return [_serialize_membership(m) for m in memberships] + + +@router.patch( + "/{membership_id}/", + response={200: ProjectMembershipSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_membership( + request, + payload: ProjectMembershipUpdateInput, + project_id: int = Path(...), + membership_id: int = Path(...), +): + _require_project_admin(request, project_id) + membership = _get_membership_or_404(project_id, membership_id) + + role_errors = _validate_role(payload.role) + if role_errors is not None: + return Status(400, role_errors) + + admin_errors = _validate_project_keeps_admin(membership, next_role=payload.role) + if admin_errors is not None: + return Status(400, admin_errors) + + membership.role = payload.role + membership.save(update_fields=["role"]) + return _serialize_membership(membership) + + +@router.delete( + "/{membership_id}/", + response={204: None, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def delete_membership( + request, project_id: int = Path(...), membership_id: int = Path(...) +): + _require_project_admin(request, project_id) + membership = _get_membership_or_404(project_id, membership_id) + + admin_errors = _validate_project_keeps_admin(membership, next_role=None) + if admin_errors is not None: + return Status(400, admin_errors) + + membership.delete() + return Status(204, None) diff --git a/projects/ninja_project_configs_api.py b/projects/ninja_project_configs_api.py new file mode 100644 index 00000000..83c9ea3d --- /dev/null +++ b/projects/ninja_project_configs_api.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +from typing import Any + +from django.core.exceptions import ValidationError +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.cron import validate_cron_expression +from core.ninja_api import api_authenticate +from digest_engine.taskiq import enqueue_task, task_always_eager +from projects.models import ProjectConfig +from projects.ninja_helpers import _get_project_or_404, _require_project_admin + +router = Router(tags=["Project Configurations"]) + + +class ProjectConfigSchema(Schema): + """Serialized project configuration payload.""" + + id: int + project: int + draft_schedule_cron: str | None = None + authority_weight_mention: float + authority_weight_engagement: float + authority_weight_recency: float + authority_weight_source_quality: float + authority_weight_cross_newsletter: float + authority_weight_feedback: float + authority_weight_duplicate: float + upvote_authority_weight: float + downvote_authority_weight: float + authority_decay_rate: float + + +class ProjectConfigCreateInput(Schema): + draft_schedule_cron: str | None = None + authority_weight_mention: float | None = None + authority_weight_engagement: float | None = None + authority_weight_recency: float | None = None + authority_weight_source_quality: float | None = None + authority_weight_cross_newsletter: float | None = None + authority_weight_feedback: float | None = None + authority_weight_duplicate: float | None = None + upvote_authority_weight: float | None = None + downvote_authority_weight: float | None = None + authority_decay_rate: float | None = None + + +class ProjectConfigUpdateInput(ProjectConfigCreateInput): + pass + + +class ProjectConfigRecomputeResponse(Schema): + status: str + project_id: int + config_id: int + + +def _serialize_config(config: ProjectConfig) -> dict[str, Any]: + return { + "id": int(config.pk), + "project": config.project_id, + "draft_schedule_cron": config.draft_schedule_cron, + "authority_weight_mention": config.authority_weight_mention, + "authority_weight_engagement": config.authority_weight_engagement, + "authority_weight_recency": config.authority_weight_recency, + "authority_weight_source_quality": config.authority_weight_source_quality, + "authority_weight_cross_newsletter": config.authority_weight_cross_newsletter, + "authority_weight_feedback": config.authority_weight_feedback, + "authority_weight_duplicate": config.authority_weight_duplicate, + "upvote_authority_weight": config.upvote_authority_weight, + "downvote_authority_weight": config.downvote_authority_weight, + "authority_decay_rate": config.authority_decay_rate, + } + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _validated_project_config_payload( + payload: dict[str, Any], + *, + project_id: int, + instance: ProjectConfig | None = None, +) -> dict[str, list[str]] | None: + """Normalize and validate one project-config payload.""" + + if "draft_schedule_cron" in payload and payload["draft_schedule_cron"] is not None: + normalized = " ".join(str(payload["draft_schedule_cron"]).split()) + if normalized: + try: + normalized = validate_cron_expression(normalized) + except ValueError: + return { + "draft_schedule_cron": ["Enter a valid 5-part cron expression."] + } + payload["draft_schedule_cron"] = normalized + + config = instance or ProjectConfig(project_id=project_id) + for field_name, value in payload.items(): + setattr(config, field_name, value) + try: + config.full_clean() + except ValidationError as exc: + return _validation_error_payload(exc) + return None + + +def _get_config_or_404(project_id: int, config_id: int) -> ProjectConfig: + config = ProjectConfig.objects.filter(project_id=project_id, pk=config_id).first() + if not config: + raise HttpError(404, "Not found.") + return config + + +@router.get("/", response=list[ProjectConfigSchema], auth=api_authenticate) +def list_configs(request, project_id: int = Path(...)): + _get_project_or_404(request, project_id) + configs = ProjectConfig.objects.filter(project_id=project_id).select_related( + "project" + ) + return [_serialize_config(c) for c in configs] + + +@router.post( + "/", + response={201: ProjectConfigSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_config( + request, payload: ProjectConfigCreateInput, project_id: int = Path(...) +): + project = _require_project_admin(request, project_id) + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validated_project_config_payload( + validated_payload, + project_id=project.id, + ) + if errors is not None: + return Status(400, errors) + config = ProjectConfig.objects.create(project=project, **validated_payload) + return Status(201, _serialize_config(config)) + + +@router.get("/{config_id}/", response=ProjectConfigSchema, auth=api_authenticate) +def get_config(request, project_id: int = Path(...), config_id: int = Path(...)): + _get_project_or_404(request, project_id) + config = _get_config_or_404(project_id, config_id) + return _serialize_config(config) + + +@router.patch( + "/{config_id}/", + response={200: ProjectConfigSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_config( + request, + payload: ProjectConfigUpdateInput, + project_id: int = Path(...), + config_id: int = Path(...), +): + _require_project_admin(request, project_id) + config = _get_config_or_404(project_id, config_id) + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validated_project_config_payload( + validated_payload, + project_id=project_id, + instance=config, + ) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(config, field_name, value) + config.save() + return _serialize_config(config) + + +@router.delete("/{config_id}/", response={204: None}, auth=api_authenticate) +def delete_config(request, project_id: int = Path(...), config_id: int = Path(...)): + _require_project_admin(request, project_id) + config = _get_config_or_404(project_id, config_id) + config.delete() + return Status(204, None) + + +@router.post( + "/{config_id}/recompute_authority/", + response={200: ProjectConfigRecomputeResponse, 202: ProjectConfigRecomputeResponse}, + auth=api_authenticate, +) +def recompute_authority( + request, project_id: int = Path(...), config_id: int = Path(...) +): + # This requires admin in DRF + _require_project_admin(request, project_id) + config = _get_config_or_404(project_id, config_id) + + from core.tasks import recompute_authority_scores, recompute_source_quality + + payload = { + "status": "queued", + "project_id": project_id, + "config_id": config.pk, + } + + if task_always_eager(): + recompute_source_quality(project_id) + recompute_authority_scores(project_id) + payload["status"] = "completed" + return Status(200, payload) + + enqueue_task(recompute_source_quality, project_id) + enqueue_task(recompute_authority_scores, project_id) + return Status(202, payload) diff --git a/projects/ninja_source_configs_api.py b/projects/ninja_source_configs_api.py new file mode 100644 index 00000000..e24a447d --- /dev/null +++ b/projects/ninja_source_configs_api.py @@ -0,0 +1,140 @@ +import datetime +from typing import Any + +from ninja import Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from ingestion.plugins import validate_plugin_config +from projects.models import SourceConfig +from projects.ninja_helpers import _get_project_or_404, _require_project_writable + +router = Router(tags=["Source Configurations"]) + + +class SourceConfigSchema(Schema): + id: int + project: int + plugin_name: str + config: dict[str, Any] + is_active: bool + last_fetched_at: datetime.datetime | str | None = None + + +class SourceConfigCreateInput(Schema): + plugin_name: str + config: dict[str, Any] | None = None + is_active: bool | None = None + + +class SourceConfigUpdateInput(Schema): + plugin_name: str | None = None + config: dict[str, Any] | None = None + is_active: bool | None = None + + +def _serialize_config(config: SourceConfig) -> dict[str, Any]: + return { + "id": int(config.pk), + "project": config.project_id, + "plugin_name": config.plugin_name, + "config": config.config, + "is_active": config.is_active, + "last_fetched_at": config.last_fetched_at, + } + + +def _validated_source_config_payload( + payload: dict[str, Any], + *, + instance: SourceConfig | None = None, +) -> dict[str, Any] | None: + """Validate and normalize one source-config payload. + + Returns a 400 payload shape when plugin validation fails. + """ + + plugin_name = payload.get("plugin_name") or getattr(instance, "plugin_name", None) + config = payload.get("config") + if config is None and instance is not None: + config = instance.config + if plugin_name: + try: + payload["config"] = validate_plugin_config(plugin_name, config or {}) + except ValueError: + return {"config": ["Invalid source configuration."]} + return None + + +def _get_source_config_or_404(project_id: int, config_id: int) -> SourceConfig: + config = SourceConfig.objects.filter(project_id=project_id, pk=config_id).first() + if not config: + raise HttpError(404, "Not found.") + return config + + +@router.get("/", response=list[SourceConfigSchema], auth=api_authenticate) +def list_source_configs(request, project_id: int = Path(...)): + _get_project_or_404(request, project_id) + configs = SourceConfig.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_config(c) for c in configs] + + +@router.post( + "/", + response={201: SourceConfigSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def create_source_config( + request, payload: SourceConfigCreateInput, project_id: int = Path(...) +): + project = _require_project_writable(request, project_id) + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validated_source_config_payload(validated_payload) + if errors is not None: + return Status(400, errors) + config = SourceConfig.objects.create(project=project, **validated_payload) + return Status(201, _serialize_config(config)) + + +@router.get("/{config_id}/", response=SourceConfigSchema, auth=api_authenticate) +def get_source_config(request, project_id: int = Path(...), config_id: int = Path(...)): + _get_project_or_404(request, project_id) + config = _get_source_config_or_404(project_id, config_id) + return _serialize_config(config) + + +@router.patch( + "/{config_id}/", + response={200: SourceConfigSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def update_source_config( + request, + payload: SourceConfigUpdateInput, + project_id: int = Path(...), + config_id: int = Path(...), +): + _require_project_writable(request, project_id) + config = _get_source_config_or_404(project_id, config_id) + validated_payload = payload.model_dump(exclude_unset=True) + errors = _validated_source_config_payload(validated_payload, instance=config) + if errors is not None: + return Status(400, errors) + for field_name, value in validated_payload.items(): + setattr(config, field_name, value) + config.save() + return _serialize_config(config) + + +@router.delete("/{config_id}/", response={204: None}, auth=api_authenticate) +def delete_source_config( + request, project_id: int = Path(...), config_id: int = Path(...) +): + _require_project_writable(request, project_id) + config = _get_source_config_or_404(project_id, config_id) + config.delete() + return Status(204, None) diff --git a/projects/serializers.py b/projects/serializers.py deleted file mode 100644 index 6dba8b7d..00000000 --- a/projects/serializers.py +++ /dev/null @@ -1,451 +0,0 @@ -"""DRF serializers for project-owned models.""" - -import logging - -from celery.schedules import crontab -from rest_framework import serializers - -from core.permissions import get_user_role -from core.serializer_mixins import ProjectScopedSerializerMixin -from ingestion.plugins import validate_plugin_config -from projects.models import ( - BlueskyCredentials, - LinkedInCredentials, - MastodonCredentials, - Project, - ProjectConfig, - ProjectMembership, - ProjectRole, - SourceConfig, -) - -logger = logging.getLogger(__name__) - - -class ProjectSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize top-level project records.""" - - user_role = serializers.SerializerMethodField() - has_bluesky_credentials = serializers.SerializerMethodField() - bluesky_handle = serializers.SerializerMethodField() - bluesky_is_active = serializers.SerializerMethodField() - bluesky_last_verified_at = serializers.SerializerMethodField() - bluesky_last_error = serializers.SerializerMethodField() - - class Meta: - model = Project - fields = [ - "id", - "name", - "topic_description", - "content_retention_days", - "intake_token", - "intake_enabled", - "user_role", - "has_bluesky_credentials", - "bluesky_handle", - "bluesky_is_active", - "bluesky_last_verified_at", - "bluesky_last_error", - "created_at", - ] - read_only_fields = ["id", "created_at"] - - def get_user_role(self, obj: Project) -> str | None: - """Return the current request user's role for this project.""" - - request = self.context.get("request") - if not request or not request.user.is_authenticated: - return None - return get_user_role(request.user, obj) - - def _get_bluesky_credentials(self, obj: Project) -> BlueskyCredentials | None: - """Return the project's stored Bluesky credentials, if configured.""" - - return BlueskyCredentials.objects.filter(project=obj).first() - - def get_has_bluesky_credentials(self, obj: Project) -> bool: - """Return whether the project has stored Bluesky credentials.""" - - return self._get_bluesky_credentials(obj) is not None - - def get_bluesky_handle(self, obj: Project) -> str: - """Return the stored Bluesky handle, or an empty string.""" - - credentials = self._get_bluesky_credentials(obj) - return credentials.handle if credentials else "" - - def get_bluesky_is_active(self, obj: Project) -> bool: - """Return whether the stored Bluesky credentials are currently active.""" - - credentials = self._get_bluesky_credentials(obj) - return credentials.is_active if credentials else False - - def get_bluesky_last_verified_at(self, obj: Project): - """Return the last successful verification timestamp, if available.""" - - credentials = self._get_bluesky_credentials(obj) - return credentials.last_verified_at if credentials else None - - def get_bluesky_last_error(self, obj: Project) -> str: - """Return the latest Bluesky verification error, or an empty string.""" - - credentials = self._get_bluesky_credentials(obj) - return credentials.last_error if credentials else "" - - -class ProjectConfigSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize per-project authority and scoring settings.""" - - def validate_draft_schedule_cron(self, value: str) -> str: - """Normalize and validate the optional draft-generation cron string.""" - - normalized = " ".join(value.split()) - if not normalized: - return "" - try: - crontab.from_string(normalized) - except Exception as exc: - raise serializers.ValidationError( - "Enter a valid 5-part cron expression." - ) from exc - return normalized - - class Meta: - model = ProjectConfig - fields = [ - "id", - "project", - "draft_schedule_cron", - "authority_weight_mention", - "authority_weight_engagement", - "authority_weight_recency", - "authority_weight_source_quality", - "authority_weight_cross_newsletter", - "authority_weight_feedback", - "authority_weight_duplicate", - "upvote_authority_weight", - "downvote_authority_weight", - "authority_decay_rate", - ] - read_only_fields = ["id", "project"] - - -class BlueskyCredentialsSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize project-scoped Bluesky credentials without exposing secrets.""" - - app_password = serializers.CharField( - write_only=True, - required=False, - allow_blank=True, - trim_whitespace=False, - ) - has_stored_credential = serializers.SerializerMethodField() - - class Meta: - model = BlueskyCredentials - fields = [ - "id", - "project", - "handle", - "pds_url", - "is_active", - "has_stored_credential", - "app_password", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - read_only_fields = [ - "id", - "project", - "has_stored_credential", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - - def get_has_stored_credential(self, obj: BlueskyCredentials) -> bool: - """Return whether the project has an encrypted Bluesky credential stored.""" - - return obj.has_stored_credential() - - def validate(self, attrs): - """Require an app password when creating a credential record.""" - - attrs = super().validate(attrs) - app_password = attrs.get("app_password", "") - if self.instance is None and not app_password: - raise serializers.ValidationError( - {"app_password": "A Bluesky app credential is required."} - ) - return attrs - - def create(self, validated_data): - """Encrypt the submitted Bluesky app password before saving the record.""" - - app_password = validated_data.pop("app_password", "") - instance = super().create(validated_data) - if app_password: - instance.set_app_password(app_password) - instance.save(update_fields=["app_password_encrypted", "updated_at"]) - return instance - - -class MastodonCredentialsSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize project-scoped Mastodon credentials without exposing secrets.""" - - access_token = serializers.CharField( - write_only=True, - required=False, - allow_blank=True, - trim_whitespace=False, - ) - has_stored_credential = serializers.SerializerMethodField() - - class Meta: - model = MastodonCredentials - fields = [ - "id", - "project", - "instance_url", - "account_acct", - "is_active", - "has_stored_credential", - "access_token", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - read_only_fields = [ - "id", - "project", - "has_stored_credential", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - - def get_has_stored_credential(self, obj: MastodonCredentials) -> bool: - """Return whether the project has an encrypted Mastodon token stored.""" - - return obj.has_stored_credential() - - def validate(self, attrs): - """Require an access token when creating a credential record.""" - - attrs = super().validate(attrs) - access_token = attrs.get("access_token", "") - if self.instance is None and not access_token: - raise serializers.ValidationError( - {"access_token": "A Mastodon access token is required."} - ) - return attrs - - def create(self, validated_data): - """Encrypt the submitted Mastodon access token before saving the record.""" - - access_token = validated_data.pop("access_token", "") - instance = super().create(validated_data) - if access_token: - instance.set_access_token(access_token) - instance.save(update_fields=["access_token_encrypted", "updated_at"]) - return instance - - def update(self, instance, validated_data): - """Keep the stored token unless a replacement access token is submitted.""" - - access_token = validated_data.pop("access_token", "") - instance = super().update(instance, validated_data) - if access_token: - instance.set_access_token(access_token) - instance.save(update_fields=["access_token_encrypted", "updated_at"]) - return instance - - -class LinkedInCredentialsSerializer( - ProjectScopedSerializerMixin, serializers.ModelSerializer -): - """Serialize project-scoped LinkedIn credentials without exposing tokens.""" - - access_token = serializers.CharField( - write_only=True, - required=False, - allow_blank=True, - trim_whitespace=False, - ) - refresh_token = serializers.CharField( - write_only=True, - required=False, - allow_blank=True, - trim_whitespace=False, - ) - has_stored_credential = serializers.SerializerMethodField() - - class Meta: - model = LinkedInCredentials - fields = [ - "id", - "project", - "member_urn", - "expires_at", - "is_active", - "has_stored_credential", - "access_token", - "refresh_token", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - read_only_fields = [ - "id", - "project", - "has_stored_credential", - "last_verified_at", - "last_error", - "created_at", - "updated_at", - ] - - def get_has_stored_credential(self, obj: LinkedInCredentials) -> bool: - """Return whether the project has both encrypted LinkedIn tokens stored.""" - - return obj.has_stored_credential() - - def validate(self, attrs): - """Require both OAuth tokens when creating a LinkedIn credential record.""" - - attrs = super().validate(attrs) - access_token = attrs.get("access_token", "") - refresh_token = attrs.get("refresh_token", "") - if self.instance is None: - if not access_token: - raise serializers.ValidationError( - {"access_token": "A LinkedIn access token is required."} - ) - if not refresh_token: - raise serializers.ValidationError( - {"refresh_token": "A LinkedIn refresh token is required."} - ) - return attrs - - def create(self, validated_data): - """Encrypt submitted LinkedIn OAuth tokens before saving the record.""" - - access_token = validated_data.pop("access_token", "") - refresh_token = validated_data.pop("refresh_token", "") - instance = super().create(validated_data) - update_fields = ["updated_at"] - if access_token: - instance.set_access_token(access_token) - update_fields.append("access_token_encrypted") - if refresh_token: - instance.set_refresh_token(refresh_token) - update_fields.append("refresh_token_encrypted") - if len(update_fields) > 1: - instance.save(update_fields=update_fields) - return instance - - def update(self, instance, validated_data): - """Keep stored LinkedIn tokens unless replacements are submitted.""" - - access_token = validated_data.pop("access_token", "") - refresh_token = validated_data.pop("refresh_token", "") - instance = super().update(instance, validated_data) - update_fields = ["updated_at"] - if access_token: - instance.set_access_token(access_token) - update_fields.append("access_token_encrypted") - if refresh_token: - instance.set_refresh_token(refresh_token) - update_fields.append("refresh_token_encrypted") - if len(update_fields) > 1: - instance.save(update_fields=update_fields) - return instance - - -class SourceConfigSerializer(ProjectScopedSerializerMixin, serializers.ModelSerializer): - """Serialize source-plugin configuration and normalize provider settings.""" - - class Meta: - model = SourceConfig - fields = [ - "id", - "project", - "plugin_name", - "config", - "is_active", - "last_fetched_at", - ] - read_only_fields = ["id", "project", "last_fetched_at"] - - def validate(self, attrs): - """Validate plugin-specific configuration with the plugin registry.""" - - plugin_name = attrs.get("plugin_name") or getattr( - self.instance, "plugin_name", None - ) - config = attrs.get("config") or getattr(self.instance, "config", {}) - if plugin_name: - try: - attrs["config"] = validate_plugin_config(plugin_name, config) - except ValueError as exc: - logger.warning( - "Rejected invalid source config", - extra={"plugin_name": str(plugin_name)}, - exc_info=exc, - ) - raise serializers.ValidationError( - {"config": "Invalid source configuration."} - ) - return attrs - - -class ProjectMembershipSerializer(serializers.ModelSerializer): - """Serialize project-member roster entries for admin workflows.""" - - username = serializers.CharField(source="user.username", read_only=True) - email = serializers.CharField(source="user.email", read_only=True) - display_name = serializers.CharField(source="user.display_name", read_only=True) - - class Meta: - model = ProjectMembership - fields = [ - "id", - "project", - "user", - "username", - "email", - "display_name", - "role", - "invited_by", - "joined_at", - ] - read_only_fields = [ - "id", - "project", - "user", - "username", - "email", - "display_name", - "invited_by", - "joined_at", - ] - - def validate_role(self, value: str) -> str: - """Restrict role updates to the supported project-role values.""" - - if value not in ProjectRole.values: - raise serializers.ValidationError("Select a valid project role.") - return value diff --git a/projects/tests/test_admin.py b/projects/tests/test_admin.py index 0d5f55b0..28e67f67 100644 --- a/projects/tests/test_admin.py +++ b/projects/tests/test_admin.py @@ -1,7 +1,7 @@ from datetime import timedelta from types import SimpleNamespace from typing import Any, cast -from unittest.mock import ANY, Mock +from unittest.mock import ANY, AsyncMock, Mock import pytest from django.contrib import messages @@ -145,10 +145,14 @@ def test_project_config_admin_recompute_action_queues_source_quality_and_authori source_admin_context, mocker ): config = ProjectConfig.objects.create(project=source_admin_context.project) - source_quality_delay_mock = mocker.patch( - "core.tasks.recompute_source_quality.delay" + source_quality_queue_mock = mocker.patch( + "core.tasks.recompute_source_quality.kiq", + new_callable=AsyncMock, + ) + authority_queue_mock = mocker.patch( + "core.tasks.recompute_authority_scores.kiq", + new_callable=AsyncMock, ) - authority_delay_mock = mocker.patch("core.tasks.recompute_authority_scores.delay") admin_instance = ProjectConfigAdmin(ProjectConfig, AdminSite()) message_user_mock = _message_user_mock(admin_instance, mocker) @@ -158,8 +162,8 @@ def test_project_config_admin_recompute_action_queues_source_quality_and_authori ) project_id = _require_pk(source_admin_context.project) - source_quality_delay_mock.assert_called_once_with(project_id) - authority_delay_mock.assert_called_once_with(project_id) + source_quality_queue_mock.assert_awaited_once_with(project_id) + authority_queue_mock.assert_awaited_once_with(project_id) message_user_mock.assert_called_once_with( ANY, "Queued source-quality and authority recomputation for 1 project config(s).", diff --git a/projects/tests/test_api.py b/projects/tests/test_api.py deleted file mode 100644 index a6d675a7..00000000 --- a/projects/tests/test_api.py +++ /dev/null @@ -1,734 +0,0 @@ -from typing import Any, cast -from unittest.mock import patch -from urllib.parse import parse_qs, urlparse - -from django.contrib.auth import get_user_model -from django.db.models import Model -from django.test import override_settings -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from projects.linkedin_oauth import ( - build_linkedin_authorize_url, - build_linkedin_oauth_state, -) -from projects.model_support import SourcePluginName -from projects.models import ( - BlueskyCredentials, - LinkedInCredentials, - MastodonCredentials, - Project, - ProjectConfig, - ProjectMembership, - ProjectRole, -) - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed API test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create a user through the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -class ProjectApiTests(APITestCase): - """Exercise project-owned project and credential API endpoints.""" - - def setUp(self): - user_model = get_user_model() - self.owner = _create_user(user_model, username="owner", password="testpass123") - self.other_user = _create_user( - user_model, username="other", password="testpass123" - ) - self.owner_project = Project.objects.create( - name="Owner Project", - topic_description="Platform engineering", - ) - self.other_project = Project.objects.create( - name="Other Project", - topic_description="Frontend", - ) - ProjectMembership.objects.create( - user=self.owner, - project=self.owner_project, - role=ProjectRole.ADMIN, - ) - ProjectMembership.objects.create( - user=self.other_user, - project=self.other_project, - role=ProjectRole.ADMIN, - ) - _typed_client(self.client).force_authenticate(self.owner) - - def assert_standardized_validation_error( - self, payload: dict[str, object], attr: str - ): - """Assert the repo-standardized validation payload shape.""" - - self.assertEqual(payload["type"], "validation_error") - errors = cast(list[dict[str, object]], payload["errors"]) - self.assertTrue(any(error["attr"] == attr for error in errors)) - - def test_project_list_requires_authentication(self): - _typed_client(self.client).force_authenticate(user=None) - - response = self.client.get(reverse("v1:project-list"), HTTP_HOST="localhost") - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual( - response.json(), - { - "type": "client_error", - "errors": [ - { - "code": "not_authenticated", - "detail": "Authentication credentials were not provided.", - "attr": None, - } - ], - }, - ) - - def test_project_list_is_scoped_to_request_user_memberships(self): - BlueskyCredentials.objects.create( - project=self.owner_project, - handle="owner-project.bsky.social", - is_active=True, - last_error="", - ) - - response = self.client.get(reverse("v1:project-list")) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_project)) - self.assertEqual(response.json()[0]["user_role"], ProjectRole.ADMIN) - self.assertEqual( - response.json()[0]["intake_token"], self.owner_project.intake_token - ) - self.assertFalse(response.json()[0]["intake_enabled"]) - self.assertTrue(response.json()[0]["has_bluesky_credentials"]) - self.assertEqual( - response.json()[0]["bluesky_handle"], "owner-project.bsky.social" - ) - self.assertTrue(response.json()[0]["bluesky_is_active"]) - self.assertEqual(response.json()[0]["bluesky_last_error"], "") - - def test_project_rotate_intake_token_returns_updated_project(self): - original_token = self.owner_project.intake_token - - response = self.client.post( - reverse( - "v1:project-rotate-intake-token", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.owner_project.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertNotEqual(self.owner_project.intake_token, original_token) - self.assertEqual( - response.json()["intake_token"], self.owner_project.intake_token - ) - - def test_project_config_detail_exposes_multi_signal_authority_weights(self): - config = ProjectConfig.objects.create( - project=self.owner_project, - draft_schedule_cron="0 9 * * *", - ) - - response = self.client.get( - reverse( - "v1:project-config-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(config), - }, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["draft_schedule_cron"], "0 9 * * *") - self.assertEqual(response.json()["authority_weight_mention"], 0.2) - self.assertEqual(response.json()["authority_weight_engagement"], 0.15) - self.assertEqual(response.json()["authority_weight_cross_newsletter"], 0.2) - - def test_project_config_patch_updates_multi_signal_authority_weights(self): - config = ProjectConfig.objects.create(project=self.owner_project) - - response = self.client.patch( - reverse( - "v1:project-config-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(config), - }, - ), - { - "draft_schedule_cron": "15 7 * * 1", - "authority_weight_engagement": 0.25, - "authority_weight_source_quality": 0.3, - }, - format="json", - ) - - config.refresh_from_db() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(config.draft_schedule_cron, "15 7 * * 1") - self.assertEqual(config.authority_weight_engagement, 0.25) - self.assertEqual(config.authority_weight_source_quality, 0.3) - - def test_project_config_patch_rejects_invalid_draft_schedule_cron(self): - config = ProjectConfig.objects.create(project=self.owner_project) - - response = self.client.patch( - reverse( - "v1:project-config-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(config), - }, - ), - {"draft_schedule_cron": "not a cron"}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), - "draft_schedule_cron", - ) - - @override_settings(CELERY_TASK_ALWAYS_EAGER=True) - @patch("core.tasks.recompute_authority_scores") - @patch("core.tasks.recompute_source_quality") - def test_project_config_recompute_authority_runs_tasks_immediately( - self, - recompute_source_quality_mock, - recompute_authority_scores_mock, - ): - config = ProjectConfig.objects.create(project=self.owner_project) - - response = self.client.post( - reverse( - "v1:project-config-recompute-authority", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(config), - }, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["status"], "completed") - self.assertEqual(response.json()["project_id"], _require_pk(self.owner_project)) - self.assertEqual(response.json()["config_id"], _require_pk(config)) - recompute_source_quality_mock.assert_called_once_with( - _require_pk(self.owner_project) - ) - recompute_authority_scores_mock.assert_called_once_with( - _require_pk(self.owner_project) - ) - - def test_bluesky_credentials_list_create_and_update_hide_stored_password(self): - list_response = self.client.get( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(list_response.json(), []) - - create_response = self.client.post( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "handle": "@Owner.Project.BSKY.social", - "pds_url": "https://pds.example.com/xrpc/", - "is_active": True, - "app_password": "app-password", - }, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - credentials = BlueskyCredentials.objects.get(project=self.owner_project) - self.assertEqual(credentials.handle, "owner.project.bsky.social") - self.assertEqual(credentials.pds_url, "https://pds.example.com") - self.assertEqual(credentials.get_app_password(), "app-password") - self.assertTrue(create_response.json()["has_stored_credential"]) - self.assertNotIn("app_password", create_response.json()) - - update_response = self.client.patch( - reverse( - "v1:project-bluesky-credentials-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(credentials), - }, - ), - { - "handle": "updated.bsky.social", - "pds_url": "", - "is_active": False, - }, - format="json", - ) - - credentials.refresh_from_db() - self.assertEqual(update_response.status_code, status.HTTP_200_OK) - self.assertEqual(credentials.handle, "updated.bsky.social") - self.assertFalse(credentials.is_active) - self.assertEqual(credentials.get_app_password(), "app-password") - - def test_bluesky_credentials_create_requires_app_password(self): - response = self.client.post( - reverse( - "v1:project-bluesky-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "handle": "owner.bsky.social", - "pds_url": "", - "is_active": True, - "app_password": "", - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "app_password") - - def test_verify_bluesky_credentials_requires_project_credentials(self): - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "bluesky_credentials" - ) - - @patch("ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials") - def test_verify_bluesky_credentials_verifies_project_account(self, verify_mock): - credentials = BlueskyCredentials( - project=self.owner_project, handle="project.bsky.social" - ) - credentials.set_app_password("app-password") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - verify_mock.assert_called_once() - verified_credentials = verify_mock.call_args.args[0] - self.assertEqual(verified_credentials, credentials) - self.assertEqual(response.json()["status"], "verified") - self.assertEqual(response.json()["handle"], "project.bsky.social") - self.assertEqual(response.json()["last_error"], "") - - @patch("core.api.logger.exception") - @patch( - "ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials", - side_effect=RuntimeError("bad login"), - ) - def test_verify_bluesky_credentials_surfaces_verification_errors( - self, _verify_mock, logger_exception_mock - ): - credentials = BlueskyCredentials( - project=self.owner_project, handle="project.bsky.social" - ) - credentials.set_app_password("app-password") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-bluesky-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "bluesky_credentials" - ) - self.assertNotIn("bad login", str(response.json())) - logger_exception_mock.assert_called_once_with( - "Bluesky credential verification failed for project id=%s", - _require_pk(self.owner_project), - ) - - def test_verify_mastodon_credentials_requires_configured_project_credentials(self): - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "mastodon_credentials" - ) - - def test_linkedin_credentials_list_create_and_update_hide_stored_tokens(self): - list_response = self.client.get( - reverse( - "v1:project-linkedin-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(list_response.json(), []) - - create_response = self.client.post( - reverse( - "v1:project-linkedin-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "member_urn": "urn:li:person:abc123", - "expires_at": "2026-04-27T13:00:00Z", - "is_active": True, - "access_token": "access-token", - "refresh_token": "refresh-token", - }, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - credentials = LinkedInCredentials.objects.get(project=self.owner_project) - self.assertEqual(credentials.member_urn, "urn:li:person:abc123") - self.assertEqual(credentials.get_access_token(), "access-token") - self.assertEqual(credentials.get_refresh_token(), "refresh-token") - self.assertTrue(create_response.json()["has_stored_credential"]) - self.assertNotIn("access_token", create_response.json()) - self.assertNotIn("refresh_token", create_response.json()) - - update_response = self.client.patch( - reverse( - "v1:project-linkedin-credentials-detail", - kwargs={ - "project_id": _require_pk(self.owner_project), - "pk": _require_pk(credentials), - }, - ), - { - "member_urn": "urn:li:person:updated", - "is_active": False, - }, - format="json", - ) - - credentials.refresh_from_db() - self.assertEqual(update_response.status_code, status.HTTP_200_OK) - self.assertEqual(credentials.member_urn, "urn:li:person:updated") - self.assertFalse(credentials.is_active) - self.assertEqual(credentials.get_access_token(), "access-token") - self.assertEqual(credentials.get_refresh_token(), "refresh-token") - - def test_linkedin_credentials_create_requires_both_oauth_tokens(self): - response = self.client.post( - reverse( - "v1:project-linkedin-credentials-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - { - "member_urn": "urn:li:person:abc123", - "expires_at": "2026-04-27T13:00:00Z", - "is_active": True, - "access_token": "", - "refresh_token": "", - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "access_token") - - def test_verify_linkedin_credentials_requires_configured_project_credentials(self): - response = self.client.post( - reverse( - "v1:project-verify-linkedin-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "linkedin_credentials" - ) - - @patch("ingestion.plugins.linkedin.LinkedInSourcePlugin.verify_credentials") - def test_verify_linkedin_credentials_verifies_project_account(self, verify_mock): - credentials = LinkedInCredentials( - project=self.owner_project, - member_urn="urn:li:person:abc123", - ) - credentials.set_access_token("access-token") - credentials.set_refresh_token("refresh-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-linkedin-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - verify_mock.assert_called_once() - verified_credentials = verify_mock.call_args.args[0] - self.assertEqual(verified_credentials, credentials) - self.assertEqual(response.json()["status"], "verified") - self.assertEqual(response.json()["member_urn"], "urn:li:person:abc123") - self.assertEqual(response.json()["last_error"], "") - - @patch("projects.api.build_linkedin_authorize_url") - def test_start_linkedin_oauth_returns_authorize_url(self, build_authorize_url_mock): - build_authorize_url_mock.return_value = ( - "https://www.linkedin.com/oauth/v2/authorization?state=signed-state" - ) - - response = self.client.post( - reverse( - "v1:project-start-linkedin-oauth", - kwargs={"id": _require_pk(self.owner_project)}, - ), - {"redirect_to": "/admin/sources?project=1"}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - build_authorize_url_mock.assert_called_once_with( - self.owner_project, - "/admin/sources?project=1", - ) - self.assertEqual( - response.json(), - { - "authorize_url": ( - "https://www.linkedin.com/oauth/v2/authorization?state=signed-state" - ) - }, - ) - - @override_settings( - LINKEDIN_CLIENT_ID="linkedin-client-id", - LINKEDIN_CLIENT_SECRET="linkedin-client-secret", - LINKEDIN_OAUTH_SCOPES="openid email w_member_social", - NEWSLETTER_PUBLIC_URL="https://public.example.com", - ) - def test_build_linkedin_authorize_url_uses_configured_scopes(self): - authorize_url = build_linkedin_authorize_url( - self.owner_project, - "/admin/sources?project=1", - ) - - parsed_url = urlparse(authorize_url) - query = parse_qs(parsed_url.query) - - self.assertEqual(parsed_url.netloc, "www.linkedin.com") - self.assertEqual( - query["redirect_uri"], - ["https://public.example.com/api/v1/linkedin/oauth/callback/"], - ) - self.assertEqual( - query["scope"], - ["openid email w_member_social"], - ) - - @patch("core.api.logger.exception") - @patch( - "ingestion.plugins.linkedin.LinkedInSourcePlugin.verify_credentials", - side_effect=RuntimeError("bad token"), - ) - def test_verify_linkedin_credentials_surfaces_verification_errors( - self, _verify_mock, logger_exception_mock - ): - credentials = LinkedInCredentials( - project=self.owner_project, - member_urn="urn:li:person:abc123", - ) - credentials.set_access_token("access-token") - credentials.set_refresh_token("refresh-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-linkedin-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "linkedin_credentials" - ) - self.assertNotIn("bad token", str(response.json())) - logger_exception_mock.assert_called_once_with( - "LinkedIn credential verification failed for project id=%s", - _require_pk(self.owner_project), - ) - - @patch("projects.linkedin_oauth.LinkedInSourcePlugin.verify_credentials") - @patch("projects.linkedin_oauth.requests.post") - @override_settings( - LINKEDIN_CLIENT_ID="linkedin-client-id", - LINKEDIN_CLIENT_SECRET="linkedin-client-secret", - ) - def test_linkedin_oauth_callback_persists_project_credentials( - self, - requests_post_mock, - verify_credentials_mock, - ): - token_response = requests_post_mock.return_value - token_response.raise_for_status.return_value = None - token_response.json.return_value = { - "access_token": "access-token", - "refresh_token": "refresh-token", - "expires_in": 3600, - } - state = build_linkedin_oauth_state( - self.owner_project, - "/admin/sources?project=1", - ) - - response = self.client.get( - reverse("v1:linkedin-oauth-callback"), - {"state": state, "code": "oauth-code"}, - ) - - self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertIn("/admin/sources?project=1", response.headers["Location"]) - self.assertIn( - "message=LinkedIn+credentials+authorized.", - response.headers["Location"], - ) - - credentials = LinkedInCredentials.objects.get(project=self.owner_project) - self.assertEqual(credentials.get_access_token(), "access-token") - self.assertEqual(credentials.get_refresh_token(), "refresh-token") - self.assertTrue(credentials.is_active) - verify_credentials_mock.assert_called_once() - - def test_linkedin_oauth_callback_rejects_invalid_state(self): - response = self.client.get( - reverse("v1:linkedin-oauth-callback"), - {"state": "bad-state", "code": "oauth-code"}, - ) - - self.assertEqual(response.status_code, status.HTTP_302_FOUND) - self.assertIn("/admin/sources", response.headers["Location"]) - self.assertIn("error=", response.headers["Location"]) - - @patch("ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials") - def test_verify_mastodon_credentials_verifies_project_account(self, verify_mock): - credentials = MastodonCredentials( - project=self.owner_project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - ) - credentials.set_access_token("access-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - verify_mock.assert_called_once() - verified_credentials = verify_mock.call_args.args[0] - self.assertEqual(verified_credentials, credentials) - self.assertEqual(response.json()["status"], "verified") - self.assertEqual(response.json()["account_acct"], "alice@hachyderm.io") - self.assertEqual(response.json()["instance_url"], "https://hachyderm.io") - self.assertEqual(response.json()["last_error"], "") - - @patch("core.api.logger.exception") - @patch( - "ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials", - side_effect=RuntimeError("bad token"), - ) - def test_verify_mastodon_credentials_surfaces_verification_errors( - self, _verify_mock, logger_exception_mock - ): - credentials = MastodonCredentials( - project=self.owner_project, - instance_url="https://hachyderm.io", - account_acct="alice@hachyderm.io", - ) - credentials.set_access_token("access-token") - credentials.save() - - response = self.client.post( - reverse( - "v1:project-verify-mastodon-credentials", - kwargs={"id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error( - response.json(), "mastodon_credentials" - ) - self.assertNotIn("bad token", str(response.json())) - logger_exception_mock.assert_called_once_with( - "Mastodon credential verification failed for project id=%s", - _require_pk(self.owner_project), - ) - - def test_source_config_create_validates_plugin_config(self): - response = self.client.post( - reverse( - "v1:project-source-config-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - {"plugin_name": SourcePluginName.RSS, "config": {}}, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assert_standardized_validation_error(response.json(), "config") diff --git a/projects/tests/test_invitations.py b/projects/tests/test_invitations.py deleted file mode 100644 index 5b21354a..00000000 --- a/projects/tests/test_invitations.py +++ /dev/null @@ -1,284 +0,0 @@ -from typing import Any, cast - -from django.core import mail -from django.db.models import Model -from django.test import override_settings -from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase - -from projects.models import Project, ProjectMembership, ProjectRole -from users.models import MembershipInvitation - - -def _require_pk(instance: Model) -> int: - """Return a saved model primary key for typed invitation test assertions.""" - - instance_pk = instance.pk - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - -def _create_user(user_model: type[Any], **kwargs: object): - """Create a user via the custom manager with a typed escape hatch.""" - - return cast(Any, user_model.objects).create_user(**kwargs) - - -@override_settings( - EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", - FRONTEND_BASE_URL="http://localhost:3000", -) -class ProjectMembershipAndInvitationApiTests(APITestCase): - def setUp(self): - user_model = self.get_user_model() - self.admin_user = _create_user( - user_model, - username="project-admin", - email="admin@example.com", - password="testpass123", - display_name="Project Admin", - ) - self.second_admin = _create_user( - user_model, - username="second-admin", - email="second-admin@example.com", - password="testpass123", - ) - self.member_user = _create_user( - user_model, - username="project-member", - email="member@example.com", - password="testpass123", - ) - self.reader_user = _create_user( - user_model, - username="project-reader", - email="reader@example.com", - password="testpass123", - ) - self.invited_user = _create_user( - user_model, - username="invited-user", - email="invitee@example.com", - password="testpass123", - ) - - self.project = Project.objects.create( - name="Membership Project", - topic_description="Platform engineering", - ) - self.admin_membership = ProjectMembership.objects.create( - user=self.admin_user, - project=self.project, - role=ProjectRole.ADMIN, - ) - self.second_admin_membership = ProjectMembership.objects.create( - user=self.second_admin, - project=self.project, - role=ProjectRole.ADMIN, - ) - self.member_membership = ProjectMembership.objects.create( - user=self.member_user, - project=self.project, - role=ProjectRole.MEMBER, - ) - self.reader_membership = ProjectMembership.objects.create( - user=self.reader_user, - project=self.project, - role=ProjectRole.READER, - ) - - @staticmethod - def get_user_model(): - from django.contrib.auth import get_user_model - - return get_user_model() - - def test_project_create_assigns_creator_as_admin_membership(self): - creator = _create_user( - self.get_user_model(), - username="creator", - email="creator@example.com", - password="testpass123", - ) - _typed_client(self.client).force_authenticate(creator) - - response = self.client.post( - reverse("v1:project-list"), - { - "name": "Creator Project", - "topic_description": "Creator-owned project", - "content_retention_days": 90, - }, - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - project = Project.objects.get(name="Creator Project") - membership = ProjectMembership.objects.get(project=project, user=creator) - self.assertEqual(membership.role, ProjectRole.ADMIN) - self.assertEqual(response.json()["user_role"], ProjectRole.ADMIN) - - def test_project_admin_can_list_update_and_remove_memberships(self): - _typed_client(self.client).force_authenticate(self.admin_user) - - list_response = self.client.get( - reverse( - "v1:project-membership-list", - kwargs={"project_id": _require_pk(self.project)}, - ) - ) - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(len(list_response.json()), 4) - - update_response = self.client.patch( - reverse( - "v1:project-membership-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.member_membership), - }, - ), - {"role": ProjectRole.READER}, - format="json", - ) - self.assertEqual(update_response.status_code, status.HTTP_200_OK) - self.member_membership.refresh_from_db() - self.assertEqual(self.member_membership.role, ProjectRole.READER) - - delete_response = self.client.delete( - reverse( - "v1:project-membership-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.reader_membership), - }, - ) - ) - self.assertEqual(delete_response.status_code, status.HTTP_204_NO_CONTENT) - self.assertFalse( - ProjectMembership.objects.filter( - pk=_require_pk(self.reader_membership) - ).exists() - ) - - def test_last_admin_cannot_be_demoted_or_removed(self): - self.second_admin_membership.delete() - _typed_client(self.client).force_authenticate(self.admin_user) - - demote_response = self.client.patch( - reverse( - "v1:project-membership-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.admin_membership), - }, - ), - {"role": ProjectRole.MEMBER}, - format="json", - ) - self.assertEqual(demote_response.status_code, status.HTTP_400_BAD_REQUEST) - - delete_response = self.client.delete( - reverse( - "v1:project-membership-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(self.admin_membership), - }, - ) - ) - self.assertEqual(delete_response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_project_admin_can_create_and_revoke_invitation(self): - _typed_client(self.client).force_authenticate(self.admin_user) - - create_response = self.client.post( - reverse( - "v1:project-invitation-list", - kwargs={"project_id": _require_pk(self.project)}, - ), - {"email": "invitee@example.com", "role": ProjectRole.MEMBER}, - format="json", - ) - - self.assertEqual(create_response.status_code, status.HTTP_201_CREATED) - invitation = MembershipInvitation.objects.get(project=self.project) - self.assertEqual(invitation.invited_by, self.admin_user) - self.assertEqual(len(mail.outbox), 1) - self.assertIn(invitation.token, str(mail.outbox[0].body)) - - revoke_response = self.client.delete( - reverse( - "v1:project-invitation-detail", - kwargs={ - "project_id": _require_pk(self.project), - "pk": _require_pk(invitation), - }, - ) - ) - self.assertEqual(revoke_response.status_code, status.HTTP_204_NO_CONTENT) - invitation.refresh_from_db() - self.assertIsNotNone(invitation.revoked_at) - - def test_invited_user_can_view_and_accept_invitation_token(self): - invitation = MembershipInvitation.objects.create( - project=self.project, - email=self.invited_user.email, - role=ProjectRole.READER, - invited_by=self.admin_user, - ) - - public_response = self.client.get( - reverse( - "membership-invitation-token", - kwargs={"token": invitation.token}, - ) - ) - self.assertEqual(public_response.status_code, status.HTTP_200_OK) - self.assertEqual(public_response.json()["project_name"], self.project.name) - - _typed_client(self.client).force_authenticate(self.invited_user) - accept_response = self.client.post( - reverse( - "membership-invitation-token", - kwargs={"token": invitation.token}, - ), - format="json", - ) - self.assertEqual(accept_response.status_code, status.HTTP_200_OK) - - invitation.refresh_from_db() - membership = ProjectMembership.objects.get( - project=self.project, - user=self.invited_user, - ) - self.assertEqual(membership.role, ProjectRole.READER) - self.assertIsNotNone(invitation.accepted_at) - - def test_accept_requires_matching_email(self): - invitation = MembershipInvitation.objects.create( - project=self.project, - email="expected@example.com", - role=ProjectRole.MEMBER, - invited_by=self.admin_user, - ) - _typed_client(self.client).force_authenticate(self.member_user) - - response = self.client.post( - reverse( - "membership-invitation-token", - kwargs={"token": invitation.token}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) diff --git a/projects/tests/test_linkedin_oauth.py b/projects/tests/test_linkedin_oauth.py new file mode 100644 index 00000000..4efd067d --- /dev/null +++ b/projects/tests/test_linkedin_oauth.py @@ -0,0 +1,96 @@ +from __future__ import annotations + +from unittest.mock import patch +from urllib.parse import parse_qs, urlparse + +import pytest +from django.test import Client, override_settings +from django.urls import reverse + +from projects.linkedin_oauth import ( + build_linkedin_authorize_url, + build_linkedin_oauth_state, +) +from projects.models import LinkedInCredentials, Project + +pytestmark = pytest.mark.django_db + + +@override_settings( + LINKEDIN_CLIENT_ID="linkedin-client-id", + LINKEDIN_CLIENT_SECRET="linkedin-client-secret", + LINKEDIN_OAUTH_SCOPES="openid email w_member_social", + NEWSLETTER_PUBLIC_URL="https://public.example.com", +) +def test_build_linkedin_authorize_url_uses_configured_scopes(): + project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + + authorize_url = build_linkedin_authorize_url( + project, + "/admin/sources?project=1", + ) + + parsed_url = urlparse(authorize_url) + query = parse_qs(parsed_url.query) + + assert parsed_url.netloc == "www.linkedin.com" + assert query["redirect_uri"] == [ + "https://public.example.com/api/v1/linkedin/oauth/callback/" + ] + assert query["scope"] == ["openid email w_member_social"] + + +@patch("projects.linkedin_oauth.LinkedInSourcePlugin.verify_credentials") +@patch("projects.linkedin_oauth.requests.post") +@override_settings( + LINKEDIN_CLIENT_ID="linkedin-client-id", + LINKEDIN_CLIENT_SECRET="linkedin-client-secret", +) +def test_linkedin_oauth_callback_persists_project_credentials( + requests_post_mock, + verify_credentials_mock, +): + project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + token_response = requests_post_mock.return_value + token_response.raise_for_status.return_value = None + token_response.json.return_value = { + "access_token": "access-token", + "refresh_token": "refresh-token", + "expires_in": 3600, + } + state = build_linkedin_oauth_state( + project, + "/admin/sources?project=1", + ) + + response = Client().get( + reverse("v1:linkedin-oauth-callback"), + {"state": state, "code": "oauth-code"}, + ) + + assert response.status_code == 302 + assert "/admin/sources?project=1" in response.headers["Location"] + assert "message=LinkedIn+credentials+authorized." in response.headers["Location"] + + credentials = LinkedInCredentials.objects.get(project=project) + assert credentials.get_access_token() == "access-token" + assert credentials.get_refresh_token() == "refresh-token" + assert credentials.is_active is True + verify_credentials_mock.assert_called_once() + + +def test_linkedin_oauth_callback_rejects_invalid_state(): + response = Client().get( + reverse("v1:linkedin-oauth-callback"), + {"state": "bad-state", "code": "oauth-code"}, + ) + + assert response.status_code == 302 + assert "/admin/sources" in response.headers["Location"] + assert "error=" in response.headers["Location"] diff --git a/projects/tests/test_ninja_api.py b/projects/tests/test_ninja_api.py new file mode 100644 index 00000000..249e2077 --- /dev/null +++ b/projects/tests/test_ninja_api.py @@ -0,0 +1,361 @@ +from http import HTTPStatus +from typing import Any, cast +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from projects.models import ( + BlueskyCredentials, + LinkedInCredentials, + MastodonCredentials, + Project, + ProjectMembership, + ProjectRole, +) + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + """Create a user through the custom manager with a typed escape hatch.""" + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectNinjaApiTests(TestCase): + """Exercise project-owned Ninja API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.other_user = _create_user( + user_model, username="other", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + self.client.force_login(self.owner) + + def test_project_list_requires_authentication(self): + self.client.logout() + response = self.client.get( + reverse("ninja-api:list_projects"), HTTP_HOST="localhost" + ) + self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED) + self.assertEqual( + response.json(), + {"detail": "Unauthorized"}, + ) + + def test_project_list_is_scoped_to_request_user_memberships(self): + BlueskyCredentials.objects.create( + project=self.owner_project, + handle="owner-project.bsky.social", + is_active=True, + last_error="", + ) + + response = self.client.get(reverse("ninja-api:list_projects")) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_project)) + self.assertEqual(response.json()[0]["user_role"], ProjectRole.ADMIN) + self.assertEqual( + response.json()[0]["intake_token"], self.owner_project.intake_token + ) + self.assertFalse(response.json()[0]["intake_enabled"]) + self.assertTrue(response.json()[0]["has_bluesky_credentials"]) + self.assertEqual( + response.json()[0]["bluesky_handle"], "owner-project.bsky.social" + ) + self.assertTrue(response.json()[0]["bluesky_is_active"]) + self.assertEqual(response.json()[0]["bluesky_last_error"], "") + + def test_canonical_v1_project_list_uses_ninja_router(self): + response = self.client.get("/api/v1/projects/") + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["id"], _require_pk(self.owner_project)) + + def test_create_and_update_project_use_native_validation_and_payload_shape(self): + create_response = self.client.post( + reverse("ninja-api:create_project"), + { + "name": "New Project", + "topic_description": "AI workflows", + "content_retention_days": 120, + "intake_enabled": True, + }, + content_type="application/json", + ) + + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + created_project = Project.objects.get(name="New Project") + self.assertEqual(create_response.json()["id"], _require_pk(created_project)) + self.assertEqual(create_response.json()["user_role"], ProjectRole.ADMIN) + self.assertTrue(create_response.json()["intake_enabled"]) + self.assertEqual(created_project.content_retention_days, 120) + self.assertTrue( + ProjectMembership.objects.filter( + project=created_project, + user=self.owner, + role=ProjectRole.ADMIN, + ).exists() + ) + + update_response = self.client.patch( + reverse( + "ninja-api:update_project", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "name": "Updated Owner Project", + "content_retention_days": 30, + "intake_enabled": True, + }, + content_type="application/json", + ) + + self.owner_project.refresh_from_db() + self.assertEqual(update_response.status_code, HTTPStatus.OK) + self.assertEqual(self.owner_project.name, "Updated Owner Project") + self.assertEqual(self.owner_project.content_retention_days, 30) + self.assertTrue(self.owner_project.intake_enabled) + self.assertEqual(update_response.json()["name"], "Updated Owner Project") + self.assertTrue(update_response.json()["intake_enabled"]) + + def test_project_create_and_update_reject_invalid_native_payloads(self): + create_response = self.client.post( + reverse("ninja-api:create_project"), + { + "name": " ", + "topic_description": "", + "content_retention_days": -1, + }, + content_type="application/json", + ) + update_response = self.client.patch( + reverse( + "ninja-api:update_project", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "content_retention_days": -5, + }, + content_type="application/json", + ) + + self.assertEqual(create_response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + create_response.json()["name"][0], "This field may not be blank." + ) + self.assertEqual( + create_response.json()["topic_description"][0], + "This field may not be blank.", + ) + self.assertEqual( + create_response.json()["content_retention_days"][0], + "Ensure this value is greater than or equal to 0.", + ) + self.assertEqual(update_response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + update_response.json()["content_retention_days"][0], + "Ensure this value is greater than or equal to 0.", + ) + + def test_project_rotate_intake_token_returns_updated_project(self): + original_token = self.owner_project.intake_token + + response = self.client.post( + reverse( + "ninja-api:rotate_intake_token", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.owner_project.refresh_from_db() + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertNotEqual(self.owner_project.intake_token, original_token) + self.assertEqual( + response.json()["intake_token"], self.owner_project.intake_token + ) + + def test_verify_bluesky_credentials_requires_project_credentials(self): + response = self.client.post( + reverse( + "ninja-api:verify_bluesky_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + + @patch("ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials") + def test_verify_bluesky_credentials_verifies_project_account(self, verify_mock): + credentials = BlueskyCredentials( + project=self.owner_project, handle="project.bsky.social" + ) + credentials.set_app_password("app-password") + credentials.save() + + response = self.client.post( + reverse( + "ninja-api:verify_bluesky_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + verify_mock.assert_called_once() + verified_credentials = verify_mock.call_args.args[0] + self.assertEqual(verified_credentials, credentials) + self.assertEqual(response.json()["status"], "verified") + self.assertEqual(response.json()["handle"], "project.bsky.social") + self.assertEqual(response.json()["last_error"], "") + + @patch("projects.ninja_api.logger.exception") + @patch( + "ingestion.plugins.bluesky.BlueskySourcePlugin.verify_credentials", + side_effect=RuntimeError("bad login"), + ) + def test_verify_bluesky_credentials_surfaces_verification_errors( + self, _verify_mock, logger_exception_mock + ): + credentials = BlueskyCredentials( + project=self.owner_project, handle="project.bsky.social" + ) + credentials.set_app_password("app-password") + credentials.save() + + response = self.client.post( + reverse( + "ninja-api:verify_bluesky_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertNotIn("bad login", str(response.json())) + logger_exception_mock.assert_called_once_with( + "Bluesky credential verification failed for project id=%s", + _require_pk(self.owner_project), + ) + + def test_verify_mastodon_credentials_requires_configured_project_credentials(self): + response = self.client.post( + reverse( + "ninja-api:verify_mastodon_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + + @patch("ingestion.plugins.mastodon.MastodonSourcePlugin.verify_credentials") + def test_verify_mastodon_credentials_verifies_project_account(self, verify_mock): + credentials = MastodonCredentials( + project=self.owner_project, + account_acct="test@mastodon.social", + instance_url="https://mastodon.social", + ) + credentials.set_access_token("access-token") + credentials.save() + + response = self.client.post( + reverse( + "ninja-api:verify_mastodon_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + verify_mock.assert_called_once() + self.assertEqual(response.json()["status"], "verified") + self.assertEqual(response.json()["account_acct"], "test@mastodon.social") + self.assertEqual(response.json()["instance_url"], "https://mastodon.social") + + def test_verify_linkedin_credentials_requires_configured_project_credentials(self): + response = self.client.post( + reverse( + "ninja-api:verify_linkedin_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + + @patch("ingestion.plugins.linkedin.LinkedInSourcePlugin.verify_credentials") + def test_verify_linkedin_credentials_verifies_project_account(self, verify_mock): + credentials = LinkedInCredentials( + project=self.owner_project, member_urn="urn:li:person:123" + ) + credentials.set_access_token("access-token") + credentials.set_refresh_token("refresh-token") + credentials.save() + + response = self.client.post( + reverse( + "ninja-api:verify_linkedin_credentials", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + verify_mock.assert_called_once() + self.assertEqual(response.json()["status"], "verified") + self.assertEqual(response.json()["member_urn"], "urn:li:person:123") + + @patch("projects.ninja_api.build_linkedin_authorize_url") + def test_start_linkedin_oauth_returns_authorize_url(self, build_authorize_url_mock): + build_authorize_url_mock.return_value = ( + "https://www.linkedin.com/oauth/v2/authorization?test=1" + ) + + response = self.client.post( + reverse( + "ninja-api:start_linkedin_oauth", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"redirect_to": "http://localhost:3000/callback"}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual( + response.json()["authorize_url"], + "https://www.linkedin.com/oauth/v2/authorization?test=1", + ) + build_authorize_url_mock.assert_called_once_with( + self.owner_project, "http://localhost:3000/callback" + ) diff --git a/projects/tests/test_ninja_credentials_api.py b/projects/tests/test_ninja_credentials_api.py new file mode 100644 index 00000000..8ad4ed3d --- /dev/null +++ b/projects/tests/test_ninja_credentials_api.py @@ -0,0 +1,248 @@ +from http import HTTPStatus +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from projects.models import ( + BlueskyCredentials, + LinkedInCredentials, + MastodonCredentials, + Project, + ProjectMembership, + ProjectRole, +) + + +def _require_pk(instance: Model) -> int: + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectCredentialsNinjaApiTests(TestCase): + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpassword") + self.other_user = _create_user( + user_model, username="other", password="testpassword" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.other_project = Project.objects.create( + name="Other Project", + topic_description="Frontend", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.other_user, + project=self.other_project, + role=ProjectRole.ADMIN, + ) + + self.member_project = Project.objects.create( + name="Member Project", + topic_description="Member", + ) + ProjectMembership.objects.create( + user=self.owner, project=self.member_project, role=ProjectRole.MEMBER + ) + + self.client.force_login(self.owner) + + def test_bluesky_credentials_list_create_and_update_hide_stored_password(self): + url_list = reverse( + "ninja-api:list_bluesky", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + + list_response = self.client.get(url_list) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(list_response.json(), []) + + create_response = self.client.post( + url_list, + { + "handle": "@Owner.Project.BSKY.social", + "pds_url": "https://pds.example.com/xrpc/", + "is_active": True, + "app_password": "app-password", + }, + content_type="application/json", + ) + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + credentials = BlueskyCredentials.objects.get(project=self.owner_project) + self.assertEqual(credentials.handle, "owner.project.bsky.social") + self.assertEqual(credentials.pds_url, "https://pds.example.com") + self.assertEqual(credentials.get_app_password(), "app-password") + self.assertTrue(create_response.json()["has_stored_credential"]) + self.assertNotIn("app_password", create_response.json()) + + update_response = self.client.patch( + reverse( + "ninja-api:update_bluesky", + kwargs={ + "project_id": _require_pk(self.owner_project), + "cred_id": _require_pk(credentials), + }, + ), + {"handle": "@new.bsky.social"}, + content_type="application/json", + ) + self.assertEqual(update_response.status_code, HTTPStatus.OK) + credentials.refresh_from_db() + self.assertEqual(credentials.handle, "new.bsky.social") + self.assertEqual(credentials.get_app_password(), "app-password") + self.assertNotIn("app_password", update_response.json()) + + def test_bluesky_credentials_permissions(self): + url_list = reverse( + "ninja-api:list_bluesky", + kwargs={"project_id": _require_pk(self.member_project)}, + ) + create_response = self.client.post( + url_list, + { + "handle": "@Owner.Project.BSKY.social", + "pds_url": "https://pds.example.com/xrpc/", + "is_active": True, + "app_password": "app-password", + }, + content_type="application/json", + ) + self.assertEqual(create_response.status_code, HTTPStatus.FORBIDDEN) + + def test_mastodon_credentials_list_create_and_update(self): + url_list = reverse( + "ninja-api:list_mastodon", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + list_response = self.client.get(url_list) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + + create_response = self.client.post( + url_list, + { + "instance_url": "https://mastodon.social", + "account_acct": "user@mastodon.social", + "is_active": True, + "access_token": "token123", + }, + content_type="application/json", + ) + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + credentials = MastodonCredentials.objects.get(project=self.owner_project) + self.assertEqual(credentials.instance_url, "https://mastodon.social") + self.assertEqual(credentials.get_access_token(), "token123") + self.assertTrue(create_response.json()["has_stored_credential"]) + self.assertNotIn("access_token", create_response.json()) + + url_detail = reverse( + "ninja-api:update_mastodon", + kwargs={ + "project_id": _require_pk(self.owner_project), + "cred_id": _require_pk(credentials), + }, + ) + update_response = self.client.patch( + url_detail, + {"account_acct": "newuser@mastodon.social"}, + content_type="application/json", + ) + self.assertEqual(update_response.status_code, HTTPStatus.OK) + + delete_response = self.client.delete( + reverse( + "ninja-api:delete_mastodon", + kwargs={ + "project_id": _require_pk(self.owner_project), + "cred_id": _require_pk(credentials), + }, + ) + ) + self.assertEqual(delete_response.status_code, HTTPStatus.NO_CONTENT) + + def test_linkedin_credentials_list_create_and_update(self): + url_list = reverse( + "ninja-api:list_linkedin", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + + create_response = self.client.post( + url_list, + { + "member_urn": "urn:li:person:12345", + "is_active": True, + "access_token": "access123", + "refresh_token": "refresh123", + }, + content_type="application/json", + ) + self.assertEqual(create_response.status_code, HTTPStatus.CREATED) + credentials = LinkedInCredentials.objects.get(project=self.owner_project) + self.assertEqual(credentials.member_urn, "urn:li:person:12345") + self.assertEqual(credentials.get_access_token(), "access123") + self.assertTrue(create_response.json()["has_stored_credential"]) + self.assertNotIn("access_token", create_response.json()) + self.assertNotIn("refresh_token", create_response.json()) + + url_detail = reverse( + "ninja-api:update_linkedin", + kwargs={ + "project_id": _require_pk(self.owner_project), + "cred_id": _require_pk(credentials), + }, + ) + update_response = self.client.patch( + url_detail, + {"member_urn": "urn:li:person:67890"}, + content_type="application/json", + ) + self.assertEqual(update_response.status_code, HTTPStatus.OK) + + def test_bluesky_credentials_create_requires_app_password(self): + url_list = reverse( + "ninja-api:list_bluesky", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + response = self.client.post( + url_list, + { + "handle": "owner.bsky.social", + "pds_url": "", + "is_active": True, + "app_password": "", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + + def test_linkedin_credentials_create_requires_both_oauth_tokens(self): + url_list = reverse( + "ninja-api:list_linkedin", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + response = self.client.post( + url_list, + { + "member_urn": "urn:li:person:12345", + "is_active": True, + "access_token": "", + "refresh_token": "refresh-token", + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) diff --git a/projects/tests/test_ninja_invitations_api.py b/projects/tests/test_ninja_invitations_api.py new file mode 100644 index 00000000..7ae964ec --- /dev/null +++ b/projects/tests/test_ninja_invitations_api.py @@ -0,0 +1,151 @@ +from http import HTTPStatus +from typing import Any, cast +from unittest.mock import patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from projects.models import Project, ProjectMembership, ProjectRole +from users.models import MembershipInvitation + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectInvitationNinjaApiTests(TestCase): + """Exercise project invitation Ninja API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member_user = _create_user( + user_model, + username="member", + email="member@example.com", + password="testpass123", + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.admin_membership = ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + self.member_membership = ProjectMembership.objects.create( + user=self.member_user, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) + self.client.login(username="owner", password="testpass123") + + def test_list_invitations(self): + MembershipInvitation.objects.create( + project=self.owner_project, + email="test1@example.com", + role=ProjectRole.MEMBER, + ) + response = self.client.get( + reverse( + "ninja-api:list_invitations", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["email"], "test1@example.com") + + @patch("projects.ninja_invitations_api._send_membership_invitation_email") + def test_create_invitation(self, mock_send_email): + response = self.client.post( + reverse( + "ninja-api:create_invitation", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"email": " TEST2@Example.com ", "role": ProjectRole.MEMBER}, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + self.assertEqual(response.json()["email"], "test2@example.com") + self.assertEqual( + MembershipInvitation.objects.filter(project=self.owner_project).count(), 1 + ) + invitation = MembershipInvitation.objects.get(project=self.owner_project) + self.assertEqual(invitation.email, "test2@example.com") + mock_send_email.assert_called_once() + + @patch("projects.ninja_invitations_api._send_membership_invitation_email") + def test_create_invitation_rejects_existing_project_member(self, mock_send_email): + response = self.client.post( + reverse( + "ninja-api:create_invitation", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"email": self.member_user.email, "role": ProjectRole.MEMBER}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["email"][0], + "That user is already a project member.", + ) + mock_send_email.assert_not_called() + + @patch("projects.ninja_invitations_api._send_membership_invitation_email") + def test_create_invitation_rejects_duplicate_active_invitation( + self, mock_send_email + ): + MembershipInvitation.objects.create( + project=self.owner_project, + email="test4@example.com", + role=ProjectRole.MEMBER, + invited_by=self.owner, + ) + + response = self.client.post( + reverse( + "ninja-api:create_invitation", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"email": "Test4@example.com", "role": ProjectRole.READER}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["email"][0], + "An active invitation already exists for this email.", + ) + mock_send_email.assert_not_called() + + def test_revoke_invitation(self): + invitation = MembershipInvitation.objects.create( + project=self.owner_project, + email="test3@example.com", + role=ProjectRole.MEMBER, + ) + response = self.client.delete( + reverse( + "ninja-api:revoke_invitation", + kwargs={ + "project_id": _require_pk(self.owner_project), + "invitation_id": _require_pk(invitation), + }, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) + invitation.refresh_from_db() + self.assertIsNotNone(invitation.revoked_at) diff --git a/projects/tests/test_ninja_memberships_api.py b/projects/tests/test_ninja_memberships_api.py new file mode 100644 index 00000000..b241bfe9 --- /dev/null +++ b/projects/tests/test_ninja_memberships_api.py @@ -0,0 +1,140 @@ +from http import HTTPStatus +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from projects.models import Project, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectMembershipNinjaApiTests(TestCase): + """Exercise project membership Ninja API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member_user = _create_user( + user_model, username="member", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + self.admin_membership = ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + self.member_membership = ProjectMembership.objects.create( + user=self.member_user, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) + self.client.login(username="owner", password="testpass123") + + def test_list_memberships(self): + response = self.client.get( + reverse( + "ninja-api:list_memberships", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.OK) + # 2 memberships + self.assertEqual(len(response.json()), 2) + + def test_update_membership_role(self): + response = self.client.patch( + reverse( + "ninja-api:update_membership", + kwargs={ + "project_id": _require_pk(self.owner_project), + "membership_id": _require_pk(self.member_membership), + }, + ), + {"role": ProjectRole.ADMIN}, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.OK) + self.member_membership.refresh_from_db() + self.assertEqual(self.member_membership.role, ProjectRole.ADMIN) + + def test_update_role_fails_for_last_admin(self): + response = self.client.patch( + reverse( + "ninja-api:update_membership", + kwargs={ + "project_id": _require_pk(self.owner_project), + "membership_id": _require_pk(self.admin_membership), + }, + ), + {"role": ProjectRole.MEMBER}, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["role"][0], + "Projects must keep at least one admin.", + ) + + def test_update_membership_rejects_invalid_role(self): + response = self.client.patch( + reverse( + "ninja-api:update_membership", + kwargs={ + "project_id": _require_pk(self.owner_project), + "membership_id": _require_pk(self.member_membership), + }, + ), + {"role": "invalid-role"}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual(response.json()["role"][0], "Select a valid project role.") + + def test_delete_membership(self): + response = self.client.delete( + reverse( + "ninja-api:delete_membership", + kwargs={ + "project_id": _require_pk(self.owner_project), + "membership_id": _require_pk(self.member_membership), + }, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) + self.assertEqual( + ProjectMembership.objects.filter(project=self.owner_project).count(), 1 + ) + + def test_delete_last_admin_membership_fails(self): + response = self.client.delete( + reverse( + "ninja-api:delete_membership", + kwargs={ + "project_id": _require_pk(self.owner_project), + "membership_id": _require_pk(self.admin_membership), + }, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["role"][0], + "Projects must keep at least one admin.", + ) diff --git a/projects/tests/test_ninja_project_configs_api.py b/projects/tests/test_ninja_project_configs_api.py new file mode 100644 index 00000000..5cc14105 --- /dev/null +++ b/projects/tests/test_ninja_project_configs_api.py @@ -0,0 +1,230 @@ +from http import HTTPStatus +from typing import Any, cast +from unittest.mock import AsyncMock, patch + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase, override_settings +from django.urls import reverse + +from projects.models import Project, ProjectConfig, ProjectMembership, ProjectRole + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + return cast(Any, user_model.objects).create_user(**kwargs) + + +class ProjectConfigNinjaApiTests(TestCase): + """Exercise project config Ninja API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.member_user = _create_user( + user_model, username="member", password="testpass123" + ) + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.member_user, + project=self.owner_project, + role=ProjectRole.MEMBER, + ) + self.client.login(username="owner", password="testpass123") + + def test_project_config_detail_exposes_multi_signal_authority_weights(self): + config = ProjectConfig.objects.create( + project=self.owner_project, + draft_schedule_cron="0 9 * * *", + ) + + response = self.client.get( + reverse( + "ninja-api:get_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(response.json()["draft_schedule_cron"], "0 9 * * *") + self.assertEqual(response.json()["authority_weight_mention"], 0.2) + self.assertEqual(response.json()["authority_weight_engagement"], 0.15) + self.assertEqual(response.json()["authority_weight_cross_newsletter"], 0.2) + + def test_project_config_create_persists_native_validated_payload(self): + response = self.client.post( + reverse( + "ninja-api:create_config", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "draft_schedule_cron": " 0 8 * * 1 ", + "authority_weight_engagement": 0.25, + }, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.CREATED) + config = ProjectConfig.objects.get(project=self.owner_project) + self.assertEqual(config.draft_schedule_cron, "0 8 * * 1") + self.assertEqual(config.authority_weight_engagement, 0.25) + self.assertEqual(response.json()["draft_schedule_cron"], "0 8 * * 1") + + def test_project_config_delete_removes_row(self): + config = ProjectConfig.objects.create(project=self.owner_project) + + response = self.client.delete( + reverse( + "ninja-api:delete_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ) + ) + + self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) + self.assertFalse(ProjectConfig.objects.filter(pk=_require_pk(config)).exists()) + + def test_project_config_patch_updates_multi_signal_authority_weights(self): + config = ProjectConfig.objects.create(project=self.owner_project) + + response = self.client.patch( + reverse( + "ninja-api:update_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + { + "draft_schedule_cron": "15 7 * * 1", + "authority_weight_engagement": 0.25, + "authority_weight_source_quality": 0.3, + }, + content_type="application/json", + ) + + config.refresh_from_db() + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(config.draft_schedule_cron, "15 7 * * 1") + self.assertEqual(config.authority_weight_engagement, 0.25) + self.assertEqual(config.authority_weight_source_quality, 0.3) + + def test_project_config_patch_rejects_invalid_draft_schedule_cron(self): + config = ProjectConfig.objects.create(project=self.owner_project) + + response = self.client.patch( + reverse( + "ninja-api:update_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + {"draft_schedule_cron": "not a cron"}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + # Check validation payload format + json_data = response.json() + self.assertIn("draft_schedule_cron", json_data) + self.assertIn( + "valid 5-part cron expression", json_data["draft_schedule_cron"][0] + ) + + @override_settings(TASKIQ_ALWAYS_EAGER=True) + @patch("core.tasks.recompute_authority_scores") + @patch("core.tasks.recompute_source_quality") + def test_project_config_recompute_authority_runs_tasks_immediately( + self, + recompute_source_quality_mock, + recompute_authority_scores_mock, + ): + config = ProjectConfig.objects.create(project=self.owner_project) + + response = self.client.post( + reverse( + "ninja-api:recompute_authority", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + self.assertEqual(response.json()["status"], "completed") + self.assertEqual(response.json()["project_id"], _require_pk(self.owner_project)) + self.assertEqual(response.json()["config_id"], _require_pk(config)) + recompute_source_quality_mock.assert_called_once_with( + _require_pk(self.owner_project) + ) + recompute_authority_scores_mock.assert_called_once_with( + _require_pk(self.owner_project) + ) + + @patch("core.tasks.recompute_authority_scores.kiq", new_callable=AsyncMock) + @patch("core.tasks.recompute_source_quality.kiq", new_callable=AsyncMock) + def test_project_config_recompute_authority_queues_taskiq_tasks( + self, + recompute_source_quality_queue_mock, + recompute_authority_scores_queue_mock, + ): + config = ProjectConfig.objects.create(project=self.owner_project) + + response = self.client.post( + reverse( + "ninja-api:recompute_authority", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.ACCEPTED) + self.assertEqual(response.json()["status"], "queued") + recompute_source_quality_queue_mock.assert_awaited_once_with( + _require_pk(self.owner_project) + ) + recompute_authority_scores_queue_mock.assert_awaited_once_with( + _require_pk(self.owner_project) + ) + + def test_members_cannot_update_config(self): + self.client.login(username="member", password="testpass123") + config = ProjectConfig.objects.create(project=self.owner_project) + response = self.client.patch( + reverse( + "ninja-api:update_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + {"draft_schedule_cron": "15 7 * * 1"}, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) diff --git a/projects/tests/test_ninja_source_configs_api.py b/projects/tests/test_ninja_source_configs_api.py new file mode 100644 index 00000000..ab54aa1f --- /dev/null +++ b/projects/tests/test_ninja_source_configs_api.py @@ -0,0 +1,156 @@ +from http import HTTPStatus +from typing import Any, cast + +from django.contrib.auth import get_user_model +from django.db.models import Model +from django.test import TestCase +from django.urls import reverse + +from projects.model_support import SourcePluginName +from projects.models import Project, ProjectMembership, ProjectRole, SourceConfig + + +def _require_pk(instance: Model) -> int: + """Return a saved model primary key for typed API test assertions.""" + instance_pk = instance.pk + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _create_user(user_model: type[Any], **kwargs: object): + return cast(Any, user_model.objects).create_user(**kwargs) + + +class SourceConfigNinjaApiTests(TestCase): + """Exercise source configurations Ninja API endpoints.""" + + def setUp(self): + user_model = get_user_model() + self.owner = _create_user(user_model, username="owner", password="testpass123") + self.reader = _create_user( + user_model, username="reader", password="testpass123" + ) + + self.owner_project = Project.objects.create( + name="Owner Project", + topic_description="Platform engineering", + ) + + ProjectMembership.objects.create( + user=self.owner, + project=self.owner_project, + role=ProjectRole.ADMIN, + ) + ProjectMembership.objects.create( + user=self.reader, + project=self.owner_project, + role=ProjectRole.READER, + ) + self.client.login(username="owner", password="testpass123") + + def test_list_source_configs(self): + SourceConfig.objects.create( + project=self.owner_project, + plugin_name=SourcePluginName.RSS, + config={"feed_url": "https://example.com/feed.xml"}, + ) + response = self.client.get( + reverse( + "ninja-api:list_source_configs", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.OK) + # Should be exactly 1 + self.assertEqual(len(response.json()), 1) + self.assertEqual(response.json()[0]["plugin_name"], SourcePluginName.RSS) + + def test_create_source_config(self): + response = self.client.post( + reverse( + "ninja-api:create_source_config", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + { + "plugin_name": SourcePluginName.RSS, + "config": {"feed_url": "https://example.com/feed.xml"}, + "is_active": True, + }, + content_type="application/json", + ) + self.assertEqual(response.status_code, HTTPStatus.CREATED) + config = SourceConfig.objects.get(project=self.owner_project) + self.assertEqual(config.plugin_name, SourcePluginName.RSS) + self.assertEqual( + config.config, + {"feed_url": "https://example.com/feed.xml"}, + ) + self.assertTrue(response.json()["is_active"]) + + def test_create_source_config_rejects_invalid_plugin_config(self): + response = self.client.post( + reverse( + "ninja-api:create_source_config", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + {"plugin_name": SourcePluginName.RSS, "config": {}}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST) + self.assertEqual( + response.json()["config"][0], + "Invalid source configuration.", + ) + + def test_update_source_config_revalidates_and_normalizes_plugin_config(self): + config = SourceConfig.objects.create( + project=self.owner_project, + plugin_name=SourcePluginName.BLUESKY, + config={"author_handle": "owner.bsky.social"}, + ) + + response = self.client.patch( + reverse( + "ninja-api:update_source_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ), + {"config": {"author_handle": "@Alice.BSKY.social"}}, + content_type="application/json", + ) + + self.assertEqual(response.status_code, HTTPStatus.OK) + config.refresh_from_db() + self.assertEqual( + config.config, + { + "author_handle": "alice.bsky.social", + "include_replies": False, + "max_posts_per_fetch": 100, + }, + ) + self.assertEqual( + response.json()["config"]["author_handle"], "alice.bsky.social" + ) + + def test_reader_cannot_delete(self): + config = SourceConfig.objects.create( + project=self.owner_project, + plugin_name=SourcePluginName.REDDIT, + config={"subreddit": "MachineLearning"}, + ) + self.client.login(username="reader", password="testpass123") + response = self.client.delete( + reverse( + "ninja-api:delete_source_config", + kwargs={ + "project_id": _require_pk(self.owner_project), + "config_id": _require_pk(config), + }, + ) + ) + self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN) diff --git a/pyproject.toml b/pyproject.toml index bfcca58a..7cbf887c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ dependencies = [ "boto3==1.43.8", "botocore==1.43.8", "cbor2==6.1.1", - "celery[redis]==5.6.3", "cfgv==3.5.0", "channels==4.3.2", "channels-redis==4.3.0", @@ -21,7 +20,6 @@ dependencies = [ "daphne==4.2.1", "Deprecated==1.3.1", "dj-database-url==3.1.2", - "dj-rest-auth==7.2.0", "django-allauth==65.16.1", "django-anymail==15.0", "django-cors-headers==4.9.0", @@ -32,15 +30,9 @@ dependencies = [ "django-types==0.24.0", "django-unfold==0.93.0", "Django==6.0.5", - "djangochannelsrestframework==1.3.0", - "djangorestframework-simplejwt==5.5.1", - "djangorestframework==3.17.1", "djlint==1.36.4", "dnspython==2.8.0", "docutils==0.22.4", - "drf-nested-routers==0.95.0", - "drf-spectacular==0.29.0", - "drf-standardized-errors==0.16.0", "editorconfig==0.17.1", "feedparser==6.0.12", "gunicorn==26.0.0", @@ -66,7 +58,6 @@ dependencies = [ "nodeenv==1.10.0", "opentelemetry-api>=1.41.1", "opentelemetry-exporter-otlp-proto-http>=1.41.1,<2", - "opentelemetry-instrumentation-celery>=0.62b1,<1", "opentelemetry-instrumentation-django>=0.62b1,<1", "opentelemetry-sdk>=1.41.1", "ormsgpack==1.12.2", @@ -77,6 +68,7 @@ dependencies = [ "py-ubjson==0.16.1", "pyasn1-modules==0.4.2", "pyasn1==0.6.3", + "PyJWT==2.12.1", "pylint-django==2.7.0", "pylint-plugin-utils==0.9.0", "pyopenssl==26.2.0", @@ -94,6 +86,9 @@ dependencies = [ "standardwebhooks==1.0.1", "structlog==25.5.0", "svix==1.93.0", + "taskiq==0.12.4", + "taskiq-aio-pika==0.4.2", + "taskiq-redis==1.0.0", "twisted==26.4.0", "txaio==25.12.2", "types-Deprecated==1.3.1.20260508", @@ -109,6 +104,7 @@ dependencies = [ "wrapt==2.1.2", "xxhash==3.7.0", "zope-interface==8.4", + "django-ninja>=1.6.2", ] [dependency-groups] diff --git a/scripts/bootstrap_dev.sh b/scripts/bootstrap_dev.sh index 2ac34e8d..5e80a60c 100755 --- a/scripts/bootstrap_dev.sh +++ b/scripts/bootstrap_dev.sh @@ -28,7 +28,8 @@ uv sync --frozen corepack enable corepack prepare pnpm@11.1.0 --activate -pnpm install --filter=@digestengine/frontend +pnpm install --frozen-lockfile +./scripts/ensure_turbo_link.sh if git rev-parse --git-dir >/dev/null 2>&1; then .venv/bin/pre-commit install --install-hooks diff --git a/scripts/disaster_recovery_rehearsal.sh b/scripts/disaster_recovery_rehearsal.sh index 86733546..d913fb92 100644 --- a/scripts/disaster_recovery_rehearsal.sh +++ b/scripts/disaster_recovery_rehearsal.sh @@ -21,7 +21,8 @@ require_command pg_dump require_env STAGING_NAMESPACE require_env STAGING_DJANGO_DEPLOYMENT -require_env STAGING_CELERY_DEPLOYMENT +require_env STAGING_TASKIQ_WORKER_DEPLOYMENT +require_env STAGING_TASKIQ_SCHEDULER_DEPLOYMENT require_env STAGING_POSTGRES_URL require_env QDRANT_SNAPSHOT_URL @@ -52,10 +53,16 @@ kubectl rollout status \ | tee "$run_dir/django-rollout.txt" kubectl rollout status \ - "deployment/${STAGING_CELERY_DEPLOYMENT}" \ + "deployment/${STAGING_TASKIQ_WORKER_DEPLOYMENT}" \ -n "$STAGING_NAMESPACE" \ --timeout=180s \ - | tee "$run_dir/celery-rollout.txt" + | tee "$run_dir/taskiq-worker-rollout.txt" + +kubectl rollout status \ + "deployment/${STAGING_TASKIQ_SCHEDULER_DEPLOYMENT}" \ + -n "$STAGING_NAMESPACE" \ + --timeout=180s \ + | tee "$run_dir/taskiq-scheduler-rollout.txt" kubectl get pods -n "$STAGING_NAMESPACE" -o wide > "$run_dir/pods.txt" kubectl get jobs -n "$STAGING_NAMESPACE" > "$run_dir/jobs.txt" @@ -66,7 +73,8 @@ Artifacts: - PostgreSQL backup: $run_dir/staging-postgres.pgdump - Qdrant snapshot response: $run_dir/qdrant-snapshot.json - Django rollout status: $run_dir/django-rollout.txt -- Celery rollout status: $run_dir/celery-rollout.txt +- Taskiq worker rollout status: $run_dir/taskiq-worker-rollout.txt +- Taskiq scheduler rollout status: $run_dir/taskiq-scheduler-rollout.txt - Pod inventory: $run_dir/pods.txt - Job inventory: $run_dir/jobs.txt EOF diff --git a/scripts/ensure_turbo_link.sh b/scripts/ensure_turbo_link.sh new file mode 100755 index 00000000..28f230f2 --- /dev/null +++ b/scripts/ensure_turbo_link.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env sh +set -eu + +cd "$(dirname "$0")/.." + +if [ -e node_modules/turbo/bin/turbo ]; then + exit 0 +fi + +target=$(node <<'NODE' +const fs = require('fs'); +const path = require('path'); + +const pnpmDir = path.join(process.cwd(), 'node_modules', '.pnpm'); +if (!fs.existsSync(pnpmDir)) { + process.exit(1); +} + +const candidates = fs + .readdirSync(pnpmDir) + .filter((name) => /^turbo@\d/.test(name)) + .sort((left, right) => left.localeCompare(right, undefined, { numeric: true })); + +const match = candidates.at(-1); +if (!match) { + process.exit(1); +} + +process.stdout.write(path.join('.pnpm', match, 'node_modules', 'turbo')); +NODE +) + +if [ -z "$target" ]; then + echo "Unable to locate the installed turbo package. Run 'pnpm install --frozen-lockfile' from the repo root." >&2 + exit 1 +fi + +rm -rf node_modules/turbo +ln -s "$target" node_modules/turbo diff --git a/stylelint.config.mjs b/stylelint.config.mjs index a11b4472..cd7d3ac9 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -44,4 +44,4 @@ export default { "rule-empty-line-before": null, "value-keyword-case": null, }, -} \ No newline at end of file +} diff --git a/trends/api.py b/trends/api.py deleted file mode 100644 index d0ea84ee..00000000 --- a/trends/api.py +++ /dev/null @@ -1,653 +0,0 @@ -"""Trends-domain API viewsets kept under the existing nested project routes.""" - -from typing import Any - -from django.conf import settings -from django.db.models import Avg, Count, OuterRef, Prefetch, Q, Subquery -from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer -from rest_framework import serializers, status, viewsets -from rest_framework.decorators import action -from rest_framework.filters import OrderingFilter -from rest_framework.response import Response - -from content.models import Content -from core.api import ( - AUTHENTICATION_REQUIRED_RESPONSE, - ProjectOwnedQuerysetMixin, - build_crud_action_overrides, - document_project_owned_viewset, -) -from core.permissions import IsProjectContributor, IsProjectMember -from trends.models import ( - ContentClusterMembership, - OriginalContentIdea, - SourceDiversitySnapshot, - ThemeSuggestion, - TopicCentroidSnapshot, - TopicCluster, - TopicVelocitySnapshot, - TrendTaskRun, -) -from trends.observability import TRACKED_TREND_TASKS -from trends.serializers import ( - OriginalContentIdeaDismissSerializer, - OriginalContentIdeaSerializer, - SourceDiversityObservabilitySummarySerializer, - SourceDiversitySnapshotSerializer, - ThemeSuggestionDismissSerializer, - ThemeSuggestionSerializer, - TopicCentroidObservabilitySummarySerializer, - TopicCentroidSnapshotSerializer, - TopicClusterDetailSerializer, - TopicClusterSerializer, - TopicVelocitySnapshotSerializer, - TrendTaskRunObservabilitySummarySerializer, - TrendTaskRunSerializer, -) -from trends.tasks import ( - accept_original_content_idea, - accept_theme_suggestion, - dismiss_original_content_idea, - dismiss_theme_suggestion, - generate_original_content_ideas, - mark_original_content_idea_written, -) - - -def _require_pk(instance: Any) -> int: - """Return a saved model primary key for trends API response payloads.""" - - instance_pk = getattr(instance, "pk", None) - if instance_pk is None: - raise ValueError(f"{instance.__class__.__name__} must be saved first.") - return int(instance_pk) - - -@document_project_owned_viewset( - resource_plural="topic clusters", - resource_singular="topic cluster", - create_description="Topic clusters are pipeline-managed analysis rows and are exposed read-only for trend exploration.", - tag="Trend Analysis", - action_overrides=build_crud_action_overrides( - TopicClusterSerializer, - resource_plural="topic clusters for the selected project", - resource_singular="topic cluster", - ), -) -class TopicClusterViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect a project's current topic clusters and velocity history.""" - - serializer_class = TopicClusterSerializer - filter_backends = [OrderingFilter] - ordering_fields = [ - "velocity_score", - "member_count", - "last_seen_at", - "first_seen_at", - ] - ordering = ["-velocity_score", "-last_seen_at"] - queryset = TopicCluster.objects.select_related("project", "dominant_entity") - - def get_queryset(self): - """Annotate clusters with the latest persisted velocity metrics.""" - - latest_snapshot_queryset = TopicVelocitySnapshot.objects.filter( - cluster_id=OuterRef("pk") - ).order_by("-computed_at") - queryset = ( - super() - .get_queryset() - .annotate( - velocity_score=Subquery( - latest_snapshot_queryset.values("velocity_score")[:1] - ), - z_score=Subquery(latest_snapshot_queryset.values("z_score")[:1]), - window_count=Subquery( - latest_snapshot_queryset.values("window_count")[:1] - ), - velocity_computed_at=Subquery( - latest_snapshot_queryset.values("computed_at")[:1] - ), - ) - ) - if self.action == "retrieve": - queryset = queryset.prefetch_related( - Prefetch( - "memberships", - queryset=ContentClusterMembership.objects.select_related( - "content" - ).order_by("-similarity", "-assigned_at"), - ), - Prefetch( - "velocity_snapshots", - queryset=TopicVelocitySnapshot.objects.order_by("-computed_at"), - ), - ) - return queryset - - def get_serializer_class(self) -> Any: - """Return the detail serializer for cluster drill-down responses.""" - - if self.action == "retrieve": - return TopicClusterDetailSerializer - return super().get_serializer_class() - - def get_permissions(self): - """Allow project members to inspect trend clusters.""" - - return [IsProjectMember()] - - @extend_schema( - summary="List velocity history", - description=( - "Return persisted velocity snapshots for one topic cluster. " - "Use the optional limit query parameter to cap the number of snapshots returned." - ), - parameters=[ - OpenApiParameter( - name="limit", - type=int, - location=OpenApiParameter.QUERY, - description="Maximum number of velocity snapshots to return.", - required=False, - ) - ], - request=None, - responses={ - 200: TopicVelocitySnapshotSerializer(many=True), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["get"], url_path="velocity_history") - def velocity_history(self, request, *args, **kwargs): - """Return recent velocity snapshots for the selected topic cluster.""" - - cluster = self.get_object() - snapshots = cluster.velocity_snapshots.order_by("-computed_at") - limit_param = request.query_params.get("limit") - if limit_param: - try: - limit = max(1, min(int(limit_param), 100)) - except ValueError as exc: - raise serializers.ValidationError( - {"limit": "Limit must be an integer between 1 and 100."} - ) from exc - snapshots = snapshots[:limit] - serializer = TopicVelocitySnapshotSerializer(snapshots, many=True) - return Response(serializer.data) - - -@document_project_owned_viewset( - resource_plural="theme suggestions", - resource_singular="theme suggestion", - create_description="Theme suggestions are pipeline-managed rows and are exposed read-only aside from editorial workflow actions.", - tag="Trend Analysis", - action_overrides=build_crud_action_overrides( - ThemeSuggestionSerializer, - resource_plural="theme suggestions for the selected project", - resource_singular="theme suggestion", - ), -) -class ThemeSuggestionViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect and resolve project-scoped theme suggestions.""" - - serializer_class = ThemeSuggestionSerializer - filter_backends = [OrderingFilter] - ordering_fields = ["created_at", "velocity_at_creation", "novelty_score", "status"] - ordering = ["status", "-velocity_at_creation", "-created_at"] - queryset = ThemeSuggestion.objects.select_related( - "project", "cluster", "decided_by" - ) - - def get_queryset(self): - """Annotate suggestion clusters with their latest velocity when present.""" - - latest_snapshot_queryset = TopicVelocitySnapshot.objects.filter( - cluster_id=OuterRef("cluster_id") - ).order_by("-computed_at") - return ( - super() - .get_queryset() - .select_related("cluster__dominant_entity") - .prefetch_related( - Prefetch( - "promoted_contents", - queryset=Content.objects.order_by( - "-newsletter_promotion_at", "-published_date" - ), - ) - ) - .annotate( - cluster__velocity_score=Subquery( - latest_snapshot_queryset.values("velocity_score")[:1] - ) - ) - ) - - def get_permissions(self): - """Allow members to read suggestions and contributors to resolve them.""" - - if self.action in {"accept", "dismiss"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - @extend_schema( - summary="Accept theme suggestion", - description="Mark a pending theme suggestion as accepted by the current editor.", - request=None, - responses={ - 200: ThemeSuggestionSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["post"], url_path="accept") - def accept(self, request, *args, **kwargs): - """Accept the selected pending theme suggestion.""" - - suggestion = self.get_object() - try: - accept_theme_suggestion(suggestion, user_id=request.user.id) - except ValueError as exc: - raise serializers.ValidationError( - {"status": "Unable to accept this theme suggestion."} - ) from exc - suggestion = self.get_queryset().get(pk=suggestion.pk) - serializer = self.get_serializer(suggestion) - return Response(serializer.data) - - @extend_schema( - summary="Dismiss theme suggestion", - description="Dismiss a pending theme suggestion and persist the editor's reason.", - request=ThemeSuggestionDismissSerializer, - responses={ - 200: ThemeSuggestionSerializer, - 400: ThemeSuggestionDismissSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["post"], url_path="dismiss") - def dismiss(self, request, *args, **kwargs): - """Dismiss the selected pending theme suggestion.""" - - suggestion = self.get_object() - serializer = ThemeSuggestionDismissSerializer( - data=request.data, - context=self.get_serializer_context(), - ) - serializer.is_valid(raise_exception=True) - try: - dismiss_theme_suggestion( - suggestion, - user_id=request.user.id, - reason=serializer.validated_data["reason"], - ) - except ValueError as exc: - raise serializers.ValidationError( - {"status": "Unable to dismiss this theme suggestion."} - ) from exc - response_serializer = self.get_serializer(suggestion) - return Response(response_serializer.data) - - -@document_project_owned_viewset( - resource_plural="original content ideas", - resource_singular="original content idea", - create_description="Original content ideas are pipeline-managed rows and are exposed read-only aside from editorial workflow actions.", - tag="Trend Analysis", - action_overrides=build_crud_action_overrides( - OriginalContentIdeaSerializer, - resource_plural="original content ideas for the selected project", - resource_singular="original content idea", - ), -) -class OriginalContentIdeaViewSet( - ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet -): - """Inspect and resolve project-scoped original-content ideas.""" - - serializer_class = OriginalContentIdeaSerializer - filter_backends = [OrderingFilter] - ordering_fields = ["created_at", "self_critique_score", "status"] - ordering = ["status", "-self_critique_score", "-created_at"] - queryset = OriginalContentIdea.objects.select_related( - "project", "related_cluster", "decided_by" - ) - - def get_queryset(self): - """Prefetch supporting contents for original-content idea responses.""" - - return ( - super() - .get_queryset() - .select_related("related_cluster__dominant_entity") - .prefetch_related( - Prefetch( - "supporting_contents", - queryset=Content.objects.order_by("-published_date", "-id"), - ) - ) - ) - - def get_permissions(self): - """Allow members to read ideas and contributors to resolve them.""" - - if self.action in {"accept", "dismiss", "mark_written", "generate"}: - return [IsProjectContributor()] - return [IsProjectMember()] - - @extend_schema( - summary="Generate original content ideas", - description=( - "Trigger original-content idea generation for the selected project. " - "When Celery runs eagerly, ideas are generated before the response is returned. " - "Otherwise, the generation task is queued for background execution." - ), - request=None, - responses={ - 200: inline_serializer( - name="OriginalContentIdeaGenerateCompletedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - "result": inline_serializer( - name="OriginalContentIdeaGenerateResult", - fields={ - "project_id": serializers.IntegerField(), - "clusters_considered": serializers.IntegerField(), - "created": serializers.IntegerField(), - "skipped": serializers.IntegerField(), - }, - ), - }, - ), - 202: inline_serializer( - name="OriginalContentIdeaGenerateQueuedResponse", - fields={ - "status": serializers.CharField(), - "project_id": serializers.IntegerField(), - }, - ), - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=False, methods=["post"], url_path="generate") - def generate(self, request, *args, **kwargs): - """Trigger original-content idea generation for the current project.""" - - project = self.get_project() - project_id = _require_pk(project) - if settings.CELERY_TASK_ALWAYS_EAGER: - result = generate_original_content_ideas(project_id) - return Response( - { - "status": "completed", - "project_id": project_id, - "result": result, - } - ) - generate_original_content_ideas.delay(project_id) - return Response( - {"status": "queued", "project_id": project_id}, - status=status.HTTP_202_ACCEPTED, - ) - - @extend_schema( - summary="Accept original content idea", - description="Mark a pending original content idea as accepted by the current editor.", - request=None, - responses={ - 200: OriginalContentIdeaSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["post"], url_path="accept") - def accept(self, request, *args, **kwargs): - """Accept the selected pending original-content idea.""" - - idea = self.get_object() - try: - accept_original_content_idea(idea, user_id=request.user.id) - except ValueError as exc: - raise serializers.ValidationError( - {"status": "Unable to accept this original content idea."} - ) from exc - response_serializer = self.get_serializer(idea) - return Response(response_serializer.data) - - @extend_schema( - summary="Dismiss original content idea", - description="Dismiss a pending original content idea and persist the editor's reason.", - request=OriginalContentIdeaDismissSerializer, - responses={ - 200: OriginalContentIdeaSerializer, - 400: OriginalContentIdeaDismissSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["post"], url_path="dismiss") - def dismiss(self, request, *args, **kwargs): - """Dismiss the selected pending original-content idea.""" - - idea = self.get_object() - serializer = OriginalContentIdeaDismissSerializer( - data=request.data, - context=self.get_serializer_context(), - ) - serializer.is_valid(raise_exception=True) - try: - dismiss_original_content_idea( - idea, - user_id=request.user.id, - reason=serializer.validated_data["reason"], - ) - except ValueError as exc: - raise serializers.ValidationError( - {"status": "Unable to dismiss this original content idea."} - ) from exc - response_serializer = self.get_serializer(idea) - return Response(response_serializer.data) - - @extend_schema( - summary="Mark original content idea written", - description="Mark an accepted original content idea as written by the current editor.", - request=None, - responses={ - 200: OriginalContentIdeaSerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Trend Analysis"], - ) - @action(detail=True, methods=["post"], url_path="mark_written") - def mark_written(self, request, *args, **kwargs): - """Mark the selected accepted original-content idea as written.""" - - idea = self.get_object() - try: - mark_original_content_idea_written(idea, user_id=request.user.id) - except ValueError as exc: - raise serializers.ValidationError( - {"status": "Unable to mark this original content idea as written."} - ) from exc - response_serializer = self.get_serializer(idea) - return Response(response_serializer.data) - - -@document_project_owned_viewset( - resource_plural="topic centroid snapshots", - resource_singular="topic centroid snapshot", - create_description="Topic centroid snapshots are pipeline-managed history rows and are exposed read-only for observability.", - tag="Observability", - action_overrides=build_crud_action_overrides( - TopicCentroidSnapshotSerializer, - resource_plural="topic centroid snapshots for the selected project", - resource_singular="topic centroid snapshot", - ), -) -class TopicCentroidSnapshotViewSet( - ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet -): - """Inspect persisted centroid history and aggregate drift for a project.""" - - serializer_class = TopicCentroidSnapshotSerializer - queryset = TopicCentroidSnapshot.objects.select_related("project") - - def get_permissions(self): - """Restrict centroid observability to project contributors.""" - return [IsProjectContributor()] - - @extend_schema( - summary="Get topic centroid summary", - description=( - "Return aggregate centroid observability metrics for the selected project, " - "including average drift and the latest persisted snapshot." - ), - request=None, - responses={ - 200: TopicCentroidObservabilitySummarySerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Observability"], - ) - @action(detail=False, methods=["get"], url_path="summary") - def summary(self, request, *args, **kwargs): - """Return centroid observability summary metrics for the current project.""" - - queryset = self.get_queryset() - metrics = queryset.aggregate( - snapshot_count=Count("id"), - active_snapshot_count=Count("id", filter=Q(centroid_active=True)), - avg_drift_from_previous=Avg("drift_from_previous"), - avg_drift_from_week_ago=Avg("drift_from_week_ago"), - ) - serializer = TopicCentroidObservabilitySummarySerializer( - { - "project": _require_pk(self.get_project()), - "snapshot_count": metrics["snapshot_count"], - "active_snapshot_count": metrics["active_snapshot_count"], - "avg_drift_from_previous": metrics["avg_drift_from_previous"], - "avg_drift_from_week_ago": metrics["avg_drift_from_week_ago"], - "latest_snapshot": queryset.order_by("-computed_at").first(), - }, - context=self.get_serializer_context(), - ) - return Response(serializer.data) - - -@document_project_owned_viewset( - resource_plural="source diversity snapshots", - resource_singular="source diversity snapshot", - create_description="Source diversity snapshots are pipeline-managed observability rows and are exposed read-only for health analysis.", - tag="Observability", - action_overrides=build_crud_action_overrides( - SourceDiversitySnapshotSerializer, - resource_plural="source diversity snapshots for the selected project", - resource_singular="source diversity snapshot", - ), -) -class SourceDiversitySnapshotViewSet( - ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet -): - """Inspect persisted source-diversity history for a project.""" - - serializer_class = SourceDiversitySnapshotSerializer - queryset = SourceDiversitySnapshot.objects.select_related("project") - - def get_permissions(self): - """Restrict source-diversity observability to project contributors.""" - - return [IsProjectContributor()] - - @extend_schema( - summary="Get source diversity summary", - description=( - "Return the latest persisted source-diversity snapshot for the selected project " - "along with the number of stored snapshots." - ), - request=None, - responses={ - 200: SourceDiversityObservabilitySummarySerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Observability"], - ) - @action(detail=False, methods=["get"], url_path="summary") - def summary(self, request, *args, **kwargs): - """Return source-diversity summary metrics for the current project.""" - - queryset = self.get_queryset() - serializer = SourceDiversityObservabilitySummarySerializer( - { - "project": _require_pk(self.get_project()), - "snapshot_count": queryset.count(), - "latest_snapshot": queryset.order_by("-computed_at").first(), - }, - context=self.get_serializer_context(), - ) - return Response(serializer.data) - - -@document_project_owned_viewset( - resource_plural="trend task runs", - resource_singular="trend task run", - create_description="Trend task runs are pipeline-managed execution records and are exposed read-only for observability.", - tag="Observability", - action_overrides=build_crud_action_overrides( - TrendTaskRunSerializer, - resource_plural="trend task runs for the selected project", - resource_singular="trend task run", - ), -) -class TrendTaskRunViewSet(ProjectOwnedQuerysetMixin, viewsets.ReadOnlyModelViewSet): - """Inspect persisted trend pipeline task executions for a project.""" - - serializer_class = TrendTaskRunSerializer - queryset = TrendTaskRun.objects.select_related("project") - - def get_permissions(self): - """Restrict trend task observability to project contributors.""" - - return [IsProjectContributor()] - - @extend_schema( - summary="Get trend task run summary", - description=( - "Return the latest persisted run for each tracked trend pipeline task, " - "along with project-level run counts and failure counts." - ), - request=None, - responses={ - 200: TrendTaskRunObservabilitySummarySerializer, - 403: AUTHENTICATION_REQUIRED_RESPONSE, - }, - tags=["Observability"], - ) - @action(detail=False, methods=["get"], url_path="summary") - def summary(self, request, *args, **kwargs): - """Return the latest project-scoped run for each tracked trend task.""" - - queryset = self.get_queryset().order_by("task_name", "-started_at") - latest_by_task: dict[str, TrendTaskRun] = {} - for task_run in queryset: - latest_by_task.setdefault(task_run.task_name, task_run) - - latest_runs = [ - latest_by_task[task_name] - for task_name in TRACKED_TREND_TASKS - if task_name in latest_by_task - ] - serializer = TrendTaskRunObservabilitySummarySerializer( - { - "project": _require_pk(self.get_project()), - "run_count": queryset.count(), - "failed_run_count": queryset.filter(status="failed").count(), - "latest_runs": latest_runs, - }, - context=self.get_serializer_context(), - ) - return Response(serializer.data) diff --git a/trends/api_urls.py b/trends/api_urls.py deleted file mode 100644 index c1f81921..00000000 --- a/trends/api_urls.py +++ /dev/null @@ -1,47 +0,0 @@ -"""API route registration for trends resources.""" - -from rest_framework_nested.routers import NestedSimpleRouter - -from trends.api import ( - OriginalContentIdeaViewSet, - SourceDiversitySnapshotViewSet, - ThemeSuggestionViewSet, - TopicCentroidSnapshotViewSet, - TopicClusterViewSet, - TrendTaskRunViewSet, -) - - -def register_project_routes(project_router: NestedSimpleRouter) -> None: - """Register nested trend observability endpoints.""" - - project_router.register( - r"clusters", - TopicClusterViewSet, - basename="project-topic-cluster", - ) - project_router.register( - r"themes", - ThemeSuggestionViewSet, - basename="project-theme-suggestion", - ) - project_router.register( - r"ideas", - OriginalContentIdeaViewSet, - basename="project-original-content-idea", - ) - project_router.register( - r"topic-centroid-snapshots", - TopicCentroidSnapshotViewSet, - basename="project-topic-centroid-snapshot", - ) - project_router.register( - r"source-diversity-snapshots", - SourceDiversitySnapshotViewSet, - basename="project-source-diversity-snapshot", - ) - project_router.register( - r"trend-task-runs", - TrendTaskRunViewSet, - basename="project-trend-task-run", - ) diff --git a/trends/ninja_api.py b/trends/ninja_api.py new file mode 100644 index 00000000..954b147f --- /dev/null +++ b/trends/ninja_api.py @@ -0,0 +1,1203 @@ +"""Django Ninja endpoints for project-scoped trends and observability resources.""" + +from __future__ import annotations + +import datetime +from typing import Any, cast +from uuid import UUID + +from django.db.models import Avg, Count, OuterRef, Prefetch, Q, Subquery +from ninja import Body, Path, Router, Schema +from ninja.errors import HttpError +from ninja.responses import Status + +from content.models import Content +from core.ninja_api import api_authenticate +from digest_engine.taskiq import enqueue_task, task_always_eager +from projects.models import Project, ProjectMembership, ProjectRole +from projects.ninja_helpers import _get_project_or_404 +from trends.models import ( + ContentClusterMembership, + OriginalContentIdea, + SourceDiversitySnapshot, + ThemeSuggestion, + TopicCentroidSnapshot, + TopicCluster, + TopicVelocitySnapshot, + TrendTaskRun, +) +from trends.observability import TRACKED_TREND_TASKS +from trends.tasks import ( + accept_original_content_idea, + accept_theme_suggestion, + dismiss_original_content_idea, + dismiss_theme_suggestion, + generate_original_content_ideas, + mark_original_content_idea_written, +) + +clusters_router = Router(tags=["Trend Analysis"]) +themes_router = Router(tags=["Trend Analysis"]) +ideas_router = Router(tags=["Trend Analysis"]) +topic_centroid_snapshots_router = Router(tags=["Observability"]) +source_diversity_snapshots_router = Router(tags=["Observability"]) +trend_task_runs_router = Router(tags=["Observability"]) + + +class ErrorResponseSchema(Schema): + """Simple field-to-message error response payload.""" + + message: str + + +class DismissInputSchema(Schema): + """Dismissal reason payload for workflow actions.""" + + reason: str + + +class TopicClusterEntitySchema(Schema): + """Dominant entity summary for one topic cluster.""" + + id: int + name: str + type: str + + +class TopicClusterContentSummarySchema(Schema): + """Content fields surfaced on cluster detail responses.""" + + id: int + url: str + title: str + published_date: datetime.datetime + source_plugin: str + + +class ContentClusterMembershipSchema(Schema): + """One content membership inside a topic cluster.""" + + id: int + content: TopicClusterContentSummarySchema + similarity: float + assigned_at: datetime.datetime + + +class TopicVelocitySnapshotSchema(Schema): + """One persisted topic velocity snapshot.""" + + id: int + cluster: int + project: int + computed_at: datetime.datetime + window_count: int + trailing_mean: float + trailing_stddev: float + z_score: float + velocity_score: float + + +class TopicClusterSchema(Schema): + """One topic cluster with its current velocity rollup.""" + + id: int + project: int + centroid_vector_id: UUID + label: str + first_seen_at: datetime.datetime + last_seen_at: datetime.datetime + is_active: bool + member_count: int + dominant_entity: TopicClusterEntitySchema | None = None + velocity_score: float | None = None + z_score: float | None = None + window_count: int | None = None + velocity_computed_at: datetime.datetime | None = None + + +class TopicClusterDetailSchema(TopicClusterSchema): + """One topic cluster with memberships and snapshot history.""" + + memberships: list[ContentClusterMembershipSchema] + velocity_history: list[TopicVelocitySnapshotSchema] + + +class ThemeSuggestionClusterSummarySchema(Schema): + """Cluster summary embedded in one theme suggestion.""" + + id: int + label: str + member_count: int + velocity_score: float | None = None + + +class ThemeSuggestionPromotedContentSchema(Schema): + """Content row marked for newsletter promotion by a theme.""" + + id: int + url: str + title: str + published_date: datetime.datetime + source_plugin: str + newsletter_promotion_at: datetime.datetime | None = None + + +class ThemeSuggestionSchema(Schema): + """Editor-facing theme suggestion payload.""" + + id: int + project: int + cluster: ThemeSuggestionClusterSummarySchema | None = None + title: str + pitch: str + why_it_matters: str + suggested_angle: str + velocity_at_creation: float + novelty_score: float + status: str + dismissal_reason: str + created_at: datetime.datetime + decided_at: datetime.datetime | None = None + decided_by: int | None = None + decided_by_username: str | None = None + promoted_contents: list[ThemeSuggestionPromotedContentSchema] + + +class OriginalContentIdeaClusterSummarySchema(Schema): + """Related cluster summary embedded in one original content idea.""" + + id: int + label: str + member_count: int + + +class OriginalContentIdeaSupportingContentSchema(Schema): + """Supporting content row linked to one original content idea.""" + + id: int + url: str + title: str + published_date: datetime.datetime + source_plugin: str + + +class OriginalContentIdeaSchema(Schema): + """Editor-facing original content idea payload.""" + + id: int + project: int + angle_title: str + summary: str + suggested_outline: str + why_now: str + supporting_contents: list[OriginalContentIdeaSupportingContentSchema] + related_cluster: OriginalContentIdeaClusterSummarySchema | None = None + generated_by_model: str + self_critique_score: float + status: str + dismissal_reason: str + created_at: datetime.datetime + decided_at: datetime.datetime | None = None + decided_by: int | None = None + decided_by_username: str | None = None + + +class OriginalContentIdeaGenerateResultSchema(Schema): + """Immediate generation counts for original content ideas.""" + + project_id: int + clusters_considered: int + created: int + skipped: int + + +class OriginalContentIdeaGenerateCompletedSchema(Schema): + """Completed original content idea generation response.""" + + status: str + project_id: int + result: OriginalContentIdeaGenerateResultSchema + + +class OriginalContentIdeaGenerateQueuedSchema(Schema): + """Queued original content idea generation response.""" + + status: str + project_id: int + + +class TopicCentroidSnapshotSchema(Schema): + """One persisted topic-centroid recomputation for a project.""" + + id: int + project: int + computed_at: datetime.datetime + centroid_active: bool + feedback_count: int + upvote_count: int + downvote_count: int + drift_from_previous: float | None = None + drift_from_week_ago: float | None = None + + +class TopicCentroidObservabilitySummarySchema(Schema): + """Project-level centroid observability summary metrics.""" + + project: int + snapshot_count: int + active_snapshot_count: int + avg_drift_from_previous: float | None = None + avg_drift_from_week_ago: float | None = None + latest_snapshot: TopicCentroidSnapshotSchema | None = None + + +class SourceDiversitySnapshotSchema(Schema): + """One persisted source-diversity snapshot for a project.""" + + id: int + project: int + computed_at: datetime.datetime + window_days: int + plugin_entropy: float + source_entropy: float + author_entropy: float + cluster_entropy: float + top_plugin_share: float + top_source_share: float + breakdown: dict[str, Any] + + +class SourceDiversityObservabilitySummarySchema(Schema): + """Project-level source-diversity observability metrics.""" + + project: int + snapshot_count: int + latest_snapshot: SourceDiversitySnapshotSchema | None = None + + +class TrendTaskRunSchema(Schema): + """One persisted trend pipeline task execution.""" + + id: int + project: int + task_name: str + task_run_id: UUID + status: str + started_at: datetime.datetime + finished_at: datetime.datetime | None = None + latency_ms: int | None = None + error_message: str + summary: dict[str, Any] + + +class TrendTaskRunObservabilitySummarySchema(Schema): + """Latest trend task runs plus project-level rollup counts.""" + + project: int + run_count: int + failed_run_count: int + latest_runs: list[TrendTaskRunSchema] + + +def _require_pk(instance: Any) -> int: + """Return a saved model primary key for trends API response payloads.""" + + instance_pk = getattr(instance, "pk", None) + if instance_pk is None: + raise ValueError(f"{instance.__class__.__name__} must be saved first.") + return int(instance_pk) + + +def _serialize_topic_cluster(cluster: TopicCluster) -> dict[str, Any]: + """Return one topic-cluster response body.""" + + dominant_entity = None + if cluster.dominant_entity_id is not None and cluster.dominant_entity is not None: + dominant_entity = { + "id": cluster.dominant_entity_id, + "name": cluster.dominant_entity.name, + "type": cluster.dominant_entity.type, + } + return { + "id": _require_pk(cluster), + "project": cluster.project_id, + "centroid_vector_id": cluster.centroid_vector_id, + "label": cluster.label, + "first_seen_at": cluster.first_seen_at, + "last_seen_at": cluster.last_seen_at, + "is_active": cluster.is_active, + "member_count": cluster.member_count, + "dominant_entity": dominant_entity, + "velocity_score": cast(float | None, getattr(cluster, "velocity_score", None)), + "z_score": cast(float | None, getattr(cluster, "z_score", None)), + "window_count": cast(int | None, getattr(cluster, "window_count", None)), + "velocity_computed_at": cast( + datetime.datetime | None, + getattr(cluster, "velocity_computed_at", None), + ), + } + + +def _serialize_membership(membership: ContentClusterMembership) -> dict[str, Any]: + """Return one content cluster membership response body.""" + + return { + "id": _require_pk(membership), + "content": { + "id": membership.content_id, + "url": membership.content.url, + "title": membership.content.title, + "published_date": membership.content.published_date, + "source_plugin": membership.content.source_plugin, + }, + "similarity": membership.similarity, + "assigned_at": membership.assigned_at, + } + + +def _serialize_topic_cluster_detail(cluster: TopicCluster) -> dict[str, Any]: + """Return one detailed topic-cluster response body.""" + + payload = _serialize_topic_cluster(cluster) + payload["memberships"] = [ + _serialize_membership(membership) for membership in cluster.memberships.all() + ] + payload["velocity_history"] = [ + _serialize_velocity_snapshot(snapshot) + for snapshot in cluster.velocity_snapshots.all() + ] + return payload + + +def _serialize_velocity_snapshot(snapshot: TopicVelocitySnapshot) -> dict[str, Any]: + """Return one topic velocity snapshot response body.""" + + return { + "id": _require_pk(snapshot), + "cluster": snapshot.cluster_id, + "project": snapshot.project_id, + "computed_at": snapshot.computed_at, + "window_count": snapshot.window_count, + "trailing_mean": snapshot.trailing_mean, + "trailing_stddev": snapshot.trailing_stddev, + "z_score": snapshot.z_score, + "velocity_score": snapshot.velocity_score, + } + + +def _serialize_theme_promoted_content(content: Content) -> dict[str, Any]: + """Return one promoted-content response body.""" + + return { + "id": _require_pk(content), + "url": content.url, + "title": content.title, + "published_date": content.published_date, + "source_plugin": content.source_plugin, + "newsletter_promotion_at": content.newsletter_promotion_at, + } + + +def _serialize_theme_suggestion(suggestion: ThemeSuggestion) -> dict[str, Any]: + """Return one theme suggestion response body.""" + + cluster_payload = None + if suggestion.cluster_id is not None and suggestion.cluster is not None: + cluster_payload = { + "id": suggestion.cluster_id, + "label": suggestion.cluster.label, + "member_count": suggestion.cluster.member_count, + "velocity_score": cast( + float | None, + getattr(suggestion, "cluster__velocity_score", None), + ), + } + return { + "id": _require_pk(suggestion), + "project": suggestion.project_id, + "cluster": cluster_payload, + "title": suggestion.title, + "pitch": suggestion.pitch, + "why_it_matters": suggestion.why_it_matters, + "suggested_angle": suggestion.suggested_angle, + "velocity_at_creation": suggestion.velocity_at_creation, + "novelty_score": suggestion.novelty_score, + "status": suggestion.status, + "dismissal_reason": suggestion.dismissal_reason, + "created_at": suggestion.created_at, + "decided_at": suggestion.decided_at, + "decided_by": suggestion.decided_by_id, + "decided_by_username": ( + suggestion.decided_by.username if suggestion.decided_by_id else None + ), + "promoted_contents": [ + _serialize_theme_promoted_content(content) + for content in suggestion.promoted_contents.all() + ], + } + + +def _serialize_original_content_supporting_content(content: Content) -> dict[str, Any]: + """Return one supporting-content response body.""" + + return { + "id": _require_pk(content), + "url": content.url, + "title": content.title, + "published_date": content.published_date, + "source_plugin": content.source_plugin, + } + + +def _serialize_original_content_idea(idea: OriginalContentIdea) -> dict[str, Any]: + """Return one original content idea response body.""" + + related_cluster = None + if idea.related_cluster_id is not None and idea.related_cluster is not None: + related_cluster = { + "id": idea.related_cluster_id, + "label": idea.related_cluster.label, + "member_count": idea.related_cluster.member_count, + } + return { + "id": _require_pk(idea), + "project": idea.project_id, + "angle_title": idea.angle_title, + "summary": idea.summary, + "suggested_outline": idea.suggested_outline, + "why_now": idea.why_now, + "supporting_contents": [ + _serialize_original_content_supporting_content(content) + for content in idea.supporting_contents.all() + ], + "related_cluster": related_cluster, + "generated_by_model": idea.generated_by_model, + "self_critique_score": idea.self_critique_score, + "status": idea.status, + "dismissal_reason": idea.dismissal_reason, + "created_at": idea.created_at, + "decided_at": idea.decided_at, + "decided_by": idea.decided_by_id, + "decided_by_username": idea.decided_by.username if idea.decided_by_id else None, + } + + +def _serialize_topic_centroid_snapshot( + snapshot: TopicCentroidSnapshot, +) -> dict[str, Any]: + """Return one topic centroid snapshot response body.""" + + return { + "id": _require_pk(snapshot), + "project": snapshot.project_id, + "computed_at": snapshot.computed_at, + "centroid_active": snapshot.centroid_active, + "feedback_count": snapshot.feedback_count, + "upvote_count": snapshot.upvote_count, + "downvote_count": snapshot.downvote_count, + "drift_from_previous": snapshot.drift_from_previous, + "drift_from_week_ago": snapshot.drift_from_week_ago, + } + + +def _serialize_source_diversity_snapshot( + snapshot: SourceDiversitySnapshot, +) -> dict[str, Any]: + """Return one source diversity snapshot response body.""" + + return { + "id": _require_pk(snapshot), + "project": snapshot.project_id, + "computed_at": snapshot.computed_at, + "window_days": snapshot.window_days, + "plugin_entropy": snapshot.plugin_entropy, + "source_entropy": snapshot.source_entropy, + "author_entropy": snapshot.author_entropy, + "cluster_entropy": snapshot.cluster_entropy, + "top_plugin_share": snapshot.top_plugin_share, + "top_source_share": snapshot.top_source_share, + "breakdown": snapshot.breakdown, + } + + +def _serialize_trend_task_run(run: TrendTaskRun) -> dict[str, Any]: + """Return one trend task run response body.""" + + return { + "id": _require_pk(run), + "project": run.project_id, + "task_name": run.task_name, + "task_run_id": run.task_run_id, + "status": run.status, + "started_at": run.started_at, + "finished_at": run.finished_at, + "latency_ms": run.latency_ms, + "error_message": run.error_message, + "summary": run.summary, + } + + +def _normalized_reason(reason: str) -> str: + """Normalize one dismissal reason and reject blank values.""" + + normalized = reason.strip() + if not normalized: + raise HttpError(400, {"reason": "Dismissal reason cannot be blank."}) + return normalized + + +def _require_project_member(request: Any, project_id: int) -> Project: + """Load one project if the current user can read project-scoped trends.""" + + return _get_project_or_404(request, project_id) + + +def _require_project_contributor(request: Any, project_id: int) -> Project: + """Load one project if the current user can mutate project-scoped trends.""" + + project = _get_project_or_404(request, project_id) + membership = ProjectMembership.objects.filter( + project=project, + user=request.user, + ).first() + if not membership or membership.role not in {ProjectRole.ADMIN, ProjectRole.MEMBER}: + raise HttpError(403, "You do not have permission to perform this action.") + return project + + +def _topic_cluster_queryset(for_detail: bool = False): + """Return the canonical annotated queryset for topic clusters.""" + + latest_snapshot_queryset = TopicVelocitySnapshot.objects.filter( + cluster_id=OuterRef("pk") + ).order_by("-computed_at") + queryset = TopicCluster.objects.select_related( + "project", "dominant_entity" + ).annotate( + velocity_score=Subquery(latest_snapshot_queryset.values("velocity_score")[:1]), + z_score=Subquery(latest_snapshot_queryset.values("z_score")[:1]), + window_count=Subquery(latest_snapshot_queryset.values("window_count")[:1]), + velocity_computed_at=Subquery( + latest_snapshot_queryset.values("computed_at")[:1] + ), + ) + if for_detail: + queryset = queryset.prefetch_related( + Prefetch( + "memberships", + queryset=ContentClusterMembership.objects.select_related( + "content" + ).order_by("-similarity", "-assigned_at"), + ), + Prefetch( + "velocity_snapshots", + queryset=TopicVelocitySnapshot.objects.order_by("-computed_at"), + ), + ) + return queryset + + +def _get_topic_cluster_or_404( + project_id: int, + cluster_id: int, + *, + for_detail: bool = False, +) -> TopicCluster: + """Load one topic cluster for the selected project.""" + + cluster = ( + _topic_cluster_queryset(for_detail=for_detail) + .filter( + project_id=project_id, + pk=cluster_id, + ) + .first() + ) + if not cluster: + raise HttpError(404, "Not found.") + return cluster + + +def _theme_suggestion_queryset(): + """Return the canonical queryset for theme suggestions.""" + + latest_snapshot_queryset = TopicVelocitySnapshot.objects.filter( + cluster_id=OuterRef("cluster_id") + ).order_by("-computed_at") + return ( + ThemeSuggestion.objects.select_related("project", "cluster", "decided_by") + .select_related("cluster__dominant_entity") + .prefetch_related( + Prefetch( + "promoted_contents", + queryset=Content.objects.order_by( + "-newsletter_promotion_at", "-published_date" + ), + ) + ) + .annotate( + cluster__velocity_score=Subquery( + latest_snapshot_queryset.values("velocity_score")[:1] + ) + ) + ) + + +def _get_theme_suggestion_or_404( + project_id: int, suggestion_id: int +) -> ThemeSuggestion: + """Load one theme suggestion for the selected project.""" + + suggestion = ( + _theme_suggestion_queryset() + .filter( + project_id=project_id, + pk=suggestion_id, + ) + .first() + ) + if not suggestion: + raise HttpError(404, "Not found.") + return suggestion + + +def _original_content_idea_queryset(): + """Return the canonical queryset for original content ideas.""" + + return OriginalContentIdea.objects.select_related( + "project", + "related_cluster", + "decided_by", + "related_cluster__dominant_entity", + ).prefetch_related( + Prefetch( + "supporting_contents", + queryset=Content.objects.order_by("-published_date", "-id"), + ) + ) + + +def _get_original_content_idea_or_404( + project_id: int, idea_id: int +) -> OriginalContentIdea: + """Load one original content idea for the selected project.""" + + idea = ( + _original_content_idea_queryset() + .filter( + project_id=project_id, + pk=idea_id, + ) + .first() + ) + if not idea: + raise HttpError(404, "Not found.") + return idea + + +def _get_topic_centroid_snapshot_or_404( + project_id: int, snapshot_id: int +) -> TopicCentroidSnapshot: + """Load one centroid snapshot for the selected project.""" + + snapshot = ( + TopicCentroidSnapshot.objects.select_related("project") + .filter( + project_id=project_id, + pk=snapshot_id, + ) + .first() + ) + if not snapshot: + raise HttpError(404, "Not found.") + return snapshot + + +def _get_source_diversity_snapshot_or_404( + project_id: int, snapshot_id: int +) -> SourceDiversitySnapshot: + """Load one source diversity snapshot for the selected project.""" + + snapshot = ( + SourceDiversitySnapshot.objects.select_related("project") + .filter( + project_id=project_id, + pk=snapshot_id, + ) + .first() + ) + if not snapshot: + raise HttpError(404, "Not found.") + return snapshot + + +def _get_trend_task_run_or_404(project_id: int, run_id: int) -> TrendTaskRun: + """Load one trend task run for the selected project.""" + + run = ( + TrendTaskRun.objects.select_related("project") + .filter( + project_id=project_id, + pk=run_id, + ) + .first() + ) + if not run: + raise HttpError(404, "Not found.") + return run + + +@clusters_router.get("/", response=list[TopicClusterSchema], auth=api_authenticate) +def list_topic_clusters(request: Any, project_id: int = Path(...)): + """List topic clusters visible to the current project member.""" + + _require_project_member(request, project_id) + clusters = ( + _topic_cluster_queryset() + .filter(project_id=project_id) + .order_by("-velocity_score", "-last_seen_at") + ) + return [_serialize_topic_cluster(cluster) for cluster in clusters] + + +@clusters_router.get( + "/{cluster_id}/", + response=dict[str, Any], + auth=api_authenticate, +) +def get_topic_cluster( + request: Any, + project_id: int = Path(...), + cluster_id: int = Path(...), +): + """Return one topic cluster with memberships and velocity history.""" + + _require_project_member(request, project_id) + return _serialize_topic_cluster_detail( + _get_topic_cluster_or_404(project_id, cluster_id, for_detail=True) + ) + + +@clusters_router.get( + "/{cluster_id}/velocity_history/", + response={200: list[TopicVelocitySnapshotSchema], 400: dict[str, str]}, + auth=api_authenticate, +) +def topic_cluster_velocity_history( + request: Any, + project_id: int = Path(...), + cluster_id: int = Path(...), + limit: int | None = None, +): + """Return recent velocity snapshots for one topic cluster.""" + + cluster = _get_topic_cluster_or_404(project_id, cluster_id) + _require_project_member(request, project_id) + snapshots = cluster.velocity_snapshots.order_by("-computed_at") + if limit is not None: + if limit < 1 or limit > 100: + return Status( + 400, + {"limit": "Limit must be an integer between 1 and 100."}, + ) + snapshots = snapshots[:limit] + return [_serialize_velocity_snapshot(snapshot) for snapshot in snapshots] + + +@themes_router.get("/", response=list[ThemeSuggestionSchema], auth=api_authenticate) +def list_theme_suggestions(request: Any, project_id: int = Path(...)): + """List theme suggestions visible to the current project member.""" + + _require_project_member(request, project_id) + suggestions = ( + _theme_suggestion_queryset() + .filter(project_id=project_id) + .order_by("status", "-velocity_at_creation", "-created_at") + ) + return [_serialize_theme_suggestion(suggestion) for suggestion in suggestions] + + +@themes_router.post( + "/{suggestion_id}/accept/", + response={200: ThemeSuggestionSchema, 400: dict[str, str]}, + auth=api_authenticate, +) +def accept_theme_suggestion_route( + request: Any, + project_id: int = Path(...), + suggestion_id: int = Path(...), +): + """Accept one pending theme suggestion.""" + + _require_project_contributor(request, project_id) + suggestion = _get_theme_suggestion_or_404(project_id, suggestion_id) + try: + accept_theme_suggestion(suggestion, user_id=request.user.id) + except ValueError: + return Status( + 400, + {"status": "Unable to accept this theme suggestion."}, + ) + return _serialize_theme_suggestion( + _get_theme_suggestion_or_404(project_id, suggestion_id) + ) + + +@themes_router.post( + "/{suggestion_id}/dismiss/", + response={200: ThemeSuggestionSchema, 400: dict[str, str]}, + auth=api_authenticate, +) +def dismiss_theme_suggestion_route( + request: Any, + payload: DismissInputSchema = Body(...), + project_id: int = Path(...), + suggestion_id: int = Path(...), +): + """Dismiss one pending theme suggestion.""" + + _require_project_contributor(request, project_id) + suggestion = _get_theme_suggestion_or_404(project_id, suggestion_id) + try: + dismiss_theme_suggestion( + suggestion, + user_id=request.user.id, + reason=_normalized_reason(payload.reason), + ) + except ValueError: + return Status( + 400, + {"status": "Unable to dismiss this theme suggestion."}, + ) + return _serialize_theme_suggestion( + _get_theme_suggestion_or_404(project_id, suggestion_id) + ) + + +@ideas_router.get("/", response=list[OriginalContentIdeaSchema], auth=api_authenticate) +def list_original_content_ideas(request: Any, project_id: int = Path(...)): + """List original content ideas visible to the current project member.""" + + _require_project_member(request, project_id) + ideas = ( + _original_content_idea_queryset() + .filter(project_id=project_id) + .order_by("status", "-self_critique_score", "-created_at") + ) + return [_serialize_original_content_idea(idea) for idea in ideas] + + +@ideas_router.post( + "/generate/", + response={ + 200: OriginalContentIdeaGenerateCompletedSchema, + 202: OriginalContentIdeaGenerateQueuedSchema, + }, + auth=api_authenticate, +) +def generate_original_content_ideas_route( + request: Any, + project_id: int = Path(...), +): + """Trigger original content idea generation for the selected project.""" + + project = _require_project_contributor(request, project_id) + resolved_project_id = _require_pk(project) + if task_always_eager(): + result = generate_original_content_ideas(resolved_project_id) + return Status( + 200, + { + "status": "completed", + "project_id": resolved_project_id, + "result": result, + }, + ) + enqueue_task(generate_original_content_ideas, resolved_project_id) + return Status(202, {"status": "queued", "project_id": resolved_project_id}) + + +@ideas_router.post( + "/{idea_id}/accept/", + response={200: OriginalContentIdeaSchema, 400: dict[str, str]}, + auth=api_authenticate, +) +def accept_original_content_idea_route( + request: Any, + project_id: int = Path(...), + idea_id: int = Path(...), +): + """Accept one pending original content idea.""" + + _require_project_contributor(request, project_id) + idea = _get_original_content_idea_or_404(project_id, idea_id) + try: + accept_original_content_idea(idea, user_id=request.user.id) + except ValueError: + return Status( + 400, + {"status": "Unable to accept this original content idea."}, + ) + return _serialize_original_content_idea( + _get_original_content_idea_or_404(project_id, idea_id) + ) + + +@ideas_router.post( + "/{idea_id}/dismiss/", + response={200: OriginalContentIdeaSchema, 400: dict[str, str]}, + auth=api_authenticate, +) +def dismiss_original_content_idea_route( + request: Any, + payload: DismissInputSchema = Body(...), + project_id: int = Path(...), + idea_id: int = Path(...), +): + """Dismiss one pending original content idea.""" + + _require_project_contributor(request, project_id) + idea = _get_original_content_idea_or_404(project_id, idea_id) + try: + dismiss_original_content_idea( + idea, + user_id=request.user.id, + reason=_normalized_reason(payload.reason), + ) + except ValueError: + return Status( + 400, + {"status": "Unable to dismiss this original content idea."}, + ) + return _serialize_original_content_idea( + _get_original_content_idea_or_404(project_id, idea_id) + ) + + +@ideas_router.post( + "/{idea_id}/mark_written/", + response={200: OriginalContentIdeaSchema, 400: dict[str, str]}, + auth=api_authenticate, +) +def mark_original_content_idea_written_route( + request: Any, + project_id: int = Path(...), + idea_id: int = Path(...), +): + """Mark one accepted original content idea as written.""" + + _require_project_contributor(request, project_id) + idea = _get_original_content_idea_or_404(project_id, idea_id) + try: + mark_original_content_idea_written(idea, user_id=request.user.id) + except ValueError: + return Status( + 400, + {"status": "Unable to mark this original content idea as written."}, + ) + return _serialize_original_content_idea( + _get_original_content_idea_or_404(project_id, idea_id) + ) + + +@topic_centroid_snapshots_router.get( + "/", + response=list[TopicCentroidSnapshotSchema], + auth=api_authenticate, +) +def list_topic_centroid_snapshots(request: Any, project_id: int = Path(...)): + """List topic centroid snapshots visible to project contributors.""" + + _require_project_contributor(request, project_id) + snapshots = TopicCentroidSnapshot.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_topic_centroid_snapshot(snapshot) for snapshot in snapshots] + + +@topic_centroid_snapshots_router.get( + "/summary/", + response=TopicCentroidObservabilitySummarySchema, + auth=api_authenticate, +) +def topic_centroid_snapshot_summary(request: Any, project_id: int = Path(...)): + """Return aggregate centroid observability metrics.""" + + project = _require_project_contributor(request, project_id) + queryset = TopicCentroidSnapshot.objects.select_related("project").filter( + project_id=project_id + ) + metrics = queryset.aggregate( + snapshot_count=Count("id"), + active_snapshot_count=Count("id", filter=Q(centroid_active=True)), + avg_drift_from_previous=Avg("drift_from_previous"), + avg_drift_from_week_ago=Avg("drift_from_week_ago"), + ) + payload = { + "project": _require_pk(project), + "snapshot_count": metrics["snapshot_count"], + "active_snapshot_count": metrics["active_snapshot_count"], + "avg_drift_from_previous": metrics["avg_drift_from_previous"], + "avg_drift_from_week_ago": metrics["avg_drift_from_week_ago"], + "latest_snapshot": queryset.order_by("-computed_at").first(), + } + return { + **payload, + "latest_snapshot": ( + _serialize_topic_centroid_snapshot(payload["latest_snapshot"]) + if payload["latest_snapshot"] is not None + else None + ), + } + + +@topic_centroid_snapshots_router.get( + "/{snapshot_id}/", + response=TopicCentroidSnapshotSchema, + auth=api_authenticate, +) +def get_topic_centroid_snapshot( + request: Any, + project_id: int = Path(...), + snapshot_id: int = Path(...), +): + """Return one topic centroid snapshot.""" + + _require_project_contributor(request, project_id) + return _serialize_topic_centroid_snapshot( + _get_topic_centroid_snapshot_or_404(project_id, snapshot_id) + ) + + +@source_diversity_snapshots_router.get( + "/", + response=list[SourceDiversitySnapshotSchema], + auth=api_authenticate, +) +def list_source_diversity_snapshots(request: Any, project_id: int = Path(...)): + """List source diversity snapshots visible to project contributors.""" + + _require_project_contributor(request, project_id) + snapshots = SourceDiversitySnapshot.objects.select_related("project").filter( + project_id=project_id + ) + return [_serialize_source_diversity_snapshot(snapshot) for snapshot in snapshots] + + +@source_diversity_snapshots_router.get( + "/summary/", + response=SourceDiversityObservabilitySummarySchema, + auth=api_authenticate, +) +def source_diversity_snapshot_summary(request: Any, project_id: int = Path(...)): + """Return project-level source diversity observability metrics.""" + + project = _require_project_contributor(request, project_id) + queryset = SourceDiversitySnapshot.objects.select_related("project").filter( + project_id=project_id + ) + payload = { + "project": _require_pk(project), + "snapshot_count": queryset.count(), + "latest_snapshot": queryset.order_by("-computed_at").first(), + } + return { + **payload, + "latest_snapshot": ( + _serialize_source_diversity_snapshot(payload["latest_snapshot"]) + if payload["latest_snapshot"] is not None + else None + ), + } + + +@source_diversity_snapshots_router.get( + "/{snapshot_id}/", + response=SourceDiversitySnapshotSchema, + auth=api_authenticate, +) +def get_source_diversity_snapshot( + request: Any, + project_id: int = Path(...), + snapshot_id: int = Path(...), +): + """Return one source diversity snapshot.""" + + _require_project_contributor(request, project_id) + return _serialize_source_diversity_snapshot( + _get_source_diversity_snapshot_or_404(project_id, snapshot_id) + ) + + +@trend_task_runs_router.get( + "/", + response=list[TrendTaskRunSchema], + auth=api_authenticate, +) +def list_trend_task_runs(request: Any, project_id: int = Path(...)): + """List persisted trend task runs visible to project contributors.""" + + _require_project_contributor(request, project_id) + runs = TrendTaskRun.objects.select_related("project").filter(project_id=project_id) + return [_serialize_trend_task_run(run) for run in runs] + + +@trend_task_runs_router.get( + "/summary/", + response=TrendTaskRunObservabilitySummarySchema, + auth=api_authenticate, +) +def trend_task_run_summary(request: Any, project_id: int = Path(...)): + """Return the latest persisted run for each tracked trend task.""" + + project = _require_project_contributor(request, project_id) + queryset = ( + TrendTaskRun.objects.select_related("project") + .filter(project_id=project_id) + .order_by("task_name", "-started_at") + ) + latest_by_task: dict[str, TrendTaskRun] = {} + for task_run in queryset: + latest_by_task.setdefault(task_run.task_name, task_run) + + latest_runs = [ + latest_by_task[task_name] + for task_name in TRACKED_TREND_TASKS + if task_name in latest_by_task + ] + payload = { + "project": _require_pk(project), + "run_count": queryset.count(), + "failed_run_count": queryset.filter(status="failed").count(), + "latest_runs": latest_runs, + } + return { + **payload, + "latest_runs": [_serialize_trend_task_run(run) for run in latest_runs], + } + + +@trend_task_runs_router.get( + "/{run_id}/", + response=TrendTaskRunSchema, + auth=api_authenticate, +) +def get_trend_task_run( + request: Any, + project_id: int = Path(...), + run_id: int = Path(...), +): + """Return one persisted trend task run.""" + + _require_project_contributor(request, project_id) + return _serialize_trend_task_run(_get_trend_task_run_or_404(project_id, run_id)) + + +__all__ = [ + "clusters_router", + "ideas_router", + "source_diversity_snapshots_router", + "themes_router", + "topic_centroid_snapshots_router", + "trend_task_runs_router", +] diff --git a/trends/serializers.py b/trends/serializers.py deleted file mode 100644 index d72a46cd..00000000 --- a/trends/serializers.py +++ /dev/null @@ -1,345 +0,0 @@ -"""DRF serializers for trends-domain models.""" - -from rest_framework import serializers - -from content.models import Content -from core.serializer_mixins import ProjectScopedSerializerMixin -from trends.models import ( - ContentClusterMembership, - OriginalContentIdea, - SourceDiversitySnapshot, - ThemeSuggestion, - TopicCentroidSnapshot, - TopicCluster, - TopicVelocitySnapshot, - TrendTaskRun, -) - - -class TopicClusterEntitySerializer(serializers.Serializer): - """Serialize the dominant entity summary for a topic cluster.""" - - id = serializers.IntegerField() - name = serializers.CharField() - type = serializers.CharField() - - -class TopicClusterContentSummarySerializer(serializers.Serializer): - """Serialize the content fields surfaced on cluster detail responses.""" - - id = serializers.IntegerField() - url = serializers.URLField() - title = serializers.CharField() - published_date = serializers.DateTimeField() - source_plugin = serializers.CharField() - - -class ContentClusterMembershipSerializer(serializers.ModelSerializer): - """Serialize one content membership within a topic cluster.""" - - content = TopicClusterContentSummarySerializer(read_only=True) - - class Meta: - model = ContentClusterMembership - fields = ["id", "content", "similarity", "assigned_at"] - read_only_fields = fields - - -class TopicVelocitySnapshotSerializer(serializers.ModelSerializer): - """Serialize one persisted topic velocity snapshot.""" - - class Meta: - model = TopicVelocitySnapshot - fields = [ - "id", - "cluster", - "project", - "computed_at", - "window_count", - "trailing_mean", - "trailing_stddev", - "z_score", - "velocity_score", - ] - read_only_fields = fields - - -class TopicClusterSerializer(serializers.ModelSerializer): - """Serialize one topic cluster with its current velocity rollup.""" - - dominant_entity = TopicClusterEntitySerializer(read_only=True) - velocity_score = serializers.FloatField(read_only=True, allow_null=True) - z_score = serializers.FloatField(read_only=True, allow_null=True) - window_count = serializers.IntegerField(read_only=True, allow_null=True) - velocity_computed_at = serializers.DateTimeField(read_only=True, allow_null=True) - - class Meta: - model = TopicCluster - fields = [ - "id", - "project", - "centroid_vector_id", - "label", - "first_seen_at", - "last_seen_at", - "is_active", - "member_count", - "dominant_entity", - "velocity_score", - "z_score", - "window_count", - "velocity_computed_at", - ] - read_only_fields = fields - - -class TopicClusterDetailSerializer(TopicClusterSerializer): - """Serialize one topic cluster with memberships and snapshot history.""" - - memberships = ContentClusterMembershipSerializer(many=True, read_only=True) - velocity_history = TopicVelocitySnapshotSerializer( - many=True, - read_only=True, - source="velocity_snapshots", - ) - - class Meta(TopicClusterSerializer.Meta): - fields = TopicClusterSerializer.Meta.fields + [ - "memberships", - "velocity_history", - ] - - -class ThemeSuggestionClusterSummarySerializer(serializers.Serializer): - """Serialize the cluster summary embedded in theme suggestions.""" - - id = serializers.IntegerField() - label = serializers.CharField(allow_blank=True) - member_count = serializers.IntegerField() - velocity_score = serializers.FloatField(read_only=True, allow_null=True) - - -class ThemeSuggestionPromotedContentSerializer(serializers.ModelSerializer): - """Serialize one content row marked for newsletter promotion by a theme.""" - - class Meta: - model = Content - fields = [ - "id", - "url", - "title", - "published_date", - "source_plugin", - "newsletter_promotion_at", - ] - read_only_fields = fields - - -class ThemeSuggestionSerializer(serializers.ModelSerializer): - """Serialize one editor-facing theme suggestion.""" - - cluster = ThemeSuggestionClusterSummarySerializer(read_only=True) - promoted_contents = ThemeSuggestionPromotedContentSerializer( - many=True, read_only=True - ) - decided_by_username = serializers.CharField( - source="decided_by.username", - read_only=True, - allow_null=True, - ) - - class Meta: - model = ThemeSuggestion - fields = [ - "id", - "project", - "cluster", - "title", - "pitch", - "why_it_matters", - "suggested_angle", - "velocity_at_creation", - "novelty_score", - "status", - "dismissal_reason", - "created_at", - "decided_at", - "decided_by", - "decided_by_username", - "promoted_contents", - ] - read_only_fields = fields - - -class ThemeSuggestionDismissSerializer( - ProjectScopedSerializerMixin, serializers.Serializer -): - """Validate dismissal requests for pending theme suggestions.""" - - reason = serializers.CharField(max_length=500) - - def validate_reason(self, value: str) -> str: - """Reject blank dismissal reasons.""" - - normalized = value.strip() - if not normalized: - raise serializers.ValidationError("Dismissal reason cannot be blank.") - return normalized - - -class OriginalContentIdeaClusterSummarySerializer(serializers.Serializer): - """Serialize the related cluster summary embedded in original-content ideas.""" - - id = serializers.IntegerField() - label = serializers.CharField(allow_blank=True) - member_count = serializers.IntegerField() - - -class OriginalContentIdeaSupportingContentSerializer(serializers.ModelSerializer): - """Serialize one supporting content row linked to an original-content idea.""" - - class Meta: - model = Content - fields = ["id", "url", "title", "published_date", "source_plugin"] - read_only_fields = fields - - -class OriginalContentIdeaSerializer(serializers.ModelSerializer): - """Serialize one editor-facing original-content idea.""" - - related_cluster = OriginalContentIdeaClusterSummarySerializer(read_only=True) - supporting_contents = OriginalContentIdeaSupportingContentSerializer( - many=True, - read_only=True, - ) - decided_by_username = serializers.CharField( - source="decided_by.username", - read_only=True, - allow_null=True, - ) - - class Meta: - model = OriginalContentIdea - fields = [ - "id", - "project", - "angle_title", - "summary", - "suggested_outline", - "why_now", - "supporting_contents", - "related_cluster", - "generated_by_model", - "self_critique_score", - "status", - "dismissal_reason", - "created_at", - "decided_at", - "decided_by", - "decided_by_username", - ] - read_only_fields = fields - - -class OriginalContentIdeaDismissSerializer( - ProjectScopedSerializerMixin, serializers.Serializer -): - """Validate dismissal requests for pending original-content ideas.""" - - reason = serializers.CharField(max_length=500) - - def validate_reason(self, value: str) -> str: - """Reject blank dismissal reasons.""" - - normalized = value.strip() - if not normalized: - raise serializers.ValidationError("Dismissal reason cannot be blank.") - return normalized - - -class TopicCentroidSnapshotSerializer(serializers.ModelSerializer): - """Serialize one persisted topic-centroid recomputation for a project.""" - - class Meta: - model = TopicCentroidSnapshot - fields = [ - "id", - "project", - "computed_at", - "centroid_active", - "feedback_count", - "upvote_count", - "downvote_count", - "drift_from_previous", - "drift_from_week_ago", - ] - read_only_fields = fields - - -class TopicCentroidObservabilitySummarySerializer(serializers.Serializer): - """Serialize project-level centroid observability summary metrics.""" - - project = serializers.IntegerField() - snapshot_count = serializers.IntegerField() - active_snapshot_count = serializers.IntegerField() - avg_drift_from_previous = serializers.FloatField(allow_null=True) - avg_drift_from_week_ago = serializers.FloatField(allow_null=True) - latest_snapshot = TopicCentroidSnapshotSerializer(allow_null=True) - - -class SourceDiversitySnapshotSerializer(serializers.ModelSerializer): - """Serialize one persisted source-diversity snapshot for a project.""" - - class Meta: - model = SourceDiversitySnapshot - fields = [ - "id", - "project", - "computed_at", - "window_days", - "plugin_entropy", - "source_entropy", - "author_entropy", - "cluster_entropy", - "top_plugin_share", - "top_source_share", - "breakdown", - ] - read_only_fields = fields - - -class SourceDiversityObservabilitySummarySerializer(serializers.Serializer): - """Serialize project-level source-diversity observability metrics.""" - - project = serializers.IntegerField() - snapshot_count = serializers.IntegerField() - latest_snapshot = SourceDiversitySnapshotSerializer(allow_null=True) - - -class TrendTaskRunSerializer(serializers.ModelSerializer): - """Serialize one persisted trend pipeline task execution.""" - - class Meta: - model = TrendTaskRun - fields = [ - "id", - "project", - "task_name", - "task_run_id", - "status", - "started_at", - "finished_at", - "latency_ms", - "error_message", - "summary", - ] - read_only_fields = fields - - -class TrendTaskRunObservabilitySummarySerializer(serializers.Serializer): - """Serialize the latest trend task runs plus project-level rollup counts.""" - - project = serializers.IntegerField() - run_count = serializers.IntegerField() - failed_run_count = serializers.IntegerField() - latest_runs = TrendTaskRunSerializer(many=True) diff --git a/trends/tasks.py b/trends/tasks.py index 06453928..649d3eac 100644 --- a/trends/tasks.py +++ b/trends/tasks.py @@ -1,12 +1,11 @@ -"""Celery tasks and helpers for trends-domain centroid and cluster recomputation.""" +"""Background tasks and helpers for trends-domain centroid and cluster recomputation.""" import math from collections import Counter from datetime import datetime, timedelta from functools import lru_cache, wraps -from typing import Any, Callable, Protocol, TypeVar, cast +from typing import Any, Callable, TypeVar, cast -from celery import shared_task from django.conf import settings from django.contrib.auth import get_user_model from django.core.cache import cache @@ -28,6 +27,7 @@ get_skill_resource_path, openrouter_chat_json, ) +from digest_engine.taskiq import broker, enqueue_task, task_always_eager from digest_engine.telemetry import trace_span from entities.models import Entity, EntityMention, EntityMentionRole from pipeline.resilience import execute_with_resilience @@ -77,22 +77,6 @@ TrendHelperCallable = TypeVar("TrendHelperCallable", bound=Callable[..., object]) -class DelayedTask(Protocol): - """Protocol for Celery tasks that can run eagerly or via ``delay``.""" - - def __call__(self, *args: object, **kwargs: object) -> object: - pass - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - -def _enqueue_task(task: object, *args: object) -> None: - """Dispatch a Celery task through a typed ``delay`` seam.""" - - cast(DelayedTask, task).delay(*args) - - def _require_pk(instance: Model) -> int: """Return a saved model primary key as an ``int``.""" @@ -140,20 +124,20 @@ def wrapper(*args: object, **kwargs: object) -> object: return decorator -@shared_task(name="core.tasks.run_all_topic_centroid_recomputations") +@broker.task(task_name="core.tasks.run_all_topic_centroid_recomputations") def run_all_topic_centroid_recomputations() -> int: """Queue topic-centroid recomputation for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_topic_centroid(project_id) else: - _enqueue_task(recompute_topic_centroid, project_id) + enqueue_task(recompute_topic_centroid, project_id) return len(project_ids) -@shared_task(name="core.tasks.recompute_topic_centroid") +@broker.task(task_name="core.tasks.recompute_topic_centroid") @observe_trend_task_run("recompute_topic_centroid") def recompute_topic_centroid(project_id: int): """Rebuild the project's feedback centroid from recent editorial signals.""" @@ -299,27 +283,27 @@ def queue_topic_centroid_recompute(project_id: int) -> bool: ): return False - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_topic_centroid(project_id) else: - _enqueue_task(recompute_topic_centroid, project_id) + enqueue_task(recompute_topic_centroid, project_id) return True -@shared_task(name="core.tasks.run_all_topic_cluster_recomputations") +@broker.task(task_name="core.tasks.run_all_topic_cluster_recomputations") def run_all_topic_cluster_recomputations() -> int: """Queue topic-cluster recomputation for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_topic_clusters(project_id) else: - _enqueue_task(recompute_topic_clusters, project_id) + enqueue_task(recompute_topic_clusters, project_id) return len(project_ids) -@shared_task(name="core.tasks.recompute_topic_clusters") +@broker.task(task_name="core.tasks.recompute_topic_clusters") @observe_trend_task_run( "recompute_topic_clusters", skipped_if=lambda summary: summary["contents_considered"] == 0, @@ -349,10 +333,10 @@ def recompute_topic_clusters(project_id: int) -> dict[str, int]: vector_cache: dict[int, list[float]] = {} cluster_groups = _build_cluster_groups(recent_contents, vector_cache) clusters_updated = _sync_topic_clusters(project_id, cluster_groups) - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_topic_velocity(project_id) else: - _enqueue_task(recompute_topic_velocity, project_id) + enqueue_task(recompute_topic_velocity, project_id) return { "project_id": project_id, "contents_considered": len(recent_contents), @@ -360,7 +344,7 @@ def recompute_topic_clusters(project_id: int) -> dict[str, int]: } -@shared_task(name="core.tasks.recompute_topic_velocity") +@broker.task(task_name="core.tasks.recompute_topic_velocity") @observe_trend_task_run( "recompute_topic_velocity", skipped_if=lambda summary: summary["clusters_evaluated"] == 0, @@ -428,12 +412,12 @@ def recompute_topic_velocity(project_id: int) -> dict[str, int]: velocity_score=velocity_score, ) snapshot_count += 1 - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): recompute_source_diversity(project_id) generate_theme_suggestions(project_id) else: - _enqueue_task(recompute_source_diversity, project_id) - _enqueue_task(generate_theme_suggestions, project_id) + enqueue_task(recompute_source_diversity, project_id) + enqueue_task(generate_theme_suggestions, project_id) return { "project_id": project_id, "clusters_evaluated": len(clusters), @@ -441,7 +425,7 @@ def recompute_topic_velocity(project_id: int) -> dict[str, int]: } -@shared_task(name="core.tasks.recompute_source_diversity") +@broker.task(task_name="core.tasks.recompute_source_diversity") @observe_trend_task_run( "recompute_source_diversity", skipped_if=lambda summary: summary["content_count"] == 0, @@ -582,7 +566,7 @@ def recompute_source_diversity(project_id: int) -> dict[str, object]: } -@shared_task(name="core.tasks.generate_theme_suggestions") +@broker.task(task_name="core.tasks.generate_theme_suggestions") @observe_trend_task_run( "generate_theme_suggestions", skipped_if=lambda summary: summary["created"] == 0 and summary["updated"] == 0, @@ -667,20 +651,20 @@ def generate_theme_suggestions(project_id: int) -> dict[str, int]: } -@shared_task(name="core.tasks.run_all_original_content_idea_generations") +@broker.task(task_name="core.tasks.run_all_original_content_idea_generations") def run_all_original_content_idea_generations() -> int: """Queue original-content ideation generation for every project.""" project_ids = list(Project.objects.values_list("id", flat=True)) for project_id in project_ids: - if settings.CELERY_TASK_ALWAYS_EAGER: + if task_always_eager(): generate_original_content_ideas(project_id) else: - _enqueue_task(generate_original_content_ideas, project_id) + enqueue_task(generate_original_content_ideas, project_id) return len(project_ids) -@shared_task(name="core.tasks.generate_original_content_ideas") +@broker.task(task_name="core.tasks.generate_original_content_ideas") @observe_trend_task_run( "generate_original_content_ideas", skipped_if=lambda summary: summary["created"] == 0, @@ -835,7 +819,7 @@ def generate_original_content_ideas(project_id: int) -> dict[str, int]: @_trace_trend_step("assign_content_to_topic_cluster") -@shared_task(name="core.tasks.assign_content_to_topic_cluster") +@broker.task(task_name="core.tasks.assign_content_to_topic_cluster") def assign_content_to_topic_cluster(content_id: int) -> dict[str, object]: """Assign one content item to the nearest active cluster when it fits.""" diff --git a/trends/tests/test_api.py b/trends/tests/test_ninja_api.py similarity index 54% rename from trends/tests/test_api.py rename to trends/tests/test_ninja_api.py index e25072c3..312ecc07 100644 --- a/trends/tests/test_api.py +++ b/trends/tests/test_ninja_api.py @@ -1,16 +1,17 @@ +from __future__ import annotations + from datetime import datetime, timezone +from http import HTTPStatus from typing import Any, cast +from unittest.mock import AsyncMock, patch from django.contrib.auth import get_user_model from django.db.models import Model +from django.test import TestCase from django.urls import reverse -from rest_framework import status -from rest_framework.test import APIClient, APITestCase from content.models import Content from entities.models import Entity -from ingestion.models import IngestionRun, RunStatus -from pipeline.models import ReviewQueue, ReviewReason, SkillResult, SkillStatus from projects.model_support import SourcePluginName from projects.models import Project, ProjectConfig, ProjectMembership, ProjectRole from trends.models import ( @@ -37,20 +38,14 @@ def _require_pk(instance: Model) -> int: return int(instance_pk) -def _typed_client(client: object) -> APIClient: - """Cast the DRF test client so Pylance sees APIClient helpers.""" - - return cast(APIClient, client) - - def _create_user(user_model: type[Any], **kwargs: object): """Create a user through the custom manager with a typed escape hatch.""" return cast(Any, user_model.objects).create_user(**kwargs) -class TrendsApiTests(APITestCase): - """Exercise project-scoped trends API endpoints from the trends app.""" +class TrendsNinjaApiTests(TestCase): + """Exercise project-scoped Ninja trends endpoints from the trends app.""" def setUp(self): user_model = get_user_model() @@ -119,9 +114,9 @@ def setUp(self): drift_from_previous=0.1, drift_from_week_ago=0.2, ) - _typed_client(self.client).force_authenticate(self.owner) + self.client.force_login(self.owner) - def test_topic_centroid_summary_action_returns_latest_snapshot_and_averages(self): + def test_topic_centroid_summary_returns_latest_snapshot_and_averages(self): older_snapshot = TopicCentroidSnapshot.objects.create( project=self.owner_project, centroid_active=True, @@ -138,12 +133,12 @@ def test_topic_centroid_summary_action_returns_latest_snapshot_and_averages(self response = self.client.get( reverse( - "v1:project-topic-centroid-snapshot-summary", + "ninja-api:topic_centroid_snapshot_summary", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertEqual(response.json()["project"], _require_pk(self.owner_project)) self.assertEqual(response.json()["snapshot_count"], 2) self.assertEqual(response.json()["active_snapshot_count"], 2) @@ -154,38 +149,7 @@ def test_topic_centroid_summary_action_returns_latest_snapshot_and_averages(self self.assertAlmostEqual(response.json()["avg_drift_from_previous"], 0.15) self.assertAlmostEqual(response.json()["avg_drift_from_week_ago"], 0.25) - def test_topic_cluster_list_returns_current_velocity_annotation(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - ) - TopicVelocitySnapshot.objects.create( - cluster=cluster, - project=self.owner_project, - window_count=3, - trailing_mean=1.0, - trailing_stddev=0.5, - z_score=2.0, - velocity_score=0.9, - ) - - response = self.client.get( - reverse( - "v1:project-topic-cluster-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(cluster)) - self.assertAlmostEqual(response.json()[0]["velocity_score"], 0.9) - - def test_topic_cluster_detail_and_velocity_history_action_return_memberships(self): + def test_topic_cluster_routes_return_annotations_and_memberships(self): cluster = TopicCluster.objects.create( project=self.owner_project, first_seen_at="2026-04-22T00:00:00Z", @@ -225,27 +189,38 @@ def test_topic_cluster_detail_and_velocity_history_action_return_memberships(sel computed_at="2026-04-24T00:00:00Z" ) + list_response = self.client.get( + reverse( + "ninja-api:list_topic_clusters", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) detail_response = self.client.get( reverse( - "v1:project-topic-cluster-detail", + "ninja-api:get_topic_cluster", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(cluster), + "cluster_id": _require_pk(cluster), }, ) ) history_response = self.client.get( reverse( - "v1:project-topic-cluster-velocity-history", + "ninja-api:topic_cluster_velocity_history", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(cluster), + "cluster_id": _require_pk(cluster), }, ), {"limit": 1}, ) - self.assertEqual(detail_response.status_code, status.HTTP_200_OK) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 1) + self.assertEqual(list_response.json()[0]["id"], _require_pk(cluster)) + self.assertAlmostEqual(list_response.json()[0]["velocity_score"], 1.0) + + self.assertEqual(detail_response.status_code, HTTPStatus.OK) self.assertEqual(detail_response.json()["id"], _require_pk(cluster)) self.assertEqual(len(detail_response.json()["memberships"]), 1) self.assertEqual( @@ -258,61 +233,11 @@ def test_topic_cluster_detail_and_velocity_history_action_return_memberships(sel _require_pk(second_snapshot), ) - self.assertEqual(history_response.status_code, status.HTTP_200_OK) + self.assertEqual(history_response.status_code, HTTPStatus.OK) self.assertEqual(len(history_response.json()), 1) self.assertEqual(history_response.json()[0]["id"], _require_pk(second_snapshot)) - def test_theme_suggestion_list_is_scoped_to_project(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - ) - suggestion = ThemeSuggestion.objects.create( - project=self.owner_project, - cluster=cluster, - title="Owner Theme", - pitch="Owner pitch", - why_it_matters="Owner why", - suggested_angle="Owner angle", - velocity_at_creation=0.9, - novelty_score=0.8, - ) - other_cluster = TopicCluster.objects.create( - project=self.other_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.other_entity, - ) - ThemeSuggestion.objects.create( - project=self.other_project, - cluster=other_cluster, - title="Other Theme", - pitch="Other pitch", - why_it_matters="Other why", - suggested_angle="Other angle", - velocity_at_creation=0.8, - novelty_score=0.7, - ) - - response = self.client.get( - reverse( - "v1:project-theme-suggestion-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(suggestion)) - self.assertEqual(response.json()[0]["status"], ThemeSuggestionStatus.PENDING) - - def test_theme_suggestion_accept_and_dismiss_actions_update_workflow_fields(self): + def test_theme_suggestion_routes_update_workflow_fields(self): cluster = TopicCluster.objects.create( project=self.owner_project, first_seen_at="2026-04-22T00:00:00Z", @@ -358,32 +283,44 @@ def test_theme_suggestion_accept_and_dismiss_actions_update_workflow_fields(self novelty_score=0.75, ) + list_response = self.client.get( + reverse( + "ninja-api:list_theme_suggestions", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) accept_response = self.client.post( reverse( - "v1:project-theme-suggestion-accept", + "ninja-api:accept_theme_suggestion_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(accept_suggestion), + "suggestion_id": _require_pk(accept_suggestion), }, ), - format="json", ) dismiss_response = self.client.post( reverse( - "v1:project-theme-suggestion-dismiss", + "ninja-api:dismiss_theme_suggestion_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(dismiss_suggestion), + "suggestion_id": _require_pk(dismiss_suggestion), }, ), {"reason": "already covered"}, - format="json", + content_type="application/json", ) accept_suggestion.refresh_from_db() dismiss_suggestion.refresh_from_db() promoted_content.refresh_from_db() - self.assertEqual(accept_response.status_code, status.HTTP_200_OK) + + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 2) + self.assertEqual( + list_response.json()[0]["status"], ThemeSuggestionStatus.PENDING + ) + + self.assertEqual(accept_response.status_code, HTTPStatus.OK) self.assertEqual(accept_suggestion.status, ThemeSuggestionStatus.ACCEPTED) self.assertEqual(accept_suggestion.decided_by, self.owner) self.assertIsNotNone(accept_suggestion.decided_at) @@ -391,82 +328,13 @@ def test_theme_suggestion_accept_and_dismiss_actions_update_workflow_fields(self self.assertEqual(promoted_content.newsletter_promotion_by, self.owner) self.assertIsNotNone(promoted_content.newsletter_promotion_at) self.assertEqual(len(accept_response.json()["promoted_contents"]), 1) - self.assertEqual( - accept_response.json()["promoted_contents"][0]["id"], - _require_pk(promoted_content), - ) - self.assertEqual(dismiss_response.status_code, status.HTTP_200_OK) + self.assertEqual(dismiss_response.status_code, HTTPStatus.OK) self.assertEqual(dismiss_suggestion.status, ThemeSuggestionStatus.DISMISSED) self.assertEqual(dismiss_suggestion.dismissal_reason, "already covered") self.assertEqual(dismiss_suggestion.decided_by, self.owner) - def test_original_content_idea_list_is_scoped_to_project(self): - cluster = TopicCluster.objects.create( - project=self.owner_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=3, - dominant_entity=self.owner_entity, - label="Owner Cluster", - ) - idea = OriginalContentIdea.objects.create( - project=self.owner_project, - related_cluster=cluster, - angle_title="Owner idea", - summary="Owner summary", - suggested_outline="Owner outline", - why_now="Owner why now", - generated_by_model="heuristic", - self_critique_score=0.82, - ) - idea.supporting_contents.add(self.owner_content) - - other_cluster = TopicCluster.objects.create( - project=self.other_project, - first_seen_at="2026-04-22T00:00:00Z", - last_seen_at="2026-04-24T00:00:00Z", - is_active=True, - member_count=2, - dominant_entity=self.other_entity, - label="Other Cluster", - ) - other_idea = OriginalContentIdea.objects.create( - project=self.other_project, - related_cluster=other_cluster, - angle_title="Other idea", - summary="Other summary", - suggested_outline="Other outline", - why_now="Other why now", - generated_by_model="heuristic", - self_critique_score=0.7, - ) - other_idea.supporting_contents.add(self.other_content) - - response = self.client.get( - reverse( - "v1:project-original-content-idea-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.json()), 1) - self.assertEqual(response.json()[0]["id"], _require_pk(idea)) - self.assertEqual( - response.json()[0]["status"], OriginalContentIdeaStatus.PENDING - ) - self.assertEqual( - response.json()[0]["related_cluster"]["id"], _require_pk(cluster) - ) - self.assertEqual(len(response.json()[0]["supporting_contents"]), 1) - self.assertEqual( - response.json()[0]["supporting_contents"][0]["id"], - _require_pk(self.owner_content), - ) - - def test_original_content_idea_workflow_actions_update_status_fields(self): + def test_original_content_idea_routes_cover_list_and_workflow_actions(self): cluster = TopicCluster.objects.create( project=self.owner_project, first_seen_at="2026-04-22T00:00:00Z", @@ -498,47 +366,55 @@ def test_original_content_idea_workflow_actions_update_status_fields(self): self_critique_score=0.75, ) + list_response = self.client.get( + reverse( + "ninja-api:list_original_content_ideas", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) accept_response = self.client.post( reverse( - "v1:project-original-content-idea-accept", + "ninja-api:accept_original_content_idea_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(accepted_then_written_idea), + "idea_id": _require_pk(accepted_then_written_idea), }, ), - format="json", ) dismiss_response = self.client.post( reverse( - "v1:project-original-content-idea-dismiss", + "ninja-api:dismiss_original_content_idea_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(dismissed_idea), + "idea_id": _require_pk(dismissed_idea), }, ), {"reason": "already assigned"}, - format="json", + content_type="application/json", ) mark_written_response = self.client.post( reverse( - "v1:project-original-content-idea-mark-written", + "ninja-api:mark_original_content_idea_written_route", kwargs={ "project_id": _require_pk(self.owner_project), - "pk": _require_pk(accepted_then_written_idea), + "idea_id": _require_pk(accepted_then_written_idea), }, ), - format="json", ) accepted_then_written_idea.refresh_from_db() dismissed_idea.refresh_from_db() - self.assertEqual(accept_response.status_code, status.HTTP_200_OK) - self.assertEqual(accept_response.json()["status"], "accepted") + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 2) self.assertEqual( - mark_written_response.status_code, - status.HTTP_200_OK, + list_response.json()[0]["status"], + OriginalContentIdeaStatus.PENDING, ) + + self.assertEqual(accept_response.status_code, HTTPStatus.OK) + self.assertEqual(accept_response.json()["status"], "accepted") + self.assertEqual(mark_written_response.status_code, HTTPStatus.OK) self.assertEqual(mark_written_response.json()["status"], "written") self.assertEqual( accepted_then_written_idea.status, @@ -547,65 +423,51 @@ def test_original_content_idea_workflow_actions_update_status_fields(self): self.assertEqual(accepted_then_written_idea.decided_by, self.owner) self.assertIsNotNone(accepted_then_written_idea.decided_at) - self.assertEqual(dismiss_response.status_code, status.HTTP_200_OK) - self.assertEqual( - dismissed_idea.status, - OriginalContentIdeaStatus.DISMISSED, - ) + self.assertEqual(dismiss_response.status_code, HTTPStatus.OK) + self.assertEqual(dismissed_idea.status, OriginalContentIdeaStatus.DISMISSED) self.assertEqual(dismissed_idea.dismissal_reason, "already assigned") self.assertEqual(dismissed_idea.decided_by, self.owner) - def test_original_content_idea_generate_action_runs_immediately_in_eager_mode( - self, - ): - with self.settings(CELERY_TASK_ALWAYS_EAGER=True): - with self.subTest("generate now returns completed result"): - from unittest.mock import patch - - with patch( - "trends.api.generate_original_content_ideas", - return_value={ - "project_id": _require_pk(self.owner_project), - "clusters_considered": 4, - "created": 2, - "skipped": 1, - }, - ) as generate_mock: - response = self.client.post( - reverse( - "v1:project-original-content-idea-generate", - kwargs={"project_id": _require_pk(self.owner_project)}, - ), - format="json", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["status"], "completed") - self.assertEqual(response.json()["project_id"], _require_pk(self.owner_project)) - self.assertEqual(response.json()["result"]["created"], 2) - generate_mock.assert_called_once_with(_require_pk(self.owner_project)) - - def test_original_content_idea_generate_action_queues_in_background_mode(self): - with self.settings(CELERY_TASK_ALWAYS_EAGER=False): - from unittest.mock import patch + def test_original_content_idea_generate_route_handles_eager_and_queued_modes(self): + with self.settings(TASKIQ_ALWAYS_EAGER=True): + with patch( + "trends.ninja_api.generate_original_content_ideas", + return_value={ + "project_id": _require_pk(self.owner_project), + "clusters_considered": 4, + "created": 2, + "skipped": 1, + }, + ) as generate_mock: + eager_response = self.client.post( + reverse( + "ninja-api:generate_original_content_ideas_route", + kwargs={"project_id": _require_pk(self.owner_project)}, + ), + ) + with self.settings(TASKIQ_ALWAYS_EAGER=False): with patch( - "trends.api.generate_original_content_ideas.delay" - ) as delay_mock: - response = self.client.post( + "trends.ninja_api.generate_original_content_ideas.kiq", + new_callable=AsyncMock, + ) as queue_mock: + queued_response = self.client.post( reverse( - "v1:project-original-content-idea-generate", + "ninja-api:generate_original_content_ideas_route", kwargs={"project_id": _require_pk(self.owner_project)}, ), - format="json", ) - self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) - self.assertEqual(response.json()["status"], "queued") - self.assertEqual(response.json()["project_id"], _require_pk(self.owner_project)) - delay_mock.assert_called_once_with(_require_pk(self.owner_project)) + self.assertEqual(eager_response.status_code, HTTPStatus.OK) + self.assertEqual(eager_response.json()["status"], "completed") + self.assertEqual(eager_response.json()["result"]["created"], 2) + generate_mock.assert_called_once_with(_require_pk(self.owner_project)) + + self.assertEqual(queued_response.status_code, HTTPStatus.ACCEPTED) + self.assertEqual(queued_response.json()["status"], "queued") + queue_mock.assert_awaited_once_with(_require_pk(self.owner_project)) - def test_source_diversity_snapshot_list_and_summary_are_scoped_to_project(self): + def test_source_diversity_and_trend_task_summaries_are_scoped_to_project(self): owner_snapshot = SourceDiversitySnapshot.objects.create( project=self.owner_project, window_days=14, @@ -624,7 +486,7 @@ def test_source_diversity_snapshot_list_and_summary_are_scoped_to_project(self): "alerts": [], }, ) - other_snapshot = SourceDiversitySnapshot.objects.create( + SourceDiversitySnapshot.objects.create( project=self.other_project, window_days=14, plugin_entropy=0.2, @@ -642,36 +504,6 @@ def test_source_diversity_snapshot_list_and_summary_are_scoped_to_project(self): "alerts": [], }, ) - - list_response = self.client.get( - reverse( - "v1:project-source-diversity-snapshot-list", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - summary_response = self.client.get( - reverse( - "v1:project-source-diversity-snapshot-summary", - kwargs={"project_id": _require_pk(self.owner_project)}, - ) - ) - - self.assertEqual(list_response.status_code, status.HTTP_200_OK) - self.assertEqual(len(list_response.json()), 1) - self.assertEqual(list_response.json()[0]["id"], _require_pk(owner_snapshot)) - self.assertNotEqual(_require_pk(owner_snapshot), _require_pk(other_snapshot)) - - self.assertEqual(summary_response.status_code, status.HTTP_200_OK) - self.assertEqual( - summary_response.json()["project"], _require_pk(self.owner_project) - ) - self.assertEqual(summary_response.json()["snapshot_count"], 1) - self.assertEqual( - summary_response.json()["latest_snapshot"]["id"], - _require_pk(owner_snapshot), - ) - - def test_trend_task_run_summary_returns_latest_run_per_tracked_task(self): older_centroid_run = TrendTaskRun.objects.create( project=self.owner_project, task_name="recompute_topic_centroid", @@ -690,10 +522,10 @@ def test_trend_task_run_summary_returns_latest_run_per_tracked_task(self): error_message="embedding outage", ) TrendTaskRun.objects.filter(pk=older_centroid_run.pk).update( - started_at="2026-04-20T00:00:00Z", + started_at="2026-04-20T00:00:00Z" ) TrendTaskRun.objects.filter(pk=latest_centroid_run.pk).update( - started_at="2026-04-21T00:00:00Z", + started_at="2026-04-21T00:00:00Z" ) source_diversity_run = TrendTaskRun.objects.create( project=self.owner_project, @@ -702,65 +534,66 @@ def test_trend_task_run_summary_returns_latest_run_per_tracked_task(self): latency_ms=18, summary={"project_id": _require_pk(self.owner_project), "content_count": 0}, ) - TrendTaskRun.objects.create( - project=self.other_project, - task_name="recompute_source_diversity", - status=TrendTaskRunStatus.COMPLETED, - latency_ms=12, - summary={"project_id": _require_pk(self.other_project), "content_count": 4}, - ) - response = self.client.get( + list_response = self.client.get( reverse( - "v1:project-trend-task-run-summary", + "ninja-api:list_source_diversity_snapshots", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + summary_response = self.client.get( + reverse( + "ninja-api:source_diversity_snapshot_summary", + kwargs={"project_id": _require_pk(self.owner_project)}, + ) + ) + task_summary_response = self.client.get( + reverse( + "ninja-api:trend_task_run_summary", kwargs={"project_id": _require_pk(self.owner_project)}, ) ) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json()["project"], _require_pk(self.owner_project)) - self.assertEqual(response.json()["run_count"], 3) - self.assertEqual(response.json()["failed_run_count"], 1) - self.assertEqual(len(response.json()["latest_runs"]), 2) + self.assertEqual(list_response.status_code, HTTPStatus.OK) + self.assertEqual(len(list_response.json()), 1) + self.assertEqual(list_response.json()[0]["id"], _require_pk(owner_snapshot)) + + self.assertEqual(summary_response.status_code, HTTPStatus.OK) + self.assertEqual(summary_response.json()["snapshot_count"], 1) self.assertEqual( - response.json()["latest_runs"][0]["id"], - _require_pk(latest_centroid_run), + summary_response.json()["latest_snapshot"]["id"], + _require_pk(owner_snapshot), ) + + self.assertEqual(task_summary_response.status_code, HTTPStatus.OK) self.assertEqual( - response.json()["latest_runs"][0]["status"], - TrendTaskRunStatus.FAILED, + task_summary_response.json()["project"], _require_pk(self.owner_project) ) + self.assertEqual(task_summary_response.json()["run_count"], 3) + self.assertEqual(task_summary_response.json()["failed_run_count"], 1) + self.assertEqual(len(task_summary_response.json()["latest_runs"]), 2) self.assertEqual( - response.json()["latest_runs"][1]["id"], + task_summary_response.json()["latest_runs"][0]["id"], + _require_pk(latest_centroid_run), + ) + self.assertEqual( + task_summary_response.json()["latest_runs"][1]["id"], _require_pk(source_diversity_run), ) - def test_metrics_endpoint_requires_bearer_metrics_token(self): - TrendTaskRun.objects.create( - project=self.owner_project, - task_name="recompute_topic_centroid", - status=TrendTaskRunStatus.COMPLETED, - latency_ms=120, - summary={"project_id": _require_pk(self.owner_project)}, - ) + def test_trends_ninja_requires_authentication(self): + self.client.logout() - with self.settings(METRICS_TOKEN="metrics-secret"): - unauthorized_response = self.client.get(reverse("metrics")) - authorized_response = self.client.get( - reverse("metrics"), - HTTP_AUTHORIZATION="Bearer metrics-secret", + response = self.client.get( + reverse( + "ninja-api:list_topic_clusters", + kwargs={"project_id": _require_pk(self.owner_project)}, ) - - self.assertEqual(unauthorized_response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(authorized_response.status_code, status.HTTP_200_OK) - self.assertTrue( - authorized_response["Content-Type"].startswith("text/plain; version=0.0.4") - ) - self.assertIn( - 'newsletter_trend_task_run_total{project_id="1",task_name="recompute_topic_centroid",status="completed"} 1', - authorized_response.content.decode("utf-8"), ) + self.assertEqual(response.status_code, HTTPStatus.UNAUTHORIZED) + self.assertEqual(response.json(), {"detail": "Unauthorized"}) + def test_metrics_endpoint_emits_latest_run_status_and_timestamps(self): older_run = TrendTaskRun.objects.create( project=self.owner_project, @@ -793,7 +626,7 @@ def test_metrics_endpoint_emits_latest_run_status_and_timestamps(self): ) response_body = response.content.decode("utf-8") - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.status_code, HTTPStatus.OK) self.assertIn( 'newsletter_trend_task_run_latest_status{project_id="1",task_name="recompute_topic_centroid",status="failed"} 1', response_body, @@ -812,86 +645,3 @@ def test_metrics_endpoint_emits_latest_run_status_and_timestamps(self): f'{{project_id="1",task_name="recompute_topic_centroid"}} {latest_finished_at.timestamp():.6f}', response_body, ) - - def test_metrics_endpoint_emits_ingestion_pipeline_and_authority_metrics(self): - self.owner_content.pipeline_state = "awaiting_review" - self.owner_content.save(update_fields=["pipeline_state"]) - other_owner_content = Content.objects.create( - project=self.owner_project, - url="https://example.com/owner-completed", - title="Completed Owner Content", - author="Owner Author", - entity=self.owner_entity, - source_plugin="rss", - published_date="2026-04-22T00:00:00Z", - content_text="Completed content text", - pipeline_state="completed", - ) - Entity.objects.create( - project=self.owner_project, - name="High Authority Entity", - type="organization", - authority_score=0.9, - ) - IngestionRun.objects.create( - project=self.owner_project, - plugin_name=SourcePluginName.RSS, - status=RunStatus.SUCCESS, - items_fetched=6, - items_ingested=4, - completed_at=datetime(2026, 4, 22, 6, 0, tzinfo=timezone.utc), - ) - SkillResult.objects.create( - content=other_owner_content, - project=self.owner_project, - skill_name="relevance_scoring", - status=SkillStatus.FAILED, - error_message="temporary failure", - latency_ms=320, - ) - ReviewQueue.objects.create( - project=self.owner_project, - content=self.owner_content, - reason=ReviewReason.BORDERLINE_RELEVANCE, - confidence=0.56, - resolved=False, - ) - - with self.settings(METRICS_TOKEN="metrics-secret"): - response = self.client.get( - reverse("metrics"), - HTTP_AUTHORIZATION="Bearer metrics-secret", - ) - - project_id = _require_pk(self.owner_project) - response_body = response.content.decode("utf-8") - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn( - 'newsletter_ingestion_run_total{project_id="%s",plugin_name="rss",status="success"} 1' - % project_id, - response_body, - ) - self.assertIn( - 'newsletter_pipeline_content_total{project_id="%s",pipeline_state="awaiting_review"} 1' - % project_id, - response_body, - ) - self.assertIn( - 'newsletter_pipeline_review_queue_depth{project_id="%s",reason="borderline_relevance"} 1' - % project_id, - response_body, - ) - self.assertIn( - 'newsletter_skill_result_failure_ratio{project_id="%s",skill_name="relevance_scoring"} 1.000000' - % project_id, - response_body, - ) - self.assertIn( - 'newsletter_entity_authority_score_count{project_id="%s"} 2' % project_id, - response_body, - ) - self.assertIn( - 'newsletter_entity_authority_score_bucket{project_id="%s",le="1.0"} 2' - % project_id, - response_body, - ) diff --git a/trends/tests/test_tasks.py b/trends/tests/test_tasks.py index 879aa575..a1dc7f69 100644 --- a/trends/tests/test_tasks.py +++ b/trends/tests/test_tasks.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, timezone from types import SimpleNamespace +from unittest.mock import AsyncMock import pytest from django.db.models import Model @@ -34,6 +35,7 @@ recompute_topic_centroid, recompute_topic_clusters, recompute_topic_velocity, + run_all_original_content_idea_generations, run_all_topic_centroid_recomputations, run_all_topic_cluster_recomputations, ) @@ -240,7 +242,10 @@ def test_recompute_topic_centroid_disables_centroid_below_minimum_upvotes( def test_run_all_topic_centroid_recomputations_enqueues_all_projects( source_plugin_context, mocker ): - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_centroid.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Other Centroid Project", topic_description="Security", @@ -249,17 +254,20 @@ def test_run_all_topic_centroid_recomputations_enqueues_all_projects( enqueued_count = run_all_topic_centroid_recomputations() assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 def test_run_all_topic_centroid_recomputations_executes_inline_when_eager( source_plugin_context, settings, mocker ): - settings.CELERY_TASK_ALWAYS_EAGER = True + settings.TASKIQ_ALWAYS_EAGER = True recompute_mock = mocker.patch("trends.tasks.recompute_topic_centroid") - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_centroid.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Inline Centroid Project", topic_description="Platform", @@ -271,13 +279,16 @@ def test_run_all_topic_centroid_recomputations_executes_inline_when_eager( recompute_mock.assert_any_call(source_plugin_context.project.id) recompute_mock.assert_any_call(_require_pk(other_project)) assert recompute_mock.call_count == 2 - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_run_all_topic_cluster_recomputations_enqueues_all_projects( source_plugin_context, mocker ): - delay_mock = mocker.patch("trends.tasks.recompute_topic_clusters.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_clusters.kiq", + new_callable=AsyncMock, + ) other_project = Project.objects.create( name="Other Cluster Project", topic_description="Security", @@ -286,9 +297,75 @@ def test_run_all_topic_cluster_recomputations_enqueues_all_projects( enqueued_count = run_all_topic_cluster_recomputations() assert enqueued_count == 2 - delay_mock.assert_any_call(source_plugin_context.project.id) - delay_mock.assert_any_call(_require_pk(other_project)) - assert delay_mock.call_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 + + +def test_run_all_topic_cluster_recomputations_executes_inline_when_eager( + source_plugin_context, settings, mocker +): + settings.TASKIQ_ALWAYS_EAGER = True + recompute_mock = mocker.patch("trends.tasks.recompute_topic_clusters") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_clusters.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Inline Cluster Project", + topic_description="Platform", + ) + + enqueued_count = run_all_topic_cluster_recomputations() + + assert enqueued_count == 2 + recompute_mock.assert_any_call(source_plugin_context.project.id) + recompute_mock.assert_any_call(_require_pk(other_project)) + assert recompute_mock.call_count == 2 + queue_mock.assert_not_awaited() + + +def test_run_all_original_content_idea_generations_enqueues_all_projects( + source_plugin_context, mocker +): + queue_mock = mocker.patch( + "trends.tasks.generate_original_content_ideas.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Other Ideation Project", + topic_description="Security", + ) + + enqueued_count = run_all_original_content_idea_generations() + + assert enqueued_count == 2 + queue_mock.assert_any_await(source_plugin_context.project.id) + queue_mock.assert_any_await(_require_pk(other_project)) + assert queue_mock.await_count == 2 + + +def test_run_all_original_content_idea_generations_executes_inline_when_eager( + source_plugin_context, settings, mocker +): + settings.TASKIQ_ALWAYS_EAGER = True + generate_mock = mocker.patch("trends.tasks.generate_original_content_ideas") + queue_mock = mocker.patch( + "trends.tasks.generate_original_content_ideas.kiq", + new_callable=AsyncMock, + ) + other_project = Project.objects.create( + name="Inline Ideation Project", + topic_description="Platform", + ) + + enqueued_count = run_all_original_content_idea_generations() + + assert enqueued_count == 2 + generate_mock.assert_any_call(source_plugin_context.project.id) + generate_mock.assert_any_call(_require_pk(other_project)) + assert generate_mock.call_count == 2 + queue_mock.assert_not_awaited() def test_recompute_source_diversity_persists_entropy_breakdown_and_alerts( @@ -569,25 +646,31 @@ def test_queue_topic_centroid_recompute_enqueues_background_task( source_plugin_context, mocker ): cache_add_mock = mocker.patch("trends.tasks.cache.add", return_value=True) - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_centroid.kiq", + new_callable=AsyncMock, + ) queued = queue_topic_centroid_recompute(source_plugin_context.project.id) assert queued is True cache_add_mock.assert_called_once() - delay_mock.assert_called_once_with(source_plugin_context.project.id) + queue_mock.assert_awaited_once_with(source_plugin_context.project.id) def test_queue_topic_centroid_recompute_skips_duplicate_queue_attempts( source_plugin_context, mocker ): mocker.patch("trends.tasks.cache.add", return_value=False) - delay_mock = mocker.patch("trends.tasks.recompute_topic_centroid.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_centroid.kiq", + new_callable=AsyncMock, + ) queued = queue_topic_centroid_recompute(source_plugin_context.project.id) assert queued is False - delay_mock.assert_not_called() + queue_mock.assert_not_awaited() def test_recompute_topic_clusters_groups_recent_similar_content( @@ -612,7 +695,10 @@ def test_recompute_topic_clusters_groups_recent_similar_content( "trends.tasks.embed_text", side_effect=lambda text: vector_lookup[text.split("\n\n", 1)[0]], ) - delay_mock = mocker.patch("trends.tasks.recompute_topic_velocity.delay") + queue_mock = mocker.patch( + "trends.tasks.recompute_topic_velocity.kiq", + new_callable=AsyncMock, + ) clustered_contents = [] for index in range(4): @@ -659,7 +745,7 @@ def test_recompute_topic_clusters_groups_recent_similar_content( assert cluster.dominant_entity == source_plugin_context.entity assert set(memberships) == {_require_pk(content) for content in clustered_contents} assert _require_pk(outlier) not in memberships - delay_mock.assert_called_once_with(project.id) + queue_mock.assert_awaited_once_with(project.id) def test_assign_content_to_topic_cluster_adds_similar_content_to_existing_cluster( @@ -739,6 +825,14 @@ def test_recompute_topic_velocity_detects_synthetic_burst( project = source_plugin_context.project fixed_now = datetime(2026, 4, 30, 12, 0, tzinfo=timezone.utc) mocker.patch("trends.tasks.timezone.now", return_value=fixed_now) + source_diversity_queue_mock = mocker.patch( + "trends.tasks.recompute_source_diversity.kiq", + new_callable=AsyncMock, + ) + theme_queue_mock = mocker.patch( + "trends.tasks.generate_theme_suggestions.kiq", + new_callable=AsyncMock, + ) cluster = TopicCluster.objects.create( project=project, first_seen_at=fixed_now - timedelta(days=8), @@ -799,6 +893,8 @@ def test_recompute_topic_velocity_detects_synthetic_burst( assert snapshot.trailing_stddev == pytest.approx(0.0) assert snapshot.z_score == pytest.approx(3.0) assert snapshot.velocity_score == pytest.approx(1.0) + source_diversity_queue_mock.assert_awaited_once_with(project.id) + theme_queue_mock.assert_awaited_once_with(project.id) def test_generate_theme_suggestions_creates_pending_suggestion( diff --git a/turbo.json b/turbo.json index f9ccc77e..5d19c0fa 100644 --- a/turbo.json +++ b/turbo.json @@ -1,5 +1,5 @@ { - "$schema": "https://v2-9-14.turborepo.dev/schema.json", + "$schema": "https://v2-9-16.turborepo.dev/schema.json", "globalDependencies": ["**/.env.*local"], "tasks": { "build": { diff --git a/users/api.py b/users/api.py deleted file mode 100644 index 2c06a128..00000000 --- a/users/api.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Current-user profile API endpoints for the users app.""" - -from __future__ import annotations - -from typing import Any, Protocol, cast - -from django.conf import settings -from django.utils import timezone -from drf_spectacular.utils import extend_schema -from rest_framework import permissions, serializers, status -from rest_framework.exceptions import PermissionDenied -from rest_framework.parsers import FormParser, MultiPartParser -from rest_framework.response import Response -from rest_framework.views import APIView - -from projects.models import ProjectMembership -from users.models import AppUser, MembershipInvitation, avatar_thumbnail_path -from users.serializers import ( - AvatarUploadSerializer, - ProfileSerializer, - PublicMembershipInvitationSerializer, -) -from users.tasks import generate_avatar_thumbnail - - -class DelayedTask(Protocol): - """Protocol for Celery tasks dispatched through ``delay``.""" - - def delay(self, *args: object, **kwargs: object) -> object: - pass - - -def _enqueue_task(task: object, *args: object) -> None: - """Dispatch a Celery task through a typed ``delay`` seam.""" - - cast(DelayedTask, task).delay(*args) - - -def _validated_data(serializer: Any) -> dict[str, Any]: - """Return validated avatar-upload serializer data with a concrete mapping type.""" - - return cast(dict[str, Any], serializer.validated_data) - - -def _delete_avatar_assets(user: AppUser) -> None: - """Delete the user's stored avatar and generated thumbnail files.""" - - if user.avatar: - storage = user.avatar.storage - avatar_name = user.avatar.name - thumbnail_name = avatar_thumbnail_path(user) - if avatar_name and storage.exists(avatar_name): - storage.delete(avatar_name) - if storage.exists(thumbnail_name): - storage.delete(thumbnail_name) - - -@extend_schema(tags=["Users"]) -class ProfileView(APIView): - """Expose the authenticated user's editable profile surface.""" - - permission_classes = [permissions.IsAuthenticated] - - @extend_schema(responses={200: ProfileSerializer}) - def get(self, request): - """Return the current user's profile payload.""" - - serializer = ProfileSerializer(request.user) - return Response(serializer.data) - - @extend_schema(request=ProfileSerializer, responses={200: ProfileSerializer}) - def patch(self, request): - """Update display-name and profile-text fields for the current user.""" - - serializer = ProfileSerializer(request.user, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(ProfileSerializer(request.user).data) - - -@extend_schema(tags=["Users"]) -class ProfileAvatarView(APIView): - """Create or delete the authenticated user's avatar image.""" - - permission_classes = [permissions.IsAuthenticated] - parser_classes = [MultiPartParser, FormParser] - - @extend_schema(request=AvatarUploadSerializer, responses={200: ProfileSerializer}) - def post(self, request): - """Store a new avatar image for the current user and queue a thumbnail.""" - - serializer = AvatarUploadSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - user = request.user - _delete_avatar_assets(user) - user.avatar = _validated_data(serializer)["avatar"] - user.save(update_fields=["avatar"]) - - if settings.CELERY_TASK_ALWAYS_EAGER: - generate_avatar_thumbnail(user.id) - else: - _enqueue_task(generate_avatar_thumbnail, user.id) - - return Response(ProfileSerializer(user).data, status=status.HTTP_200_OK) - - @extend_schema(responses={200: ProfileSerializer}) - def delete(self, request): - """Remove the current user's avatar image and generated thumbnail.""" - - user = request.user - _delete_avatar_assets(user) - user.avatar = None - user.save(update_fields=["avatar"]) - return Response(ProfileSerializer(user).data) - - -@extend_schema(tags=["Users"]) -class MembershipInvitationTokenView(APIView): - """Expose and redeem one invitation token.""" - - def get_permissions(self): - """Allow anyone to inspect an invitation, but require auth to accept it.""" - - if self.request.method == "GET": - permission_classes = [permissions.AllowAny] - else: - permission_classes = [permissions.IsAuthenticated] - return [permission() for permission in permission_classes] - - def _get_invitation(self, token: str) -> MembershipInvitation: - """Load one invitation row by token or return 404.""" - - return MembershipInvitation.objects.select_related("project", "invited_by").get( - token=token - ) - - @extend_schema(responses={200: PublicMembershipInvitationSerializer}) - def get(self, request, token: str): - """Return public invite details for one token.""" - - invitation = self._get_invitation(token) - return Response(PublicMembershipInvitationSerializer(invitation).data) - - @extend_schema(responses={200: PublicMembershipInvitationSerializer}) - def post(self, request, token: str): - """Accept a project invitation for the authenticated user.""" - - invitation = self._get_invitation(token) - if invitation.revoked_at is not None: - raise serializers.ValidationError( - {"token": "This invitation has been revoked."} - ) - if invitation.accepted_at is not None: - raise serializers.ValidationError( - {"token": "This invitation has already been accepted."} - ) - - user_email = (request.user.email or "").strip().lower() - invitation_email = invitation.email.strip().lower() - if not user_email or user_email != invitation_email: - raise PermissionDenied( - f"Sign in as {invitation.email} to accept this invite." - ) - - membership, created = ProjectMembership.objects.update_or_create( - user=request.user, - project=invitation.project, - defaults={ - "role": invitation.role, - "invited_by": invitation.invited_by, - }, - ) - if created and membership.joined_at is None: - membership.save(update_fields=["joined_at"]) - - invitation.accepted_at = timezone.now() - invitation.save(update_fields=["accepted_at"]) - - return Response(PublicMembershipInvitationSerializer(invitation).data) diff --git a/users/api_urls.py b/users/api_urls.py deleted file mode 100644 index e0e053aa..00000000 --- a/users/api_urls.py +++ /dev/null @@ -1,19 +0,0 @@ -"""URL routes for current-user profile endpoints.""" - -from django.urls import path - -from users.api import ( - MembershipInvitationTokenView, - ProfileAvatarView, - ProfileView, -) - -urlpatterns = [ - path("profile/", ProfileView.as_view(), name="profile"), - path("profile/avatar/", ProfileAvatarView.as_view(), name="profile-avatar"), - path( - "invitations//", - MembershipInvitationTokenView.as_view(), - name="membership-invitation-token", - ), -] diff --git a/users/auth_views.py b/users/auth_views.py new file mode 100644 index 00000000..915d2526 --- /dev/null +++ b/users/auth_views.py @@ -0,0 +1,420 @@ +"""Authentication views backed by Django auth and JWTs.""" + +from __future__ import annotations + +import json +from urllib.parse import urlencode + +from allauth.account.adapter import get_adapter as get_account_adapter +from allauth.account.forms import default_token_generator +from allauth.account.utils import setup_user_email, url_str_to_user_pk, user_pk_to_url_str, user_username +from django.conf import settings +from django.contrib.auth import ( + authenticate, + update_session_auth_hash, +) +from django.contrib.auth import ( + login as django_login, +) +from django.contrib.auth import ( + logout as django_logout, +) +from django.contrib.auth.forms import SetPasswordForm +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import ValidationError +from django.core.validators import validate_email +from django.http import HttpRequest, HttpResponseNotAllowed, JsonResponse +from django.utils.decorators import method_decorator +from django.utils.encoding import force_str +from django.views import View +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +from core.jwt import issue_auth_tokens +from core.ninja_api import api_authenticate +from users.models import AppUser + + +def _auth_error(message: str, *, status_code: int = 400) -> JsonResponse: + """Return a normalized auth error payload.""" + + return JsonResponse({"detail": message}, status=status_code) + + +def _auth_response(user: AppUser, *, status_code: int = 200) -> JsonResponse: + """Return the shared JWT auth payload for one authenticated user.""" + + tokens = issue_auth_tokens(user) + return JsonResponse( + { + "access": tokens["access"], + "refresh": tokens["refresh"], + "user": { + "id": user.id, + "username": user.username, + "email": user.email, + }, + }, + status=status_code, + ) + + +def _parse_json_payload( + request: HttpRequest, +) -> tuple[dict[str, object] | None, JsonResponse | None]: + """Decode one JSON request body or return a normalized error response.""" + + try: + payload = json.loads(request.body.decode("utf-8") or "{}") + except json.JSONDecodeError: + return None, _auth_error("Request body must be valid JSON.") + + if not isinstance(payload, dict): + return None, _auth_error("Request body must be a JSON object.") + return payload, None + + +def _validation_error_message(error: ValidationError) -> str: + """Flatten one Django validation error into a stable response string.""" + + if hasattr(error, "messages") and error.messages: + return str(error.messages[0]) + return str(error) + + +def _validation_error_response(errors: dict[str, object]) -> JsonResponse: + """Return one field-error payload compatible with the existing auth clients.""" + + return JsonResponse(errors, status=400) + + +def _serialize_auth_user(user: AppUser) -> dict[str, object]: + """Return the compatibility payload shape for auth user details.""" + + return { + "pk": user.pk, + "username": user.username, + "email": user.email, + "first_name": user.first_name, + "last_name": user.last_name, + } + + +def _authenticated_user(request: HttpRequest) -> AppUser | None: + """Return one authenticated user using the shared API auth bridge.""" + + authenticated_user = api_authenticate(request) + if isinstance(authenticated_user, AppUser): + return authenticated_user + return None + + +def _password_reset_url(*, uid: str, token: str) -> str: + """Build the frontend password reset URL carried in reset emails.""" + + query = urlencode({"uid": uid, "token": token}) + return f"{settings.FRONTEND_BASE_URL.rstrip('/')}/reset-password?{query}" + + +def _send_password_reset_email(request: HttpRequest, *, email: str) -> None: + """Send password reset emails without revealing whether the email exists.""" + + account_adapter = get_account_adapter(request) + cleaned_email = account_adapter.clean_email(email) + users = AppUser.objects.filter(email__iexact=cleaned_email, is_active=True) + current_site = get_current_site(request) + + for user in users: + token = default_token_generator.make_token(user) + uid = user_pk_to_url_str(user) + context = { + "current_site": current_site, + "user": user, + "password_reset_url": _password_reset_url(uid=uid, token=token), + "request": request, + "token": token, + "uid": uid, + } + login_methods = getattr(settings, "ACCOUNT_LOGIN_METHODS", set()) + if "email" not in login_methods: + context["username"] = user_username(user) + account_adapter.send_mail( + "account/email/password_reset_key", + cleaned_email, + context, + ) + + +def _reset_password_user(*, uid: str) -> AppUser | None: + """Resolve one allauth-style password reset uid to a user instance.""" + + try: + user_pk = force_str(url_str_to_user_pk(uid)) + return AppUser.objects.get(pk=user_pk) + except (TypeError, ValueError, OverflowError, AppUser.DoesNotExist): + return None + + +def _authenticate_identifier_password( + request: HttpRequest, + *, + identifier: str, + password: str, +) -> AppUser | None: + """Authenticate one user by username first, then by email fallback.""" + + authenticated_user = authenticate(request, username=identifier, password=password) + if isinstance(authenticated_user, AppUser): + return authenticated_user + + matched_user = AppUser.objects.filter(email__iexact=identifier).first() + if matched_user is None: + return None + + authenticated_user = authenticate( + request, + username=matched_user.username, + password=password, + ) + if isinstance(authenticated_user, AppUser): + return authenticated_user + return None + + +@csrf_exempt +@require_POST +def login_view(request: HttpRequest) -> JsonResponse: + """Authenticate credentials and return JWT auth data for frontend sessions.""" + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + identifier = str(payload.get("username", "")).strip() + password = str(payload.get("password", "")) + if not identifier or not password: + return _auth_error("Enter both username and password.") + + user = _authenticate_identifier_password( + request, + identifier=identifier, + password=password, + ) + if user is None: + return _auth_error("Bad credentials.") + + django_login(request, user) + return _auth_response(user) + + +@csrf_exempt +@require_POST +def register_view(request: HttpRequest) -> JsonResponse: + """Create one user account and return JWT auth data for the new session.""" + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + username = str(payload.get("username", "")).strip() + email = str(payload.get("email", "")).strip().lower() + password1 = str(payload.get("password1", "")) + password2 = str(payload.get("password2", "")) + if not username or not email or not password1 or not password2: + return _auth_error("Enter a username, email, password1, and password2.") + if password1 != password2: + return _auth_error("The two password fields didn't match.") + + account_adapter = get_account_adapter(request) + try: + cleaned_email = account_adapter.clean_email(email) + validate_email(cleaned_email) + cleaned_username = account_adapter.clean_username(username) + cleaned_password = account_adapter.clean_password(password1) + except ValidationError as error: + return _auth_error(_validation_error_message(error)) + + if AppUser.objects.filter(email__iexact=cleaned_email).exists(): + return _auth_error("User is already registered with this e-mail address.") + + user = AppUser.objects.create_user( + username=cleaned_username, + email=cleaned_email, + password=cleaned_password, + ) + setup_user_email(request, user, []) + account_adapter.login(request, user) + return _auth_response(user, status_code=201) + + +@csrf_exempt +@require_POST +def logout_view(request: HttpRequest) -> JsonResponse: + """End the current Django session and return the legacy logout payload.""" + + django_logout(request) + return JsonResponse({"detail": "Successfully logged out."}) + + +@csrf_exempt +@require_POST +def password_reset_view(request: HttpRequest) -> JsonResponse: + """Send a password reset email without exposing account existence.""" + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + email = str(payload.get("email", "")).strip() + if not email: + return _validation_error_response({"email": ["This field is required."]}) + + try: + validate_email(email) + except ValidationError: + return _validation_error_response({"email": ["Enter a valid email address."]}) + + _send_password_reset_email(request, email=email) + return JsonResponse({"detail": "Password reset e-mail has been sent."}) + + +@csrf_exempt +@require_POST +def password_reset_confirm_view(request: HttpRequest) -> JsonResponse: + """Validate one reset token and save a new password.""" + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + uid = str(payload.get("uid", "")).strip() + token = str(payload.get("token", "")).strip() + new_password1 = str(payload.get("new_password1", "")) + new_password2 = str(payload.get("new_password2", "")) + if not uid: + return _validation_error_response({"uid": ["This field is required."]}) + if not token: + return _validation_error_response({"token": ["This field is required."]}) + + user = _reset_password_user(uid=uid) + if user is None: + return _validation_error_response({"uid": ["Invalid value"]}) + if not default_token_generator.check_token(user, token): + return _validation_error_response({"token": ["Invalid value"]}) + + form = SetPasswordForm( + user=user, + data={"new_password1": new_password1, "new_password2": new_password2}, + ) + if not form.is_valid(): + return JsonResponse(form.errors, status=400) + + form.save() + return JsonResponse({"detail": "Password has been reset with the new password."}) + + +@csrf_exempt +@require_POST +def password_change_view(request: HttpRequest) -> JsonResponse: + """Update the authenticated user's password and keep the session active.""" + + user = _authenticated_user(request) + if user is None: + return _auth_error( + "Authentication credentials were not provided.", + status_code=401, + ) + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + form = SetPasswordForm( + user=user, + data={ + "new_password1": str(payload.get("new_password1", "")), + "new_password2": str(payload.get("new_password2", "")), + }, + ) + if not form.is_valid(): + return JsonResponse(form.errors, status=400) + + form.save() + update_session_auth_hash(request, user) + return JsonResponse({"detail": "New password has been saved."}) + + +@method_decorator(csrf_exempt, name="dispatch") +class user_view(View): + """Read or update the authenticated user without framework-specific auth glue.""" + + http_method_names = ["get", "patch", "put"] + + def get(self, request: HttpRequest) -> JsonResponse: + user = _authenticated_user(request) + if user is None: + return _auth_error( + "Authentication credentials were not provided.", + status_code=401, + ) + + return JsonResponse(_serialize_auth_user(user)) + + def patch(self, request: HttpRequest) -> JsonResponse: + return self._update(request) + + def put(self, request: HttpRequest) -> JsonResponse: + return self._update(request) + + def _update(self, request: HttpRequest) -> JsonResponse: + user = _authenticated_user(request) + if user is None: + return _auth_error( + "Authentication credentials were not provided.", + status_code=401, + ) + + payload, error_response = _parse_json_payload(request) + if error_response is not None: + return error_response + assert payload is not None + + account_adapter = get_account_adapter(request) + username_value = payload.get("username") + if isinstance(username_value, str): + cleaned_username = username_value.strip() + if cleaned_username and cleaned_username != user.username: + try: + user.username = account_adapter.clean_username(cleaned_username) + except ValidationError as error: + return _auth_error(_validation_error_message(error)) + + first_name_value = payload.get("first_name") + if isinstance(first_name_value, str): + user.first_name = first_name_value + + last_name_value = payload.get("last_name") + if isinstance(last_name_value, str): + user.last_name = last_name_value + + user.save(update_fields=["username", "first_name", "last_name"]) + return JsonResponse(_serialize_auth_user(user)) + + def http_method_not_allowed(self, request: HttpRequest, *args, **kwargs): + return HttpResponseNotAllowed(self._allowed_methods()) + + +__all__ = [ + "login_view", + "logout_view", + "password_change_view", + "password_reset_confirm_view", + "password_reset_view", + "register_view", + "user_view", +] diff --git a/users/ninja_api.py b/users/ninja_api.py new file mode 100644 index 00000000..81843578 --- /dev/null +++ b/users/ninja_api.py @@ -0,0 +1,294 @@ +"""Django Ninja endpoints for the first increment of the users API migration.""" + +from __future__ import annotations + +from typing import Any, cast + +from django.core.exceptions import ValidationError +from django.utils import timezone +from ninja import File, Router, Schema +from ninja.errors import HttpError +from ninja.files import UploadedFile +from ninja.responses import Status + +from core.ninja_api import api_authenticate +from digest_engine.taskiq import enqueue_task, run_task_inline, task_always_eager +from projects.models import ProjectMembership +from users.models import ( + AVATAR_ALLOWED_CONTENT_TYPES, + AVATAR_MAX_FILE_SIZE, + AppUser, + MembershipInvitation, + avatar_thumbnail_path, +) +from users.tasks import generate_avatar_thumbnail + +router = Router(tags=["Users"]) + + +class UserProfileSchema(Schema): + """Serialized current-user profile payload.""" + + id: int + username: str + email: str + display_name: str | None = None + avatar: str | None = None + avatar_url: str | None = None + avatar_thumbnail_url: str | None = None + bio: str | None = None + timezone: str | None = None + first_name: str | None = None + last_name: str | None = None + + +class ProfileUpdateInput(Schema): + """Editable fields accepted by the profile patch endpoint.""" + + display_name: str | None = None + bio: str | None = None + timezone: str | None = None + + +class PublicMembershipInvitationSchema(Schema): + """Public invitation payload served by the invite token endpoint.""" + + token: str + project_id: int + project_name: str + email: str + role: str + status: str + accepted_at: str | None = None + revoked_at: str | None = None + + +def _serialize_profile(user: AppUser) -> dict[str, Any]: + """Return the profile response body for one user.""" + + return { + "id": int(user.pk), + "username": user.username, + "email": user.email, + "display_name": user.display_name, + "avatar": user.avatar_url, + "avatar_url": user.avatar_url, + "avatar_thumbnail_url": user.avatar_thumbnail_url, + "bio": user.bio, + "timezone": user.timezone, + "first_name": user.first_name, + "last_name": user.last_name, + } + + +def _serialize_public_invitation(invitation: MembershipInvitation) -> dict[str, Any]: + """Return the public invitation response body for one token.""" + + return { + "token": invitation.token, + "project_id": invitation.project_id, + "project_name": invitation.project.name, + "email": invitation.email, + "role": invitation.role, + "status": _invitation_status(invitation), + "accepted_at": ( + invitation.accepted_at.isoformat() if invitation.accepted_at else None + ), + "revoked_at": ( + invitation.revoked_at.isoformat() if invitation.revoked_at else None + ), + } + + +def _invitation_status(invitation: MembershipInvitation) -> str: + """Return the simple lifecycle status for one invitation token.""" + + if invitation.revoked_at is not None: + return "revoked" + if invitation.accepted_at is not None: + return "accepted" + return "pending" + + +def _delete_avatar_assets(user: AppUser) -> None: + """Delete the user's stored avatar and generated thumbnail files.""" + + if user.avatar: + storage = user.avatar.storage + avatar_name = user.avatar.name + thumbnail_name = avatar_thumbnail_path(user) + if avatar_name and storage.exists(avatar_name): + storage.delete(avatar_name) + if storage.exists(thumbnail_name): + storage.delete(thumbnail_name) + + +def _validation_error_payload(exc: ValidationError) -> dict[str, list[str]]: + """Convert a Django validation error into the Ninja error payload shape.""" + + if hasattr(exc, "message_dict"): + return { + field: [str(message) for message in messages] + for field, messages in exc.message_dict.items() + } + return {"__all__": [str(message) for message in exc.messages]} + + +def _validated_profile_payload( + payload: dict[str, Any], + *, + user: AppUser, +) -> dict[str, list[str]] | None: + """Validate one profile update payload against the AppUser model.""" + + for field_name, value in payload.items(): + setattr(user, field_name, value) + try: + user.full_clean() + except ValidationError as exc: + return _validation_error_payload(exc) + return None + + +def _validated_avatar_upload(avatar: UploadedFile) -> dict[str, list[str]] | None: + """Validate one uploaded avatar against the configured content limits.""" + + content_type = getattr(avatar, "content_type", "") + if content_type not in AVATAR_ALLOWED_CONTENT_TYPES: + return {"avatar": ["Upload a PNG, JPEG, or WebP avatar image."]} + avatar_size = getattr(avatar, "size", None) + if avatar_size is None or avatar_size > AVATAR_MAX_FILE_SIZE: + return {"avatar": ["Avatar images must be 2 MB or smaller."]} + return None + + +def _get_invitation_or_404(token: str) -> MembershipInvitation: + """Load one invitation row by token or raise a Ninja 404.""" + + try: + return MembershipInvitation.objects.select_related("project", "invited_by").get( + token=token + ) + except MembershipInvitation.DoesNotExist as exc: + raise HttpError(404, {"detail": "Not found."}) from exc + + +@router.get("/profile/", response=UserProfileSchema, auth=api_authenticate) +def get_profile(request): + """Return the current authenticated user's profile payload.""" + + return _serialize_profile(cast(AppUser, request.user)) + + +@router.patch( + "/profile/", + response={200: UserProfileSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def patch_profile(request, payload: ProfileUpdateInput): + """Update the current user's editable profile fields.""" + + user = cast(AppUser, request.user) + updated_fields = list(payload.model_dump(exclude_unset=True).keys()) + errors = _validated_profile_payload( + payload.model_dump(exclude_unset=True), + user=user, + ) + if errors is not None: + return Status(400, errors) + if updated_fields: + user.save(update_fields=updated_fields) + return _serialize_profile(user) + + +@router.post( + "/profile/avatar/", + response={200: UserProfileSchema, 400: dict[str, list[str]]}, + auth=api_authenticate, +) +def upload_profile_avatar(request, avatar: File[UploadedFile]): + """Store a new avatar image for the current user and queue a thumbnail.""" + + errors = _validated_avatar_upload(avatar) + if errors is not None: + return Status(400, errors) + + user = cast(AppUser, request.user) + _delete_avatar_assets(user) + user.avatar = avatar + user.save(update_fields=["avatar"]) + + if task_always_eager(): + run_task_inline(generate_avatar_thumbnail, user.id) + else: + enqueue_task(generate_avatar_thumbnail, user.id) + + return _serialize_profile(user) + + +@router.delete( + "/profile/avatar/", + response=UserProfileSchema, + auth=api_authenticate, +) +def delete_profile_avatar(request): + """Remove the current user's avatar image and generated thumbnail.""" + + user = cast(AppUser, request.user) + _delete_avatar_assets(user) + user.avatar = None + user.save(update_fields=["avatar"]) + return _serialize_profile(user) + + +@router.get("/invitations/{token}/", response=PublicMembershipInvitationSchema) +def get_membership_invitation(request, token: str): + """Return public invitation details for one token.""" + + del request + return _serialize_public_invitation(_get_invitation_or_404(token)) + + +@router.post( + "/invitations/{token}/", + response={ + 200: PublicMembershipInvitationSchema, + 400: dict[str, list[str]], + 403: dict[str, str], + }, + auth=api_authenticate, +) +def accept_membership_invitation(request, token: str): + """Accept a project invitation for the authenticated user.""" + + invitation = _get_invitation_or_404(token) + if invitation.revoked_at is not None: + return Status(400, {"token": ["This invitation has been revoked."]}) + if invitation.accepted_at is not None: + return Status(400, {"token": ["This invitation has already been accepted."]}) + + user = cast(AppUser, request.user) + user_email = (user.email or "").strip().lower() + invitation_email = invitation.email.strip().lower() + if not user_email or user_email != invitation_email: + return Status( + 403, {"detail": f"Sign in as {invitation.email} to accept this invite."} + ) + + membership, created = ProjectMembership.objects.update_or_create( + user=user, + project=invitation.project, + defaults={ + "role": invitation.role, + "invited_by": invitation.invited_by, + }, + ) + if created and membership.joined_at is None: + membership.save(update_fields=["joined_at"]) + + invitation.accepted_at = timezone.now() + invitation.save(update_fields=["accepted_at"]) + return _serialize_public_invitation(invitation) + + +__all__ = ["router"] diff --git a/users/serializers.py b/users/serializers.py deleted file mode 100644 index 42cde508..00000000 --- a/users/serializers.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Serializers for the custom user profile surface.""" - -from django.conf import settings -from rest_framework import serializers - -from users.models import ( - AVATAR_ALLOWED_CONTENT_TYPES, - AVATAR_MAX_FILE_SIZE, - AppUser, - MembershipInvitation, -) - - -class AppUserSerializer(serializers.ModelSerializer): - """Serialize the core user identity fields used by backend callers.""" - - avatar_url = serializers.ReadOnlyField() - avatar_thumbnail_url = serializers.ReadOnlyField() - - class Meta: - model = AppUser - fields = [ - "id", - "username", - "email", - "display_name", - "avatar", - "avatar_url", - "avatar_thumbnail_url", - "bio", - "timezone", - "first_name", - "last_name", - ] - read_only_fields = ["id"] - - -class ProfileSerializer(AppUserSerializer): - """Alias serializer used for profile-focused endpoints.""" - - class Meta(AppUserSerializer.Meta): - read_only_fields = [ - "id", - "username", - "email", - "avatar", - "avatar_url", - "avatar_thumbnail_url", - "first_name", - "last_name", - ] - - -class AvatarUploadSerializer(serializers.Serializer): - """Validate one uploaded avatar file for the current user.""" - - avatar = serializers.ImageField() - - def validate_avatar(self, value): - """Accept only supported image types under the configured size limit.""" - - content_type = getattr(value, "content_type", "") - if content_type not in AVATAR_ALLOWED_CONTENT_TYPES: - raise serializers.ValidationError( - "Upload a PNG, JPEG, or WebP avatar image." - ) - if value.size > AVATAR_MAX_FILE_SIZE: - raise serializers.ValidationError("Avatar images must be 2 MB or smaller.") - return value - - -class MembershipInvitationSerializer(serializers.ModelSerializer): - """Serialize one project invitation for project-admin workflows.""" - - invited_by_email = serializers.SerializerMethodField() - invite_url = serializers.SerializerMethodField() - - class Meta: - model = MembershipInvitation - fields = [ - "id", - "project", - "email", - "role", - "token", - "invited_by", - "invited_by_email", - "invite_url", - "created_at", - "accepted_at", - "revoked_at", - ] - read_only_fields = [ - "id", - "project", - "token", - "invited_by", - "invited_by_email", - "invite_url", - "created_at", - "accepted_at", - "revoked_at", - ] - - def validate_email(self, value: str) -> str: - """Normalize invited email addresses to lower-case.""" - - return value.strip().lower() - - def validate(self, attrs): - """Reject duplicate active invitations and existing memberships.""" - - attrs = super().validate(attrs) - project = self.context.get("project") - if project is None: - return attrs - - email = attrs.get("email", "").strip().lower() - if not email: - return attrs - - if project.memberships.filter(user__email__iexact=email).exists(): - raise serializers.ValidationError( - {"email": "That user is already a project member."} - ) - - active_invitation_exists = MembershipInvitation.objects.filter( - project=project, - email__iexact=email, - accepted_at__isnull=True, - revoked_at__isnull=True, - ).exists() - if active_invitation_exists: - raise serializers.ValidationError( - {"email": "An active invitation already exists for this email."} - ) - - return attrs - - def get_invited_by_email(self, obj: MembershipInvitation) -> str: - """Return the inviter email when available.""" - - return obj.invited_by.email if obj.invited_by else "" - - def get_invite_url(self, obj: MembershipInvitation) -> str: - """Return the frontend invitation URL for the token.""" - - return f"{settings.FRONTEND_BASE_URL.rstrip('/')}/invite/{obj.token}" - - -class PublicMembershipInvitationSerializer(serializers.ModelSerializer): - """Serialize invitation details safe to expose on the public invite page.""" - - project_id = serializers.IntegerField(source="project.id", read_only=True) - project_name = serializers.CharField(source="project.name", read_only=True) - status = serializers.SerializerMethodField() - - class Meta: - model = MembershipInvitation - fields = [ - "token", - "project_id", - "project_name", - "email", - "role", - "status", - "accepted_at", - "revoked_at", - ] - read_only_fields = fields - - def get_status(self, obj: MembershipInvitation) -> str: - """Return a simple token lifecycle status for the invite page.""" - - if obj.revoked_at is not None: - return "revoked" - if obj.accepted_at is not None: - return "accepted" - return "pending" diff --git a/users/tasks.py b/users/tasks.py index d16d8858..5c8e016c 100644 --- a/users/tasks.py +++ b/users/tasks.py @@ -2,12 +2,13 @@ from __future__ import annotations +import asyncio from io import BytesIO -from celery import shared_task from django.core.files.base import ContentFile from PIL import Image, ImageOps +from digest_engine.taskiq import broker from users.models import AppUser, avatar_thumbnail_path @@ -50,8 +51,8 @@ def build_avatar_thumbnail(user: AppUser) -> str | None: return thumbnail_name -@shared_task(name="users.tasks.generate_avatar_thumbnail") -def generate_avatar_thumbnail(user_id: int) -> str | None: +@broker.task(task_name="users.tasks.generate_avatar_thumbnail") +async def generate_avatar_thumbnail(user_id: int) -> str | None: """Generate a stored thumbnail for the given user's avatar. Args: @@ -61,5 +62,5 @@ def generate_avatar_thumbnail(user_id: int) -> str | None: The stored thumbnail path, or ``None`` when no avatar exists. """ - user = AppUser.objects.get(pk=user_id) - return build_avatar_thumbnail(user) + user = await AppUser.objects.aget(pk=user_id) + return await asyncio.to_thread(build_avatar_thumbnail, user) diff --git a/users/tests/test_auth_views.py b/users/tests/test_auth_views.py new file mode 100644 index 00000000..243e00fd --- /dev/null +++ b/users/tests/test_auth_views.py @@ -0,0 +1,435 @@ +"""Focused tests for the auth views.""" + +from __future__ import annotations + +import json + +import pytest +from allauth.account.forms import default_token_generator +from allauth.account.models import EmailAddress +from allauth.account.utils import user_pk_to_url_str +from django.core import mail +from django.test import Client + +from users.models import AppUser + +pytestmark = pytest.mark.django_db + + +def test_login_view_accepts_username_and_returns_jwt_credentials(): + client = Client() + user = AppUser.objects.create_user( + username="credentials-user", + email="credentials@example.com", + password="testpass123", + ) + + response = client.post( + "/api/auth/login/", + data=json.dumps({"username": user.username, "password": "testpass123"}), + content_type="application/json", + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["access"] + assert payload["refresh"] + assert payload["user"]["username"] == user.username + assert client.session.get("_auth_user_id") == str(user.id) + + +def test_login_view_accepts_email_and_issued_access_token_authenticates_ninja_api(): + client = Client() + user = AppUser.objects.create_user( + username="email-user", + email="email-user@example.com", + password="testpass123", + display_name="Email Login", + ) + + login_response = client.post( + "/api/auth/login/", + data=json.dumps({"username": user.email, "password": "testpass123"}), + content_type="application/json", + ) + + assert login_response.status_code == 200 + access_token = login_response.json()["access"] + + profile_response = client.get( + "/api/v1/profile/", + HTTP_AUTHORIZATION=f"Bearer {access_token}", + ) + + assert profile_response.status_code == 200 + assert profile_response.json()["username"] == user.username + + +def test_login_view_rejects_bad_credentials(): + client = Client() + AppUser.objects.create_user( + username="bad-login-user", + email="bad-login@example.com", + password="testpass123", + ) + + response = client.post( + "/api/auth/login/", + data=json.dumps({"username": "bad-login@example.com", "password": "wrong"}), + content_type="application/json", + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "Bad credentials." + + +def test_register_view_creates_user_returns_jwt_credentials_and_logs_the_user_in(): + client = Client() + + response = client.post( + "/api/auth/registration/", + data=json.dumps( + { + "username": "registered-user", + "email": "registered@example.com", + "password1": "testpass123", + "password2": "testpass123", + } + ), + content_type="application/json", + ) + + assert response.status_code == 201 + payload = response.json() + assert payload["access"] + assert payload["refresh"] + assert payload["user"]["username"] == "registered-user" + + user = AppUser.objects.get(username="registered-user") + assert client.session.get("_auth_user_id") == str(user.id) + assert EmailAddress.objects.filter( + user=user, + email="registered@example.com", + ).exists() + + +def test_register_view_issued_access_token_authenticates_ninja_api(): + client = Client() + + registration_response = client.post( + "/api/auth/registration/", + data=json.dumps( + { + "username": "registered-bearer-user", + "email": "registered-bearer@example.com", + "password1": "testpass123", + "password2": "testpass123", + } + ), + content_type="application/json", + ) + + assert registration_response.status_code == 201 + access_token = registration_response.json()["access"] + + profile_response = client.get( + "/api/v1/profile/", + HTTP_AUTHORIZATION=f"Bearer {access_token}", + ) + + assert profile_response.status_code == 200 + assert profile_response.json()["username"] == "registered-bearer-user" + + +def test_register_view_rejects_password_mismatch(): + client = Client() + + response = client.post( + "/api/auth/registration/", + data=json.dumps( + { + "username": "mismatch-user", + "email": "mismatch@example.com", + "password1": "testpass123", + "password2": "wrongpass456", + } + ), + content_type="application/json", + ) + + assert response.status_code == 400 + assert response.json()["detail"] == "The two password fields didn't match." + + +def test_register_view_rejects_duplicate_email(): + client = Client() + AppUser.objects.create_user( + username="existing-user", + email="duplicate@example.com", + password="testpass123", + ) + + response = client.post( + "/api/auth/registration/", + data=json.dumps( + { + "username": "new-user", + "email": "duplicate@example.com", + "password1": "testpass123", + "password2": "testpass123", + } + ), + content_type="application/json", + ) + + assert response.status_code == 400 + assert ( + response.json()["detail"] + == "User is already registered with this e-mail address." + ) + + +def test_logout_view_clears_the_session_and_returns_success_detail(): + client = Client() + user = AppUser.objects.create_user( + username="logout-user", + email="logout@example.com", + password="testpass123", + ) + client.force_login(user) + + response = client.post("/api/auth/logout/") + + assert response.status_code == 200 + assert response.json() == {"detail": "Successfully logged out."} + assert client.session.get("_auth_user_id") is None + + +def test_user_view_returns_legacy_details_payload_for_bearer_auth(): + client = Client() + user = AppUser.objects.create_user( + username="details-user", + email="details@example.com", + password="testpass123", + first_name="Detail", + last_name="Reader", + ) + + login_response = client.post( + "/api/auth/login/", + data=json.dumps({"username": user.username, "password": "testpass123"}), + content_type="application/json", + ) + + assert login_response.status_code == 200 + access_token = login_response.json()["access"] + + response = client.get( + "/api/auth/user/", + HTTP_AUTHORIZATION=f"Bearer {access_token}", + ) + + assert response.status_code == 200 + assert response.json() == { + "pk": user.pk, + "username": "details-user", + "email": "details@example.com", + "first_name": "Detail", + "last_name": "Reader", + } + + +def test_user_view_updates_supported_fields_and_keeps_email_read_only(): + client = Client() + user = AppUser.objects.create_user( + username="editable-user", + email="editable@example.com", + password="testpass123", + first_name="Old", + last_name="Name", + ) + client.force_login(user) + + response = client.patch( + "/api/auth/user/", + data=json.dumps( + { + "username": "edited-user", + "first_name": "New", + "last_name": "Person", + "email": "ignored@example.com", + } + ), + content_type="application/json", + ) + + assert response.status_code == 200 + user.refresh_from_db() + assert user.username == "edited-user" + assert user.first_name == "New" + assert user.last_name == "Person" + assert user.email == "editable@example.com" + assert response.json() == { + "pk": user.pk, + "username": "edited-user", + "email": "editable@example.com", + "first_name": "New", + "last_name": "Person", + } + + +def test_user_view_rejects_unauthenticated_requests(): + client = Client() + + response = client.get("/api/auth/user/") + + assert response.status_code == 401 + assert response.json()["detail"] == "Authentication credentials were not provided." + + +def test_password_reset_view_sends_email_and_returns_success_detail(): + client = Client() + AppUser.objects.create_user( + username="reset-user", + email="reset@example.com", + password="testpass123", + ) + + response = client.post( + "/api/auth/password/reset/", + data=json.dumps({"email": "reset@example.com"}), + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json() == {"detail": "Password reset e-mail has been sent."} + assert len(mail.outbox) == 1 + assert "reset-password?" in mail.outbox[0].body + assert "uid=" in mail.outbox[0].body + assert "token=" in mail.outbox[0].body + + +def test_password_reset_view_does_not_leak_unknown_email_addresses(): + client = Client() + + response = client.post( + "/api/auth/password/reset/", + data=json.dumps({"email": "missing@example.com"}), + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json() == {"detail": "Password reset e-mail has been sent."} + assert len(mail.outbox) == 0 + + +def test_password_reset_confirm_view_sets_the_new_password(): + client = Client() + user = AppUser.objects.create_user( + username="reset-confirm-user", + email="reset-confirm@example.com", + password="oldpass123", + ) + uid = user_pk_to_url_str(user) + token = default_token_generator.make_token(user) + + response = client.post( + "/api/auth/password/reset/confirm/", + data=json.dumps( + { + "uid": uid, + "token": token, + "new_password1": "newpass456", + "new_password2": "newpass456", + } + ), + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json() == { + "detail": "Password has been reset with the new password." + } + + login_response = client.post( + "/api/auth/login/", + data=json.dumps({"username": user.username, "password": "newpass456"}), + content_type="application/json", + ) + assert login_response.status_code == 200 + + +def test_password_reset_confirm_view_rejects_invalid_token(): + client = Client() + user = AppUser.objects.create_user( + username="invalid-reset-user", + email="invalid-reset@example.com", + password="oldpass123", + ) + + response = client.post( + "/api/auth/password/reset/confirm/", + data=json.dumps( + { + "uid": user_pk_to_url_str(user), + "token": "bad-token", + "new_password1": "newpass456", + "new_password2": "newpass456", + } + ), + content_type="application/json", + ) + + assert response.status_code == 400 + assert response.json() == {"token": ["Invalid value"]} + + +def test_password_change_view_updates_password_and_keeps_session_valid(): + client = Client() + user = AppUser.objects.create_user( + username="password-change-user", + email="password-change@example.com", + password="oldpass123", + ) + client.force_login(user) + + response = client.post( + "/api/auth/password/change/", + data=json.dumps( + { + "new_password1": "newpass456", + "new_password2": "newpass456", + } + ), + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json() == {"detail": "New password has been saved."} + assert client.session.get("_auth_user_id") == str(user.id) + + client.logout() + login_response = client.post( + "/api/auth/login/", + data=json.dumps({"username": user.username, "password": "newpass456"}), + content_type="application/json", + ) + assert login_response.status_code == 200 + + +def test_password_change_view_rejects_unauthenticated_requests(): + client = Client() + + response = client.post( + "/api/auth/password/change/", + data=json.dumps( + { + "new_password1": "newpass456", + "new_password2": "newpass456", + } + ), + content_type="application/json", + ) + + assert response.status_code == 401 + assert response.json()["detail"] == "Authentication credentials were not provided." diff --git a/users/tests/test_ninja_profile_api.py b/users/tests/test_ninja_profile_api.py new file mode 100644 index 00000000..77e6f41e --- /dev/null +++ b/users/tests/test_ninja_profile_api.py @@ -0,0 +1,346 @@ +"""Tests for the initial Django Ninja users API surface.""" + +from __future__ import annotations + +import base64 +import json +from http import HTTPStatus +from io import BytesIO +from typing import Any, cast +from unittest.mock import AsyncMock, patch + +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import Client, override_settings +from django.utils import timezone +from PIL import Image + +from core.jwt import issue_auth_tokens +from projects.models import Project, ProjectMembership, ProjectRole +from users.models import AppUser, MembershipInvitation + +pytestmark = pytest.mark.django_db + + +def _response(value: object) -> Any: + """Return a typed response object for assertion helpers.""" + + return cast(Any, value) + + +def _basic_auth_header(username: str, password: str) -> str: + """Return one HTTP Basic authorization header value.""" + + encoded = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("ascii") + return f"Basic {encoded}" + + +def make_avatar_file( + *, + filename: str = "avatar.png", + image_format: str = "PNG", + content_type: str = "image/png", +) -> SimpleUploadedFile: + """Build a small in-memory image upload for avatar tests.""" + + buffer = BytesIO() + Image.new("RGB", (512, 512), color=(12, 34, 56)).save(buffer, format=image_format) + return SimpleUploadedFile(filename, buffer.getvalue(), content_type=content_type) + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_get_returns_the_authenticated_user(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-reader", + email="reader@example.com", + password="testpass123", + display_name="Profile Reader", + bio="Writes weekly edits.", + ) + client.force_login(user) + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response(client.get("/api/v1/profile/")) + + assert response.status_code == HTTPStatus.OK + assert response.json()["username"] == "ninja-profile-reader" + assert response.json()["display_name"] == "Profile Reader" + assert response.json()["avatar_url"] is None + assert response.json()["avatar_thumbnail_url"] is None + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_get_accepts_bearer_auth(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-bearer", + email="bearer@example.com", + password="testpass123", + display_name="Bearer User", + ) + access_token = issue_auth_tokens(user)["access"] + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.get( + "/api/v1/profile/", + HTTP_AUTHORIZATION=f"Bearer {access_token}", + ) + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["username"] == "ninja-profile-bearer" + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_get_accepts_basic_auth(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-basic", + email="basic@example.com", + password="testpass123", + display_name="Basic User", + ) + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.get( + "/api/v1/profile/", + HTTP_AUTHORIZATION=_basic_auth_header(user.username, "testpass123"), + ) + ) + + assert response.status_code == HTTPStatus.OK + assert response.json()["username"] == "ninja-profile-basic" + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_patch_updates_profile_fields(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-editor", password="testpass123" + ) + client.force_login(user) + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.patch( + "/api/v1/profile/", + data=json.dumps( + { + "display_name": "Profile Editor", + "bio": "Owns the editorial calendar.", + "timezone": "America/New_York", + } + ), + content_type="application/json", + ) + ) + + user.refresh_from_db() + assert response.status_code == HTTPStatus.OK + assert user.display_name == "Profile Editor" + assert user.bio == "Owns the editorial calendar." + assert user.timezone == "America/New_York" + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_avatar_upload_returns_avatar_and_thumbnail_urls(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-avatar", password="testpass123" + ) + client.force_login(user) + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.post( + "/api/v1/profile/avatar/", + {"avatar": make_avatar_file()}, + ) + ) + + user.refresh_from_db() + thumbnail_path = tmp_path / f"avatars/{user.id}/thumb.webp" + + assert response.status_code == HTTPStatus.OK + assert user.avatar.name.endswith("avatar.png") + assert response.json()["avatar_url"].endswith(f"/{user.avatar.name}") + assert response.json()["avatar_thumbnail_url"].endswith( + f"/avatars/{user.id}/thumb.webp" + ) + assert thumbnail_path.exists() + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_avatar_upload_rejects_invalid_content_type(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-invalid-avatar", password="testpass123" + ) + client.force_login(user) + + invalid_file = SimpleUploadedFile( + "avatar.gif", + b"GIF89a", + content_type="image/gif", + ) + + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.post( + "/api/v1/profile/avatar/", + {"avatar": invalid_file}, + ) + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.json()["avatar"][0] == "Upload a PNG, JPEG, or WebP avatar image." + + +@override_settings(TASKIQ_ALWAYS_EAGER=True, MEDIA_URL="/media/") +def test_ninja_profile_avatar_delete_clears_avatar_and_thumbnail(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-delete", password="testpass123" + ) + client.force_login(user) + + with override_settings(MEDIA_ROOT=tmp_path): + upload_response = _response( + client.post( + "/api/v1/profile/avatar/", + {"avatar": make_avatar_file()}, + ) + ) + assert upload_response.status_code == HTTPStatus.OK + + response = _response(client.delete("/api/v1/profile/avatar/")) + user.refresh_from_db() + thumbnail_path = tmp_path / f"avatars/{user.id}/thumb.webp" + + assert response.status_code == HTTPStatus.OK + assert user.avatar.name == "" + assert response.json()["avatar_url"] is None + assert response.json()["avatar_thumbnail_url"] is None + assert thumbnail_path.exists() is False + + +@override_settings(TASKIQ_ALWAYS_EAGER=False, MEDIA_URL="/media/") +def test_ninja_profile_avatar_upload_queues_taskiq_thumbnail_task(tmp_path): + client = Client() + user = AppUser.objects.create_user( + username="ninja-profile-avatar-queued", password="testpass123" + ) + client.force_login(user) + + with patch( + "users.ninja_api.generate_avatar_thumbnail.kiq", + new_callable=AsyncMock, + ) as queue_thumbnail: + with override_settings(MEDIA_ROOT=tmp_path): + response = _response( + client.post( + "/api/v1/profile/avatar/", + {"avatar": make_avatar_file()}, + ) + ) + + user.refresh_from_db() + thumbnail_path = tmp_path / f"avatars/{user.id}/thumb.webp" + + assert response.status_code == HTTPStatus.OK + queue_thumbnail.assert_awaited_once_with(user.id) + assert user.avatar.name.endswith("avatar.png") + assert thumbnail_path.exists() is False + + +def test_ninja_invitation_token_get_returns_public_payload(): + client = Client() + inviter = AppUser.objects.create_user( + username="ninja-inviter", + email="inviter@example.com", + password="testpass123", + ) + project = Project.objects.create( + name="Ninja Invitation Project", + topic_description="Incremental API migration", + ) + invitation = MembershipInvitation.objects.create( + project=project, + email="invitee@example.com", + role=ProjectRole.READER, + invited_by=inviter, + ) + + response = _response(client.get(f"/api/v1/invitations/{invitation.token}/")) + + assert response.status_code == HTTPStatus.OK + assert response.json()["project_name"] == "Ninja Invitation Project" + assert response.json()["status"] == "pending" + + +def test_ninja_invitation_token_post_accepts_matching_user(): + client = Client() + inviter = AppUser.objects.create_user( + username="ninja-inviter-accept", + email="inviter@example.com", + password="testpass123", + ) + invitee = AppUser.objects.create_user( + username="ninja-invitee", + email="invitee@example.com", + password="testpass123", + ) + project = Project.objects.create( + name="Ninja Accept Project", + topic_description="Incremental API migration", + ) + invitation = MembershipInvitation.objects.create( + project=project, + email=invitee.email, + role=ProjectRole.MEMBER, + invited_by=inviter, + ) + client.force_login(invitee) + + response = _response(client.post(f"/api/v1/invitations/{invitation.token}/")) + + invitation.refresh_from_db() + membership = ProjectMembership.objects.get(user=invitee, project=project) + assert response.status_code == HTTPStatus.OK + assert membership.role == ProjectRole.MEMBER + assert invitation.accepted_at is not None + assert response.json()["status"] == "accepted" + + +def test_ninja_invitation_token_post_rejects_revoked_invitation(): + client = Client() + inviter = AppUser.objects.create_user( + username="ninja-inviter-revoked", + email="inviter@example.com", + password="testpass123", + ) + invitee = AppUser.objects.create_user( + username="ninja-invitee-revoked", + email="invitee@example.com", + password="testpass123", + ) + project = Project.objects.create( + name="Ninja Revoked Project", + topic_description="Incremental API migration", + ) + invitation = MembershipInvitation.objects.create( + project=project, + email=invitee.email, + role=ProjectRole.MEMBER, + invited_by=inviter, + revoked_at=timezone.now(), + ) + client.force_login(invitee) + + response = _response(client.post(f"/api/v1/invitations/{invitation.token}/")) + + assert response.status_code == HTTPStatus.BAD_REQUEST + assert response.json()["token"][0] == "This invitation has been revoked." diff --git a/users/tests/test_profile_api.py b/users/tests/test_profile_api.py deleted file mode 100644 index 1d44ab79..00000000 --- a/users/tests/test_profile_api.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Tests for the current-user profile API.""" - -from __future__ import annotations - -from io import BytesIO -from typing import Any, cast - -import pytest -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import override_settings -from PIL import Image -from rest_framework.test import APIClient - -from users.models import AppUser - -pytestmark = pytest.mark.django_db - - -def _response(value: object) -> Any: - """Return a typed API response for response assertions in tests.""" - - return cast(Any, value) - - -def make_avatar_file( - *, - filename: str = "avatar.png", - image_format: str = "PNG", - content_type: str = "image/png", -) -> SimpleUploadedFile: - """Build a small in-memory image upload for avatar tests.""" - - buffer = BytesIO() - Image.new("RGB", (512, 512), color=(12, 34, 56)).save(buffer, format=image_format) - return SimpleUploadedFile(filename, buffer.getvalue(), content_type=content_type) - - -@override_settings(CELERY_TASK_ALWAYS_EAGER=True, MEDIA_URL="/media/") -def test_profile_get_returns_the_authenticated_user(tmp_path): - client = APIClient() - user = AppUser.objects.create_user( - username="profile-reader", - email="reader@example.com", - password="testpass123", - display_name="Profile Reader", - bio="Writes weekly edits.", - ) - client.force_authenticate(user) - - with override_settings(MEDIA_ROOT=tmp_path): - response = _response(client.get("/api/v1/profile/")) - - assert response.status_code == 200 - assert response.json()["username"] == "profile-reader" - assert response.json()["display_name"] == "Profile Reader" - assert response.json()["avatar_url"] is None - assert response.json()["avatar_thumbnail_url"] is None - - -@override_settings(CELERY_TASK_ALWAYS_EAGER=True, MEDIA_URL="/media/") -def test_profile_patch_updates_profile_fields(tmp_path): - client = APIClient() - user = AppUser.objects.create_user( - username="profile-editor", password="testpass123" - ) - client.force_authenticate(user) - - with override_settings(MEDIA_ROOT=tmp_path): - response = _response( - client.patch( - "/api/v1/profile/", - { - "display_name": "Profile Editor", - "bio": "Owns the editorial calendar.", - "timezone": "America/New_York", - }, - format="json", - ) - ) - - user.refresh_from_db() - assert response.status_code == 200 - assert user.display_name == "Profile Editor" - assert user.bio == "Owns the editorial calendar." - assert user.timezone == "America/New_York" - - -@override_settings(CELERY_TASK_ALWAYS_EAGER=True, MEDIA_URL="/media/") -def test_profile_avatar_upload_returns_avatar_and_thumbnail_urls(tmp_path): - client = APIClient() - user = AppUser.objects.create_user( - username="profile-avatar", password="testpass123" - ) - client.force_authenticate(user) - - with override_settings(MEDIA_ROOT=tmp_path): - response = _response( - client.post( - "/api/v1/profile/avatar/", - {"avatar": make_avatar_file()}, - format="multipart", - ) - ) - - user.refresh_from_db() - thumbnail_path = tmp_path / f"avatars/{user.id}/thumb.webp" - - assert response.status_code == 200 - assert user.avatar.name.endswith("avatar.png") - assert response.json()["avatar_url"].endswith(f"/{user.avatar.name}") - assert response.json()["avatar_thumbnail_url"].endswith( - f"/avatars/{user.id}/thumb.webp" - ) - assert thumbnail_path.exists() - - -@override_settings(CELERY_TASK_ALWAYS_EAGER=True, MEDIA_URL="/media/") -def test_profile_avatar_delete_clears_avatar_and_thumbnail(tmp_path): - client = APIClient() - user = AppUser.objects.create_user( - username="profile-delete", password="testpass123" - ) - client.force_authenticate(user) - - with override_settings(MEDIA_ROOT=tmp_path): - upload_response = _response( - client.post( - "/api/v1/profile/avatar/", - {"avatar": make_avatar_file()}, - format="multipart", - ) - ) - assert upload_response.status_code == 200 - - response = _response(client.delete("/api/v1/profile/avatar/")) - user.refresh_from_db() - thumbnail_path = tmp_path / f"avatars/{user.id}/thumb.webp" - - assert response.status_code == 200 - assert user.avatar.name == "" - assert response.json()["avatar_url"] is None - assert response.json()["avatar_thumbnail_url"] is None - assert thumbnail_path.exists() is False diff --git a/uv.lock b/uv.lock index 90f9e3e3..7dca20c5 100644 --- a/uv.lock +++ b/uv.lock @@ -7,15 +7,118 @@ resolution-markers = [ ] [[package]] -name = "amqp" -version = "5.3.1" +name = "aio-pika" +version = "9.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiormq" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/63/56354526f2e6e915c93bee6e4dedb35888fe82d6bc1a19f35f5a77e795ff/aio_pika-9.6.2.tar.gz", hash = "sha256:c49e9246080dc8ffa1bb0e4aca407bf3d8ad78c3ee3a93df88b68fe65d7a49b9", size = 70851, upload-time = "2026-03-22T19:03:20.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/05/256fa313f48bed075056d13593b92ce804be05d75f4f312be24edb82860a/aio_pika-9.6.2-py3-none-any.whl", hash = "sha256:2a5478af920d169795071c9c09c7542cd8cdece60438cf7804533dcbcce93b7f", size = 56269, upload-time = "2026-03-22T19:03:19.558Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiormq" +version = "6.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pamqp" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/0e/db90154d52d399108903fe603e5110a533c42065180265dd003788264080/aiormq-6.9.4.tar.gz", hash = "sha256:0e7c01b662804e1cc7ace9a17794e8c1192a27fc2afa96162362a6e61ae8e8ef", size = 49232, upload-time = "2026-03-23T09:18:19.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/48/1ce3773f392f02ceda37aee168fade9d725483a9592c202d06044cd093ff/aiormq-6.9.4-py3-none-any.whl", hash = "sha256:726a8586695e863fba68cf88842065ab12348c9438dcebdfc9d0bddaf6083277", size = 32166, upload-time = "2026-03-23T09:18:17.523Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "vine" }, + { name = "frozenlist" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013, upload-time = "2024-11-12T19:55:44.051Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944, upload-time = "2024-11-12T19:55:41.782Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] @@ -129,15 +232,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, ] -[[package]] -name = "billiard" -version = "4.2.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/23/b12ac0bcdfb7360d664f40a00b1bda139cbbbced012c34e375506dbd0143/billiard-4.2.4.tar.gz", hash = "sha256:55f542c371209e03cd5862299b74e52e4fbcba8250ba611ad94276b369b6a85f", size = 156537, upload-time = "2025-11-30T13:28:48.52Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/87/8bab77b323f16d67be364031220069f79159117dd5e43eeb4be2fef1ac9b/billiard-4.2.4-py3-none-any.whl", hash = "sha256:525b42bdec68d2b983347ac312f892db930858495db601b5836ac24e6477cde5", size = 87070, upload-time = "2025-11-30T13:28:47.016Z" }, -] - [[package]] name = "black" version = "26.3.1" @@ -225,31 +319,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/29/e257a381d494615348c7266fc173a36edce142533a5befe3c0967fd45ab4/cbor2-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:f0bc04543c562bd0b35fc79a29528017fe63104757c1b421d8c1ddfbe6761eca", size = 290270, upload-time = "2026-05-14T10:57:40.597Z" }, ] -[[package]] -name = "celery" -version = "5.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "billiard" }, - { name = "click" }, - { name = "click-didyoumean" }, - { name = "click-plugins" }, - { name = "click-repl" }, - { name = "kombu" }, - { name = "python-dateutil" }, - { name = "tzlocal" }, - { name = "vine" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e8/b4/a1233943ab5c8ea05fb877a88a0a0622bf47444b99e4991a8045ac37ea1d/celery-5.6.3.tar.gz", hash = "sha256:177006bd2054b882e9f01be59abd8529e88879ef50d7918a7050c5a9f4e12912", size = 1742243, upload-time = "2026-03-26T12:14:51.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/c9/6eccdda96e098f7ae843162db2d3c149c6931a24fda69fe4ab84d0027eb5/celery-5.6.3-py3-none-any.whl", hash = "sha256:0808f42f80909c4d5833202360ffafb2a4f83f4d8e23e1285d926610e9a7afa6", size = 451235, upload-time = "2026-03-26T12:14:49.491Z" }, -] - -[package.optional-dependencies] -redis = [ - { name = "kombu", extra = ["redis"] }, -] - [[package]] name = "certifi" version = "2026.4.22" @@ -410,43 +479,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, ] -[[package]] -name = "click-didyoumean" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/ce/217289b77c590ea1e7c24242d9ddd6e249e52c795ff10fac2c50062c48cb/click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463", size = 3089, upload-time = "2024-03-24T08:22:07.499Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5b/974430b5ffdb7a4f1941d13d83c64a0395114503cc357c6b9ae4ce5047ed/click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c", size = 3631, upload-time = "2024-03-24T08:22:06.356Z" }, -] - -[[package]] -name = "click-plugins" -version = "1.1.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/a4/34847b59150da33690a36da3681d6bbc2ec14ee9a846bc30a6746e5984e4/click_plugins-1.1.1.2.tar.gz", hash = "sha256:d7af3984a99d243c131aa1a828331e7630f4a88a9741fd05c927b204bcf92261", size = 8343, upload-time = "2025-06-25T00:47:37.555Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/9a/2abecb28ae875e39c8cad711eb1186d8d14eab564705325e77e4e6ab9ae5/click_plugins-1.1.1.2-py2.py3-none-any.whl", hash = "sha256:008d65743833ffc1f5417bf0e78e8d2c23aab04d9745ba817bd3e71b0feb6aa6", size = 11051, upload-time = "2025-06-25T00:47:36.731Z" }, -] - -[[package]] -name = "click-repl" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "prompt-toolkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/a2/57f4ac79838cfae6912f997b4d1a64a858fb0c86d7fcaae6f7b58d267fca/click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9", size = 10449, upload-time = "2023-06-15T12:43:51.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/40/9d857001228658f0d59e97ebd4c346fe73e138c6de1bce61dc568a57c7f8/click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812", size = 10289, upload-time = "2023-06-15T12:43:48.626Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -721,7 +753,6 @@ dependencies = [ { name = "boto3" }, { name = "botocore" }, { name = "cbor2" }, - { name = "celery", extra = ["redis"] }, { name = "cfgv" }, { name = "channels" }, { name = "channels-redis" }, @@ -732,26 +763,20 @@ dependencies = [ { name = "daphne" }, { name = "deprecated" }, { name = "dj-database-url" }, - { name = "dj-rest-auth" }, { name = "django" }, { name = "django-allauth" }, { name = "django-anymail" }, { name = "django-cors-headers" }, { name = "django-import-export" }, + { name = "django-ninja" }, { name = "django-storages" }, { name = "django-stubs" }, { name = "django-stubs-ext" }, { name = "django-types" }, { name = "django-unfold" }, - { name = "djangochannelsrestframework" }, - { name = "djangorestframework" }, - { name = "djangorestframework-simplejwt" }, { name = "djlint" }, { name = "dnspython" }, { name = "docutils" }, - { name = "drf-nested-routers" }, - { name = "drf-spectacular" }, - { name = "drf-standardized-errors" }, { name = "editorconfig" }, { name = "feedparser" }, { name = "gunicorn" }, @@ -777,7 +802,6 @@ dependencies = [ { name = "nodeenv" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation-celery" }, { name = "opentelemetry-instrumentation-django" }, { name = "opentelemetry-sdk" }, { name = "ormsgpack" }, @@ -788,6 +812,7 @@ dependencies = [ { name = "py-ubjson" }, { name = "pyasn1" }, { name = "pyasn1-modules" }, + { name = "pyjwt" }, { name = "pylint-django" }, { name = "pylint-plugin-utils" }, { name = "pyopenssl" }, @@ -805,6 +830,9 @@ dependencies = [ { name = "standardwebhooks" }, { name = "structlog" }, { name = "svix" }, + { name = "taskiq" }, + { name = "taskiq-aio-pika" }, + { name = "taskiq-redis" }, { name = "twisted" }, { name = "txaio" }, { name = "types-deprecated" }, @@ -837,7 +865,6 @@ requires-dist = [ { name = "boto3", specifier = "==1.43.8" }, { name = "botocore", specifier = "==1.43.8" }, { name = "cbor2", specifier = "==6.1.1" }, - { name = "celery", extras = ["redis"], specifier = "==5.6.3" }, { name = "cfgv", specifier = "==3.5.0" }, { name = "channels", specifier = "==4.3.2" }, { name = "channels-redis", specifier = "==4.3.0" }, @@ -848,26 +875,20 @@ requires-dist = [ { name = "daphne", specifier = "==4.2.1" }, { name = "deprecated", specifier = "==1.3.1" }, { name = "dj-database-url", specifier = "==3.1.2" }, - { name = "dj-rest-auth", specifier = "==7.2.0" }, { name = "django", specifier = "==6.0.5" }, { name = "django-allauth", specifier = "==65.16.1" }, { name = "django-anymail", specifier = "==15.0" }, { name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-import-export", specifier = "==4.4.1" }, + { name = "django-ninja", specifier = ">=1.6.2" }, { name = "django-storages", specifier = "==1.14.6" }, { name = "django-stubs", specifier = "==6.0.4" }, { name = "django-stubs-ext", specifier = "==6.0.4" }, { name = "django-types", specifier = "==0.24.0" }, { name = "django-unfold", specifier = "==0.93.0" }, - { name = "djangochannelsrestframework", specifier = "==1.3.0" }, - { name = "djangorestframework", specifier = "==3.17.1" }, - { name = "djangorestframework-simplejwt", specifier = "==5.5.1" }, { name = "djlint", specifier = "==1.36.4" }, { name = "dnspython", specifier = "==2.8.0" }, { name = "docutils", specifier = "==0.22.4" }, - { name = "drf-nested-routers", specifier = "==0.95.0" }, - { name = "drf-spectacular", specifier = "==0.29.0" }, - { name = "drf-standardized-errors", specifier = "==0.16.0" }, { name = "editorconfig", specifier = "==0.17.1" }, { name = "feedparser", specifier = "==6.0.12" }, { name = "gunicorn", specifier = "==26.0.0" }, @@ -893,7 +914,6 @@ requires-dist = [ { name = "nodeenv", specifier = "==1.10.0" }, { name = "opentelemetry-api", specifier = ">=1.41.1" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.41.1,<2" }, - { name = "opentelemetry-instrumentation-celery", specifier = ">=0.62b1,<1" }, { name = "opentelemetry-instrumentation-django", specifier = ">=0.62b1,<1" }, { name = "opentelemetry-sdk", specifier = ">=1.41.1" }, { name = "ormsgpack", specifier = "==1.12.2" }, @@ -904,6 +924,7 @@ requires-dist = [ { name = "py-ubjson", specifier = "==0.16.1" }, { name = "pyasn1", specifier = "==0.6.3" }, { name = "pyasn1-modules", specifier = "==0.4.2" }, + { name = "pyjwt", specifier = "==2.12.1" }, { name = "pylint-django", specifier = "==2.7.0" }, { name = "pylint-plugin-utils", specifier = "==0.9.0" }, { name = "pyopenssl", specifier = "==26.2.0" }, @@ -921,6 +942,9 @@ requires-dist = [ { name = "standardwebhooks", specifier = "==1.0.1" }, { name = "structlog", specifier = "==25.5.0" }, { name = "svix", specifier = "==1.93.0" }, + { name = "taskiq", specifier = "==0.12.4" }, + { name = "taskiq-aio-pika", specifier = "==0.4.2" }, + { name = "taskiq-redis", specifier = "==1.0.0" }, { name = "twisted", specifier = "==26.4.0" }, { name = "txaio", specifier = "==25.12.2" }, { name = "types-deprecated", specifier = "==1.3.1.20260508" }, @@ -974,16 +998,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" }, ] -[[package]] -name = "dj-rest-auth" -version = "7.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "djangorestframework" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/14/fd/c8a185a98f6860b802dc5e663c652efc01cd68f4e046c65f8cb9328c0077/dj_rest_auth-7.2.0.tar.gz", hash = "sha256:f77af37da5cf6ee28f03f283a378a2f5761860e72d1d6f2fa3d50e6286000482", size = 490126, upload-time = "2026-03-15T04:05:39.272Z" } - [[package]] name = "django" version = "6.0.5" @@ -1053,6 +1067,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/91/d1/110aeb2acffea56d4222861b5678c2643f0bda00081e40d687077348bb7c/django_import_export-4.4.1-py3-none-any.whl", hash = "sha256:8be2782e505ae303ccb02070a1b4c528995922126fca9ee449eb28666835dd4b", size = 157691, upload-time = "2026-05-05T12:42:42.554Z" }, ] +[[package]] +name = "django-ninja" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/7c/3307e17b872f545c88314b2737a22f965785dfb5a120d739b0131d0492c3/django_ninja-1.6.2.tar.gz", hash = "sha256:d56ae5aa4791068ef4ac9a66cfdf2fc11f507413ded35abb79c51d0d52ad6412", size = 3685599, upload-time = "2026-03-18T20:06:47.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/0c/25f72060a39632fbd2d90e9c8b6052a09cd45b0598fc06c0758d313f0052/django_ninja-1.6.2-py3-none-any.whl", hash = "sha256:20095f5900bada22ea00cf1a58af50bdb285b2354c61a9d9b47d0dc89ac462d6", size = 2374994, upload-time = "2026-03-18T20:06:45.676Z" }, +] + [[package]] name = "django-storages" version = "1.14.6" @@ -1117,46 +1144,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/6c/c6c63af9eaabf85877005ba307a7af80088e9d28efd77c6431ec8c61c3c3/django_unfold-0.93.0-py3-none-any.whl", hash = "sha256:d0a09ebe906278fa994d6eb808880ba3f24c20d8c3a5ca37f82ee89969fd459c", size = 1249541, upload-time = "2026-05-11T10:13:09.185Z" }, ] -[[package]] -name = "djangochannelsrestframework" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "channels" }, - { name = "django" }, - { name = "djangorestframework" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/55/a2bae8b7e8a024167e76a131c7c9d4cb406ad131e4d618d0edf06d05f899/djangochannelsrestframework-1.3.0.tar.gz", hash = "sha256:f797a2b7cdc916f38da32de2a61f708efd2b81ea98afcc3dcc1f7fb4e5269fc3", size = 39904, upload-time = "2025-02-12T03:16:34.455Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/16/2056b143502f3486b9bdce30f245ecc244e4e37c7119f55cdcad7e73c910/djangochannelsrestframework-1.3.0-py3-none-any.whl", hash = "sha256:f4c1e5173952fc86eaf48aff812395c897284859981758ace49856fad2683dee", size = 34345, upload-time = "2025-02-12T03:16:31.998Z" }, -] - -[[package]] -name = "djangorestframework" -version = "3.17.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" }, -] - -[[package]] -name = "djangorestframework-simplejwt" -version = "5.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "djangorestframework" }, - { name = "pyjwt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/27/2874a325c11112066139769f7794afae238a07ce6adf96259f08fd37a9d7/djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f", size = 101265, upload-time = "2025-07-21T16:52:25.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, -] - [[package]] name = "djlint" version = "1.36.4" @@ -1199,49 +1186,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] -[[package]] -name = "drf-nested-routers" -version = "0.95.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "djangorestframework" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/b2/070908ccd41bd6f025f5507d7f6e50009abe8be48393cc697d159b539f0b/drf_nested_routers-0.95.0.tar.gz", hash = "sha256:815978f802e578fd7035c74040c104909cbe97615de89a275d77e928f4029891", size = 23318, upload-time = "2025-09-09T02:01:58.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/61/69d951a6e8031389f5feade38531432325019e1047b23d5c6036a86c5e8f/drf_nested_routers-0.95.0-py2.py3-none-any.whl", hash = "sha256:dd489c33d667aaa81383ffaa8c74781d2b353d8f0795716ae37fc59ee297b7c4", size = 36407, upload-time = "2025-09-09T02:01:56.999Z" }, -] - -[[package]] -name = "drf-spectacular" -version = "0.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "djangorestframework" }, - { name = "inflection" }, - { name = "jsonschema" }, - { name = "pyyaml" }, - { name = "uritemplate" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/0e/a4f50d83e76cbe797eda88fc0083c8ca970cfa362b5586359ef06ec6f70a/drf_spectacular-0.29.0.tar.gz", hash = "sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc", size = 241722, upload-time = "2025-11-02T03:40:26.348Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d9/502c56fc3ca960075d00956283f1c44e8cafe433dada03f9ed2821f3073b/drf_spectacular-0.29.0-py3-none-any.whl", hash = "sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a", size = 105433, upload-time = "2025-11-02T03:40:24.823Z" }, -] - -[[package]] -name = "drf-standardized-errors" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "djangorestframework" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/99/3f/1a465bde6583355309559dfcfb4e7fdf1b3687f73894d7de8a959c67b50c/drf_standardized_errors-0.16.0.tar.gz", hash = "sha256:77b367955af7ed246db12d299a5a75a71eccef54dae4ef69ead29f5fb01be192", size = 62112, upload-time = "2026-04-29T09:38:41.613Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7f/a72f5550b3520676e05c43f8da26b4264d994883e93c4c633f79b2f82a03/drf_standardized_errors-0.16.0-py3-none-any.whl", hash = "sha256:cf0ef3120cbbe81c1880cbb6b2d9b26cc6d146d07df06de3deb59e7ea5fb78c9", size = 26076, upload-time = "2026-04-29T09:38:39.939Z" }, -] - [[package]] name = "editorconfig" version = "0.17.1" @@ -1272,6 +1216,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "fsspec" version = "2026.4.0" @@ -1615,53 +1632,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "kombu" -version = "5.6.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "amqp" }, - { name = "packaging" }, - { name = "tzdata" }, - { name = "vine" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/a5/607e533ed6c83ae1a696969b8e1c137dfebd5759a2e9682e26ff1b97740b/kombu-5.6.2.tar.gz", hash = "sha256:8060497058066c6f5aed7c26d7cd0d3b574990b09de842a8c5aaed0b92cc5a55", size = 472594, upload-time = "2025-12-29T20:30:07.779Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/0f/834427d8c03ff1d7e867d3db3d176470c64871753252b21b4f4897d1fa45/kombu-5.6.2-py3-none-any.whl", hash = "sha256:efcfc559da324d41d61ca311b0c64965ea35b4c55cc04ee36e55386145dace93", size = 214219, upload-time = "2025-12-29T20:30:05.74Z" }, -] - -[package.optional-dependencies] -redis = [ - { name = "redis" }, -] - [[package]] name = "langchain-core" version = "1.4.0" @@ -2000,6 +1970,87 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -2287,20 +2338,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/0f/45adbaea1f81b847cffdcee4f4b5f89297e42facf7fac78c7aaac4c38e75/opentelemetry_instrumentation-0.62b1-py3-none-any.whl", hash = "sha256:976fc6e640f2006599e97429c949e622c108d0c17c2059347d1e6c93c707f257", size = 34163, upload-time = "2026-04-24T13:21:31.722Z" }, ] -[[package]] -name = "opentelemetry-instrumentation-celery" -version = "0.62b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/86/9e78c174b2f6ea92af3f99aa7488807b74290a5cd44a8e05bfbfd7b109be/opentelemetry_instrumentation_celery-0.62b1.tar.gz", hash = "sha256:f0035abd464a2989414a9c5ecdd79a25c87bd8c43f96c7f39e07000c6f25dfef", size = 14809, upload-time = "2026-04-24T13:22:45.656Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/51/f38a31ac8f8e3bd365f301f697661679addaf548d52a05cfdde4448a5493/opentelemetry_instrumentation_celery-0.62b1-py3-none-any.whl", hash = "sha256:50567a47b7adc4ea552d09709de4d73fea7b4ff24ab0e9d38739d03fcd3f95ef", size = 13864, upload-time = "2026-04-24T13:21:46.557Z" }, -] - [[package]] name = "opentelemetry-instrumentation-django" version = "0.62b1" @@ -2457,6 +2494,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "pamqp" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/62/35bbd3d3021e008606cd0a9532db7850c65741bbf69ac8a3a0d8cfeb7934/pamqp-3.3.0.tar.gz", hash = "sha256:40b8795bd4efcf2b0f8821c1de83d12ca16d5760f4507836267fd7a02b06763b", size = 30993, upload-time = "2024-01-12T20:37:25.085Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/8d/c1e93296e109a320e508e38118cf7d1fc2a4d1c2ec64de78565b3c445eb5/pamqp-3.3.0-py2.py3-none-any.whl", hash = "sha256:c901a684794157ae39b52cbf700db8c9aae7a470f13528b9d7b4e5f7202f8eb0", size = 33848, upload-time = "2024-01-12T20:37:21.359Z" }, +] + [[package]] name = "pathspec" version = "1.1.1" @@ -2597,15 +2643,80 @@ wheels = [ ] [[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -2705,6 +2816,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pycron" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/5d/340be12ae4a69c33102dfb6ddc1dc6e53e69b2d504fa26b5d34a472c3057/pycron-3.2.0.tar.gz", hash = "sha256:e125a28aca0295769541a40633f70b602579df48c9cb357c36c28d2628ba2b13", size = 4248, upload-time = "2025-06-05T13:24:12.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/76/caf316909f4545e7158e0e1defd8956a1da49f4af04f5d16b18c358dfeac/pycron-3.2.0-py3-none-any.whl", hash = "sha256:6d2349746270bd642b71b9f7187cf13f4d9ee2412b4710396a507b5fe4f60dac", size = 4904, upload-time = "2025-06-05T13:24:11.477Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -3028,24 +3148,14 @@ wheels = [ [[package]] name = "redis" -version = "6.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" +version = "5.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, + { name = "pyjwt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/cf/128b1b6d7086200c9f387bd4be9b2572a30b90745ef078bd8b235042dc9f/redis-5.3.1.tar.gz", hash = "sha256:ca49577a531ea64039b5a36db3d6cd1a0c7a60c34124d46924a45b956e8cf14c", size = 4626200, upload-time = "2025-07-25T08:06:27.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/7f/26/5c5fa0e83c3621db835cfc1f1d789b37e7fa99ed54423b5f519beb931aa7/redis-5.3.1-py3-none-any.whl", hash = "sha256:dc1909bd24669cc31b5f67a039700b16ec30571096c5f1f0d9d2324bff31af97", size = 272833, upload-time = "2025-07-25T08:06:26.317Z" }, ] [[package]] @@ -3160,72 +3270,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, -] - [[package]] name = "ruff" version = "0.15.13" @@ -3513,6 +3557,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" }, ] +[[package]] +name = "taskiq" +version = "0.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anyio" }, + { name = "packaging" }, + { name = "pycron" }, + { name = "pydantic" }, + { name = "taskiq-dependencies" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/7d/32bdfddd09fe3520bfb4978618dd87292201ca3f0bf9b9c413eb4ee946eb/taskiq-0.12.4.tar.gz", hash = "sha256:887e6acc72eea322a1515d9863551f00be7c81c6a0f72b2961569e58d68edc34", size = 397640, upload-time = "2026-05-08T10:43:59.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/0a/35b7b2673f923fd57bda8820afee03a6bb57883c1a61ee9e04e31e37ca50/taskiq-0.12.4-py3-none-any.whl", hash = "sha256:b910d3676e45b317678b4a8a1865ba6c261f696a02541461c765a7773cf4fa4b", size = 91804, upload-time = "2026-05-08T10:44:00.889Z" }, +] + +[[package]] +name = "taskiq-aio-pika" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aio-pika" }, + { name = "taskiq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/af/f534056b2164eea181c6e975850bc74f92b1d7509a4cd15edad088ad3e9d/taskiq_aio_pika-0.4.2.tar.gz", hash = "sha256:d62f4956ef3cb3deed3847bd70c381249f24186ac5c0cb60fb4bc609e61c2268", size = 6777, upload-time = "2025-04-20T23:31:59.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/11/ded35d67affe8acb5c5e48aaedcc29202995bc84a1449ed7672ff6c37a11/taskiq_aio_pika-0.4.2-py3-none-any.whl", hash = "sha256:a8eb80f9ada920d7384635c3853799b6f20e6123e453aa5929e5d35f39890878", size = 6801, upload-time = "2025-04-20T23:31:58.629Z" }, +] + +[[package]] +name = "taskiq-dependencies" +version = "1.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/90/47a627696e53bfdcacabc3e8c05b73bf1424685bcb5f17209cb8b12da1bf/taskiq_dependencies-1.5.7.tar.gz", hash = "sha256:0d3b240872ef152b719153b9526d866d2be978aeeaea6600e878414babc2dcb4", size = 14875, upload-time = "2025-02-26T22:07:39.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/6d/4a012f2de002c2e93273f5e7d3e3feea02f7fdbb7b75ca2ca1dd10703091/taskiq_dependencies-1.5.7-py3-none-any.whl", hash = "sha256:6fcee5d159bdb035ef915d4d848826169b6f06fe57cc2297a39b62ea3e76036f", size = 13801, upload-time = "2025-02-26T22:07:38.622Z" }, +] + +[[package]] +name = "taskiq-redis" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "taskiq" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/39/7c066c604f55d5dc5f632bd24d265a72cbed278740c12d7aa91f4015b735/taskiq_redis-1.0.0.tar.gz", hash = "sha256:45b2ac312a61725b4a5bdc5595f160d83986eac21eb5bc080ede5e6272d87d61", size = 9676, upload-time = "2024-06-11T18:26:43.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/5d/f101e0277122e9c064716ce2707cca191c337a362fbec7029759d824f8ac/taskiq_redis-1.0.0-py3-none-any.whl", hash = "sha256:867949594e63402bdb8378fcc9f1e4e1a18360cb7d3da30fe06d8c33cb74822f", size = 11932, upload-time = "2024-06-11T18:26:41.886Z" }, +] + [[package]] name = "tenacity" version = "9.1.4" @@ -3780,18 +3876,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] -[[package]] -name = "tzlocal" -version = "5.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tzdata", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" }, -] - [[package]] name = "u-msgpack-python" version = "2.8.0" @@ -3936,15 +4020,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/64/8be140712e3fa9d8406f0cb61876ce6d02f72067d4f9d31d1bf73e127c01/uuid_utils-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b3f0e567b5e992b28a50f50e0aeba546a2e2d3e463590eb5543204cb5d0f40b3", size = 171358, upload-time = "2026-05-11T12:07:30.282Z" }, ] -[[package]] -name = "vine" -version = "5.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/e4/d07b5f29d283596b9727dd5275ccbceb63c44a1a82aa9e4bfd20426762ac/vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0", size = 48980, upload-time = "2023-11-05T08:46:53.857Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/ff/7c0c86c43b3cbb927e0ccc0255cb4057ceba4799cd44ae95174ce8e8b5b2/vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc", size = 9636, upload-time = "2023-11-05T08:46:51.205Z" }, -] - [[package]] name = "virtualenv" version = "21.3.3" @@ -3981,15 +4056,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] -[[package]] -name = "wcwidth" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, -] - [[package]] name = "websocket-client" version = "1.9.0" @@ -4166,6 +4232,71 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, ] +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] + [[package]] name = "zipp" version = "3.23.1"