From 4961b055b95244ad3a9c98f314005a657f0fa06c Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 12:05:46 +0530 Subject: [PATCH 01/18] UN-4232 [FEAT] Agent-KV API serving the table extractor only Tasks 2 and 3 of docs/superpowers/plans/2026-10-06-table-extractor-api-carveout.md. Stands the Agent-KV API up on a branch cut from main, routing `table` and nothing else, so table extraction reaches customers without waiting for the KV engine, the schema codegen path or the hardened sandbox to stabilise. WHAT CAME ACROSS The `agent_kv` Django app and its URL/settings mounts, the AGENT_KV storage type, the `agent_kv_callback` queue and its ide_callback tasks, the scheduler's sweep/TTL beat tasks, the internal API client, the webhook notifier, and the compose/worker wiring for all of it. FOUR SUBTRACTIONS, EACH REVERTIBLE IN ONE LINE 1. `kv` is out of `EXTRACTOR_ROUTES`. `SUPPORTED_EXTRACTORS` is derived from that table, so a `kv` submit now returns 400 at the serializer. This is the load-bearing line: leave `kv` routable with no `agentic_kv` plugin deployed and the submit returns 202, dispatches to `celery_executor_agentic_kv`, and sits in DISPATCHED forever -- no error at the producer, nothing in any log. `V1_EXTRACTOR_NAME`, `STAGE_NAMES` and `KVOptionsSerializer` all stay in the tree, dormant and still tested. 2. `celery_executor_agentic_kv` is out of both compose fleets and run-worker.sh, for the same reason. 3. `/validate` is unregistered. It compiles `kv` key schemas and nothing else; publishing it for an extractor this deployment refuses is an incoherent contract. The view class and `unstract/agent-kv-schema` stay. 4. No sandbox: not the worker, not its compose service, not PG_ROLE_SANDBOX, not its SANDBOX_* env block, not its registry/enum entries, not its coverage target. `agentic_table` keeps running generated code in the executor pod exactly as the IDE table path does in production today; UN-4215 is the fast-follow. worker-unified.Dockerfile is reverted to main: its UID pin exists so the sandbox pod can assert runAsNonRoot with a numeric UID, and its comments cite a chart template this PR does not ship. TESTS Two guards were INVERTED rather than deleted, which is the point of them: * `test_queue_consumer_wiring.py` used to assert the KV queue HAS a consumer; it now asserts no fleet advertises it. Mutation-checked -- re-adding the queue to compose fails the guard. * `test_registry.py` was entirely sandbox assertions; it now asserts the ide_callback worker subscribes to `agent_kv_callback` and that both terminal callbacks route there. A job whose callback is unconsumed runs to SUCCESS and then sits in RUNNING forever, result unpersisted and slot unreleased. Backend suite re-pointed from `kv` to `table`. `KVOptionsSerializer` is now tested directly rather than through a submit -- `kv` is refused at `validate_name` before any options validator runs, so driving those rules through `SubmitSerializer` would have asserted nothing while still passing. E2E: the generic scenarios (auth, cancel, concurrency slot release, delete, page cap, sync-wait, webhook, 429, unreadable PDF, Excel) re-pointed at the table extractor; the five kv-engine scenarios dropped (happy path -- the table twin already covers it, /validate, calculations, hostile calculations, and the KV document cache, which the table path does not have until spec step 5). Green: 191 backend agent_kv, 65 workers, 42 agent-kv-schema, 1 filesystem. ruff 0.3.4 (the pinned pre-commit version) clean on check and format. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/__init__.py | 0 backend/agent_kv/apps.py | 6 + backend/agent_kv/constants.py | 65 + backend/agent_kv/dispatch.py | 158 + backend/agent_kv/exceptions.py | 16 + backend/agent_kv/execution_serializers.py | 323 ++ backend/agent_kv/execution_urls.py | 29 + backend/agent_kv/execution_views.py | 373 ++ backend/agent_kv/execution_views_result.py | 63 + backend/agent_kv/internal_urls.py | 31 + backend/agent_kv/internal_views.py | 267 + backend/agent_kv/key_validator.py | 45 + backend/agent_kv/maintenance.py | 249 + backend/agent_kv/management/__init__.py | 0 .../agent_kv/management/commands/__init__.py | 0 .../management/commands/agent_kv_sweep.py | 24 + .../commands/agent_kv_ttl_cleanup.py | 24 + backend/agent_kv/migrations/0001_initial.py | 157 + .../migrations/0002_agentkvjob_extractor.py | 21 + .../0003_agentkvjob_cleanup_failed_at.py | 41 + backend/agent_kv/migrations/__init__.py | 0 backend/agent_kv/models.py | 142 + backend/agent_kv/permissions.py | 8 + backend/agent_kv/rate_limiter.py | 109 + backend/agent_kv/serializers.py | 15 + backend/agent_kv/storage.py | 165 + backend/agent_kv/tests/__init__.py | 0 backend/agent_kv/tests/_factories.py | 26 + backend/agent_kv/tests/fixtures/two_page.pdf | Bin 0 -> 551 bytes backend/agent_kv/tests/test_auth.py | 130 + backend/agent_kv/tests/test_dispatch.py | 297 ++ backend/agent_kv/tests/test_internal_views.py | 686 +++ backend/agent_kv/tests/test_job_views.py | 513 ++ backend/agent_kv/tests/test_key_management.py | 95 + .../tests/test_maintenance_commands.py | 54 + backend/agent_kv/tests/test_models.py | 54 + backend/agent_kv/tests/test_rate_limiter.py | 155 + backend/agent_kv/tests/test_storage.py | 304 ++ .../agent_kv/tests/test_submit_serializer.py | 365 ++ backend/agent_kv/tests/test_submit_view.py | 733 +++ backend/agent_kv/tests/test_sweeps.py | 646 +++ .../tests/test_table_extractor_routing.py | 241 + backend/agent_kv/tests/test_validate_view.py | 130 + backend/agent_kv/urls.py | 23 + backend/agent_kv/views.py | 55 + backend/backend/base_urls.py | 2 + backend/backend/internal_base_urls.py | 6 + backend/backend/settings/base.py | 57 +- backend/backend/urls_v2.py | 1 + backend/pyproject.toml | 2 + backend/sample.env | 27 + backend/uv.lock | 7 + docker/docker-compose.yaml | 9 +- docker/sample.env | 49 + docs/agent-kv-api.md | 1011 ++++ ...2026-10-06-table-extractor-api-carveout.md | 358 ++ pyproject.toml | 1 + tests/compose/docker-compose.test.yaml | 47 + tests/e2e/agent_kv/__init__.py | 0 tests/e2e/agent_kv/conftest.py | 283 + tests/e2e/agent_kv/fixtures/invoice.pdf | Bin 0 -> 2435 bytes tests/e2e/agent_kv/fixtures/invoice.xlsx | Bin 0 -> 5105 bytes tests/e2e/agent_kv/fixtures/rent_roll.pdf | Bin 0 -> 2492 bytes tests/e2e/agent_kv/test_agent_kv_e2e.py | 709 +++ tests/groups.yaml | 45 +- tox.ini | 5 + unstract/agent-kv-schema/pyproject.toml | 13 + .../src/unstract/agent_kv_schema/__init__.py | 15 + .../src/unstract/agent_kv_schema/compile.py | 265 + .../unstract/agent_kv_schema/constraints.py | 258 + .../unstract/agent_kv_schema/dataclasses.py | 53 + .../src/unstract/agent_kv_schema/kv_schema.py | 224 + .../unstract/agent_kv_schema/validators.py | 115 + unstract/agent-kv-schema/tests/__init__.py | 0 .../agent-kv-schema/tests/test_compile.py | 250 + .../agent-kv-schema/tests/test_constraints.py | 0 .../tests/test_constraints_tolerance.py | 131 + unstract/agent-kv-schema/uv.lock | 8 + .../filesystem/file_storage_config.py | 2 + .../unstract/filesystem/file_storage_types.py | 1 + unstract/filesystem/tests/__init__.py | 0 .../tests/test_agent_kv_storage_type.py | 15 + uv.lock | 7 + workers/ide_callback/agent_kv_tasks.py | 181 + workers/ide_callback/tasks.py | 16 + workers/ide_callback/tests/__init__.py | 0 .../tests/test_agent_kv_callbacks.py | 401 ++ workers/ide_callback/worker.py | 13 + workers/pyproject.toml | 2 + workers/run-worker-docker.sh | 5 +- workers/run-worker.sh | 7 +- workers/sample.env | 18 + workers/scheduler/agent_kv_tasks.py | 71 + workers/scheduler/tasks.py | 27 +- workers/shared/api/internal_client.py | 109 + workers/shared/enums/worker_enums_base.py | 4 + .../shared/infrastructure/config/registry.py | 5 + .../infrastructure/config/tests/__init__.py | 0 .../config/tests/test_registry.py | 44 + workers/shared/tests/test_agent_kv_client.py | 90 + workers/shared/tests/test_webhook_notify.py | 221 + workers/shared/utils/webhook_notify.py | 85 + .../tests/test_agent_kv_scheduler_tasks.py | 95 + workers/tests/test_queue_consumer_wiring.py | 190 + .../tests/test_worker_task_module_imports.py | 68 +- workers/uv.lock | 4677 +++++++++-------- 106 files changed, 14777 insertions(+), 2366 deletions(-) create mode 100644 backend/agent_kv/__init__.py create mode 100644 backend/agent_kv/apps.py create mode 100644 backend/agent_kv/constants.py create mode 100644 backend/agent_kv/dispatch.py create mode 100644 backend/agent_kv/exceptions.py create mode 100644 backend/agent_kv/execution_serializers.py create mode 100644 backend/agent_kv/execution_urls.py create mode 100644 backend/agent_kv/execution_views.py create mode 100644 backend/agent_kv/execution_views_result.py create mode 100644 backend/agent_kv/internal_urls.py create mode 100644 backend/agent_kv/internal_views.py create mode 100644 backend/agent_kv/key_validator.py create mode 100644 backend/agent_kv/maintenance.py create mode 100644 backend/agent_kv/management/__init__.py create mode 100644 backend/agent_kv/management/commands/__init__.py create mode 100644 backend/agent_kv/management/commands/agent_kv_sweep.py create mode 100644 backend/agent_kv/management/commands/agent_kv_ttl_cleanup.py create mode 100644 backend/agent_kv/migrations/0001_initial.py create mode 100644 backend/agent_kv/migrations/0002_agentkvjob_extractor.py create mode 100644 backend/agent_kv/migrations/0003_agentkvjob_cleanup_failed_at.py create mode 100644 backend/agent_kv/migrations/__init__.py create mode 100644 backend/agent_kv/models.py create mode 100644 backend/agent_kv/permissions.py create mode 100644 backend/agent_kv/rate_limiter.py create mode 100644 backend/agent_kv/serializers.py create mode 100644 backend/agent_kv/storage.py create mode 100644 backend/agent_kv/tests/__init__.py create mode 100644 backend/agent_kv/tests/_factories.py create mode 100644 backend/agent_kv/tests/fixtures/two_page.pdf create mode 100644 backend/agent_kv/tests/test_auth.py create mode 100644 backend/agent_kv/tests/test_dispatch.py create mode 100644 backend/agent_kv/tests/test_internal_views.py create mode 100644 backend/agent_kv/tests/test_job_views.py create mode 100644 backend/agent_kv/tests/test_key_management.py create mode 100644 backend/agent_kv/tests/test_maintenance_commands.py create mode 100644 backend/agent_kv/tests/test_models.py create mode 100644 backend/agent_kv/tests/test_rate_limiter.py create mode 100644 backend/agent_kv/tests/test_storage.py create mode 100644 backend/agent_kv/tests/test_submit_serializer.py create mode 100644 backend/agent_kv/tests/test_submit_view.py create mode 100644 backend/agent_kv/tests/test_sweeps.py create mode 100644 backend/agent_kv/tests/test_table_extractor_routing.py create mode 100644 backend/agent_kv/tests/test_validate_view.py create mode 100644 backend/agent_kv/urls.py create mode 100644 backend/agent_kv/views.py create mode 100644 docs/agent-kv-api.md create mode 100644 docs/superpowers/plans/2026-10-06-table-extractor-api-carveout.md create mode 100644 tests/e2e/agent_kv/__init__.py create mode 100644 tests/e2e/agent_kv/conftest.py create mode 100644 tests/e2e/agent_kv/fixtures/invoice.pdf create mode 100644 tests/e2e/agent_kv/fixtures/invoice.xlsx create mode 100644 tests/e2e/agent_kv/fixtures/rent_roll.pdf create mode 100644 tests/e2e/agent_kv/test_agent_kv_e2e.py create mode 100644 unstract/agent-kv-schema/pyproject.toml create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/__init__.py create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py create mode 100644 unstract/agent-kv-schema/src/unstract/agent_kv_schema/validators.py create mode 100644 unstract/agent-kv-schema/tests/__init__.py create mode 100644 unstract/agent-kv-schema/tests/test_compile.py create mode 100644 unstract/agent-kv-schema/tests/test_constraints.py create mode 100644 unstract/agent-kv-schema/tests/test_constraints_tolerance.py create mode 100644 unstract/agent-kv-schema/uv.lock create mode 100644 unstract/filesystem/tests/__init__.py create mode 100644 unstract/filesystem/tests/test_agent_kv_storage_type.py create mode 100644 workers/ide_callback/agent_kv_tasks.py create mode 100644 workers/ide_callback/tests/__init__.py create mode 100644 workers/ide_callback/tests/test_agent_kv_callbacks.py create mode 100644 workers/scheduler/agent_kv_tasks.py create mode 100644 workers/shared/infrastructure/config/tests/__init__.py create mode 100644 workers/shared/infrastructure/config/tests/test_registry.py create mode 100644 workers/shared/tests/test_agent_kv_client.py create mode 100644 workers/shared/tests/test_webhook_notify.py create mode 100644 workers/shared/utils/webhook_notify.py create mode 100644 workers/tests/test_agent_kv_scheduler_tasks.py create mode 100644 workers/tests/test_queue_consumer_wiring.py diff --git a/backend/agent_kv/__init__.py b/backend/agent_kv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/agent_kv/apps.py b/backend/agent_kv/apps.py new file mode 100644 index 0000000000..0b70b303e1 --- /dev/null +++ b/backend/agent_kv/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AgentKvConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "agent_kv" diff --git a/backend/agent_kv/constants.py b/backend/agent_kv/constants.py new file mode 100644 index 0000000000..431316e078 --- /dev/null +++ b/backend/agent_kv/constants.py @@ -0,0 +1,65 @@ +#: v1 accepts exactly one extractor (`kv`), so the job row does not carry which +#: one it ran. When fan-out lands this becomes per-job state rather than a +#: constant -- the wire format (spec §7.0) is already shaped for that. +V1_EXTRACTOR_NAME = "kv" + +STAGE_NAMES = [ + "document_processing", + "extraction", + "qa", + "challenge", + "normalize", + "constraints", + "codegen", + "code_execution", +] +EXECUTOR_NAME = "agentic_kv" +OPERATION_KV_EXTRACT = "kv_extract" +EXECUTION_SOURCE = "agent_kv_api" + +#: The table extractor, served by the cloud `agentic_table` plugin's blind-API +#: operation. Same executor as the IDE table path (and therefore the same, +#: already-wired `celery_executor_agentic_table` queue) with a second operation +#: -- a new executor name would derive a new queue needing wiring at five +#: sites, and an unwired queue accepts work and drains nothing, silently. +TABLE_EXTRACTOR_NAME = "table" + +TABLE_EXECUTOR_NAME = "agentic_table" +OPERATION_TABLE_EXTRACT_API = "table_extract_api" + +#: Which executor and operation each extractor dispatches to. `dispatch_job` +#: reads this rather than hardcoding one pair, so adding an extractor is one +#: entry here plus its options serializer and stage list. +#: +#: **`kv` is deliberately absent.** This deployment ships the `agentic_table` +#: plugin and not `agentic_kv`, so nothing drains `celery_executor_agentic_kv`. +#: `SUPPORTED_EXTRACTORS` is derived from this table's keys +#: (`execution_serializers.py`), so the omission turns a `kv` submit into a 400 +#: at the serializer. Restoring the entry below is all it takes to re-enable +#: the extractor once the plugin ships -- and restoring it WITHOUT the plugin +#: is the failure this guards: the submit would be accepted with a 202, +#: dispatched to a queue with no consumer, and sit in DISPATCHED forever with +#: no error at the producer and nothing in any log to find. +#: +#: `V1_EXTRACTOR_NAME`, `STAGE_NAMES` and the KV options serializer stay in the +#: tree, dormant and still under test, so re-enabling is one line rather than a +#: content merge against the branch that carries the engine. +EXTRACTOR_ROUTES = { + TABLE_EXTRACTOR_NAME: (TABLE_EXECUTOR_NAME, OPERATION_TABLE_EXTRACT_API), +} + +#: The table engine reports one coarse stage: it has no node-level progress +#: hooks (the IDE path gets `stream_log` only), so inventing finer stages here +#: would describe progress the executor cannot actually report. +TABLE_STAGE_NAMES = ["table_extraction"] + +#: Stage names ARE wire format -- they are returned to clients -- and they are +#: extractor-specific (`qa`/`challenge`/`codegen` mean nothing to the table +#: extractor). `_status_document` filters a job's recorded stages through the +#: list for the extractor that ran: `StageReportView` persists whatever name +#: the executor sends, so without a per-extractor list a table job's stages +#: would be stored and then silently filtered out of every status response. +STAGE_NAMES_BY_EXTRACTOR = { + V1_EXTRACTOR_NAME: STAGE_NAMES, + TABLE_EXTRACTOR_NAME: TABLE_STAGE_NAMES, +} diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py new file mode 100644 index 0000000000..54166bc7ad --- /dev/null +++ b/backend/agent_kv/dispatch.py @@ -0,0 +1,158 @@ +"""Executor dispatch glue (spec §5.3). One dispatch per job; UUID task_id.""" + +import logging +import uuid + +from celery import signature +from django.conf import settings +from django.utils import timezone + +from agent_kv.constants import EXECUTION_SOURCE, EXTRACTOR_ROUTES +from agent_kv.models import AgentKVJob, JobStatus +from unstract.sdk1.execution.context import ExecutionContext + +logger = logging.getLogger(__name__) + +CALLBACK_QUEUE = "agent_kv_callback" + + +class DispatchError(Exception): + """Enqueue failed; the caller terminalizes the job (spec §5.3).""" + + +def _dispatcher(): + # No `celery_app`: UN-4046 removed that parameter when the routing + # dispatcher's Celery branch went with the pg_queue_enabled flag. Passing it + # raises TypeError, so every submit failed to dispatch -- and because it + # fails at the call rather than at import, nothing catches it until a real + # request is made. + from pg_queue.executor_rpc import get_executor_dispatcher + + return get_executor_dispatcher() + + +def _platform_api_key(job) -> str: + # Lazy import: avoids Django app registry init order (mirrors + # PromptStudioHelper._get_platform_api_key). + from platform_settings_v2.platform_auth_service import ( + PlatformAuthenticationService, + ) + + # ``get_active_platform_key`` takes the org's public *slug* + # (``Organization.organization_id``, e.g. ``org_abc123``) and resolves it + # via ``get_organization_by_org_id`` -- NOT the row's UUID primary key that + # ``job.organization_id`` holds. Passing the PK here silently resolves to + # no organization and every dispatch fails with ``ActiveKeyNotFound`` + # (caught live in the Task 13b integration run). + org_slug = job.organization.organization_id + platform_key = PlatformAuthenticationService.get_active_platform_key(org_slug) + if not platform_key: + raise DispatchError(f"No active platform key for org {org_slug}") + return str(platform_key.key) + + +def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None: + executor_name, operation = EXTRACTOR_ROUTES[extractor] + org_id = str(job.organization_id) + # Everything that can fail — platform-key lookup, context construction, + # and the enqueue call itself — lives inside this try so no internal + # failure (e.g. a transient DB error resolving the platform key) can + # escape as a raw, uncaught exception. Only the post-success bookkeeping + # below runs outside it. + try: + job.task_id = uuid.uuid4() + context = ExecutionContext( + executor_name=executor_name, + operation=operation, + run_id=str(job.id), + execution_source=EXECUTION_SOURCE, + organization_id=org_id, + executor_params={ + "job_id": str(job.id), + "input_ref": job.input_ref, + "schema": schema, + "options": options, + "platform_api_key": _platform_api_key(job), + # The CAP the engine must enforce (spec §6.1/§6.6), not the + # measured count -- job.pages_total is None for Excel (no + # pre-OCR page concept), which would otherwise leave the + # engine with nothing to check the post-OCR virtual-page cap + # against. The measured count still rides along separately. + "max_pages": settings.AGENT_KV_MAX_PAGES, + "pages_total": job.pages_total, + }, + ) + cb_kwargs = {"callback_kwargs": {"job_id": str(job.id), "org_id": org_id}} + _dispatcher().dispatch_with_callback( + context, + on_success=signature( + "agent_kv_complete", kwargs=cb_kwargs, queue=CALLBACK_QUEUE + ), + on_error=signature("agent_kv_error", kwargs=cb_kwargs, queue=CALLBACK_QUEUE), + task_id=str(job.task_id), + ) + except DispatchError: + raise + except Exception as e: + raise DispatchError(str(e)) from e + job.status = JobStatus.DISPATCHED + job.dispatched_at = timezone.now() + # Guarded queryset UPDATE, not job.save(): a plain save would blindly + # overwrite whatever status this job already raced to. Concretely: the + # executor can fail (or the job be cancelled) essentially instantly + # after enqueue, and its finalize callback can land -- marking the row + # FAILED/CANCELLED -- before this post-enqueue bookkeeping runs. An + # unconditional save() here would rewrite that terminal status back to + # DISPATCHED, un-terminalizing the job forever (nothing else ever + # revisits a DISPATCHED row). Only a still-PENDING row is advanced; a + # row this UPDATE doesn't match is left exactly as the winning writer + # left it. `modified_at` is stamped automatically by + # BaseModelQuerySet.update() (utils/models/base_model.py). + # Everything below is POST-ENQUEUE bookkeeping. The task is already on the + # queue, so a failure here must never be reported as a failed dispatch: + # `SubmitView` turns a DispatchError into a FAILED job, and the executor + # would then run, callback, and find a terminal row it cannot write to -- + # the caller told nothing was billed for work that did run. Wrapped rather + # than left to propagate, which is what the single pre-review UPDATE did. + # + # Losing the bookkeeping entirely is recoverable: sweep phase 1 reaps a + # still-PENDING row with no `dispatched_at`, and phase 2's + # `dispatched_at IS NULL` arm covers the non-PENDING case. + try: + _record_dispatch(job) + except Exception: + logger.exception( + "agent-kv: dispatch bookkeeping failed for job %s after enqueue " + "(task is queued; the sweep will reconcile)", + job.id, + ) + + +def _record_dispatch(job) -> None: + """Persist task_id/status/dispatched_at against whatever the row raced to.""" + advanced = AgentKVJob.objects.filter(id=job.id, status=JobStatus.PENDING).update( + task_id=job.task_id, + status=job.status, + dispatched_at=job.dispatched_at, + ) + if not advanced: + # The row moved off PENDING between the enqueue above and this write. + # The benign case is a terminal status (the guard's whole purpose) -- + # but there is a non-terminal one: StageReportView promotes + # PENDING -> RUNNING on the executor's FIRST stage report, which can + # easily land before this bookkeeping. The guard above then matches 0 + # rows and `dispatched_at` stays NULL -- and a non-terminal row with a + # NULL `dispatched_at` is invisible to BOTH sweep phases: phase 1 + # requires `status=PENDING`, phase 2 filters `dispatched_at__lt=cutoff` + # and SQL `NULL < x` is never true. The job reports `running` forever + # and `GET result` 409s for the life of the row, with nothing able to + # recover it. + # + # So stamp the dispatch bookkeeping for any still-non-terminal row, + # WITHOUT touching `status`: the row genuinely was dispatched, and + # moving RUNNING back to DISPATCHED would lose the executor's own + # progress. `dispatched_at__isnull=True` keeps this idempotent and + # stops a retry overwriting the original dispatch time. + AgentKVJob.objects.filter(id=job.id, dispatched_at__isnull=True).exclude( + status__in=list(AgentKVJob.TERMINAL) + ).update(task_id=job.task_id, dispatched_at=job.dispatched_at) diff --git a/backend/agent_kv/exceptions.py b/backend/agent_kv/exceptions.py new file mode 100644 index 0000000000..459821d2ef --- /dev/null +++ b/backend/agent_kv/exceptions.py @@ -0,0 +1,16 @@ +from rest_framework.exceptions import APIException + + +class EngineUnavailable(APIException): + status_code = 501 + default_detail = "agent-kv engine not available on this deployment" + + +class RateLimited(APIException): + status_code = 429 + default_detail = "Too many requests" + + +class JobNotFound(APIException): + status_code = 404 + default_detail = "Job not found" diff --git a/backend/agent_kv/execution_serializers.py b/backend/agent_kv/execution_serializers.py new file mode 100644 index 0000000000..38e845b497 --- /dev/null +++ b/backend/agent_kv/execution_serializers.py @@ -0,0 +1,323 @@ +"""Submit-time validation: every §6.1 cap lives here, before any paid work.""" + +import json + +import pdfplumber +from django.conf import settings +from rest_framework import serializers + +from agent_kv.constants import EXTRACTOR_ROUTES, TABLE_EXTRACTOR_NAME, V1_EXTRACTOR_NAME +from unstract.agent_kv_schema import SchemaError, compile_schema + +# Images are deliberately ABSENT, and that is a cross-repo contract, not an +# oversight. The cloud engine's `_build_agent_graph` (agentic_kv +# kv_extractor.py) treats only `.pdf/.xlsx/.xls` as a document; anything else +# takes a branch that skips `document_processor` entirely, and +# `ImageLoader.load_pages` -- the only thing that would populate pages for an +# image -- has no call site anywhere in the plugin (the engine's own comment +# there says images are "out of P2 scope"). +# +# Accepting them here anyway failed OPEN: an image dispatched normally with +# `pages_total=1`, every key came back not-found, and the job returned +# `success: true` with a page billed. Refusing at submit is the honest +# behaviour until the engine side is wired; re-add them in the same change that +# gives `load_pages` a call site, not before. +ALLOWED_EXTENSIONS = {".pdf", ".xlsx", ".xls"} +PDF_LIKE = {".pdf"} +EXTRACTION_MODES = ("whole-doc", "per-page") + + +# Derived, not repeated: the result and status documents key their payloads by +# these names, so a literal here that no route/stage list knows about would let +# the serializer accept a name the responses file under something else. +SUPPORTED_EXTRACTORS = tuple(EXTRACTOR_ROUTES) + + +class KVOptionsSerializer(serializers.Serializer): + """The `kv` extractor's own knobs (spec §7.1). + + These used to be top-level submit fields. They are extractor-scoped now + because they are meaningless to any other extractor -- `qa` and `challenge` + describe the KV agent pipeline, not "the request". + """ + + qa = serializers.BooleanField(required=False, default=True) + challenge = serializers.BooleanField(required=False, default=True) + extraction_mode = serializers.ChoiceField( + required=False, choices=EXTRACTION_MODES, default="whole-doc" + ) + structured_output = serializers.BooleanField(required=False, default=False) + calculations = serializers.CharField(required=False, allow_blank=True, default="") + document_class = serializers.CharField( + required=False, allow_blank=True, default="", max_length=256 + ) + key_notes = serializers.CharField( + required=False, allow_blank=True, default="", max_length=10_000 + ) + + def validate_calculations(self, v): + if v and not settings.AGENT_KV_CALCULATIONS_ENABLED: + raise serializers.ValidationError( + "calculations is not available on this deployment yet" + ) + if len(v.encode("utf-8")) > settings.AGENT_KV_MAX_CALCULATIONS_BYTES: + raise serializers.ValidationError( + f"calculations exceeds {settings.AGENT_KV_MAX_CALCULATIONS_BYTES} bytes" + ) + return v + + def validate_structured_output(self, v): + if v and not settings.AGENT_KV_STRUCTURED_OUTPUT_ENABLED: + raise serializers.ValidationError( + "structured_output is not available on this deployment yet" + ) + return v + + def validate(self, data): + # DRF silently DROPS unknown fields. For per-extractor options that is + # the wrong default: an option aimed at the wrong extractor (or a typo) + # would be discarded and the job would run with a silently different + # configuration than the caller asked for. Reject instead. + unknown = set(self.initial_data) - set(self.fields) + if unknown: + raise serializers.ValidationError( + f"unknown options for extractor 'kv': {sorted(unknown)}" + ) + return data + + +class TableOptionsSerializer(serializers.Serializer): + """The `table` extractor's own knobs (spec §7.1). + + Deliberately a subset of what the IDE path accepts: `output_path` and the + IDE callback hints (`prompt_key`, `doc_name`) are meaningless on the blind + API, and `enable_highlight` has no consumer there. + + **`enable_header_mapping` changes the result shape.** With it off, the + engine returns `output.tables` as a flat list of row dicts. With it on, the + engine wraps them as `{"header_mapping": ..., "rows": [...]}` + (`runner.py:1712`; the executor's own enrichment path unwraps the same + shape at `executor.py:577`). The API returns whichever the caller asked + for, unchanged -- so a client that sets this flag must read `.rows`. + """ + + instructions = serializers.CharField( + required=False, allow_blank=True, default="", max_length=10_000 + ) + json_structure = serializers.CharField( + required=False, allow_blank=True, default="", max_length=100_000 + ) + enable_header_mapping = serializers.BooleanField(required=False, default=False) + correct_number_separators = serializers.BooleanField(required=False, default=False) + number_format = serializers.ChoiceField( + required=False, choices=("US", "EU"), default="US" + ) + + def validate(self, data): + # Same reason KVOptionsSerializer rejects unknowns: DRF drops them + # silently, so an option aimed at the wrong extractor would be + # discarded and the job would run with a configuration the caller + # never asked for. + unknown = set(self.initial_data) - set(self.fields) + if unknown: + raise serializers.ValidationError( + f"unknown options for extractor '{TABLE_EXTRACTOR_NAME}': " + f"{sorted(unknown)}" + ) + return data + + +class TableKeysSerializer(serializers.Serializer): + """The `table` extractor's `keys`. + + The wire format gives every extractor a `keys` member; for the table + extractor the thing being asked for is a table, named by `target_table` + (the engine's one required extraction parameter). + """ + + target_table = serializers.CharField(max_length=256) + + def validate(self, data): + unknown = set(self.initial_data) - set(self.fields) + if unknown: + raise serializers.ValidationError( + f"unknown keys for extractor '{TABLE_EXTRACTOR_NAME}': {sorted(unknown)}" + ) + return data + + +# Each extractor's own validators. Keyed by the same names as EXTRACTOR_ROUTES; +# `test_every_supported_extractor_has_a_route_stage_list_and_options_serializer` +# keeps the three tables in step. +_OPTIONS_SERIALIZERS = { + V1_EXTRACTOR_NAME: KVOptionsSerializer, + TABLE_EXTRACTOR_NAME: TableOptionsSerializer, +} + + +class ExtractorSerializer(serializers.Serializer): + """One entry of the submit's `extractors` array (spec §7.0/§7.1).""" + + name = serializers.CharField() + keys = serializers.JSONField() + options = serializers.DictField(required=False, default=dict) + + compiled = None + + def validate_name(self, v): + if v not in SUPPORTED_EXTRACTORS: + raise serializers.ValidationError( + f"unknown extractor '{v}'; supported: {list(SUPPORTED_EXTRACTORS)}" + ) + return v + + def validate_keys(self, spec): + name = (self.initial_data or {}).get("name") + if name == TABLE_EXTRACTOR_NAME: + keys = TableKeysSerializer(data=spec if isinstance(spec, dict) else {}) + keys.is_valid(raise_exception=True) + return keys.validated_data + # kv: size is capped on the SERIALIZED form -- the cap exists to bound + # parse and compile cost, and `keys` arrives here already parsed out of + # the `extractors` JSON. + # ensure_ascii=False so this measures the SAME bytes the outer + # `extractors` cap measured. + serialized = json.dumps(spec, ensure_ascii=False).encode("utf-8") + if len(serialized) > settings.AGENT_KV_MAX_SCHEMA_BYTES: + raise serializers.ValidationError("keys schema too large") + try: + self.compiled = compile_schema(spec) + except SchemaError as e: + raise serializers.ValidationError(str(e)) + return spec + + def validate(self, data): + # Same reason the options block rejects unknowns, one level up: DRF drops + # unrecognised keys, so `"option"` or `"Options"` for `"options"` would + # be discarded whole, `options` would default to {}, and the job would + # run with qa=True/challenge=True -- roughly double the LLM spend the + # caller asked for, with a 202 and no indication anything was ignored. + unknown = set(self.initial_data) - set(self.fields) + if unknown: + raise serializers.ValidationError( + f"unknown keys on extractor entry: {sorted(unknown)}" + ) + opts_cls = _OPTIONS_SERIALIZERS[data["name"]] + opts = opts_cls(data=data.get("options") or {}) + opts.is_valid(raise_exception=True) + data["options"] = opts.validated_data + return data + + +class SubmitSerializer(serializers.Serializer): + """Submit-time validation: every §6.1 cap lives here, before any paid work. + + The wire format is extractor-scoped (§7.0): per-extractor schema and knobs + live inside `extractors`, and only fields describing the REQUEST stay top + level. There is deliberately no alias for the old flat shape. + """ + + file = serializers.FileField() + extractors = serializers.CharField() # JSON array string, or a file part + # Job-level: the page range drives the shared OCR pass and the §6.1 page + # cap, so it cannot differ between extractors reading the same document. + page_start = serializers.IntegerField(required=False, default=1, min_value=1) + page_end = serializers.IntegerField( + required=False, default=None, allow_null=True, min_value=1 + ) + timeout = serializers.IntegerField(required=False, default=0, min_value=0) + tags = serializers.ListField( + child=serializers.CharField(max_length=64), + required=False, + default=list, + max_length=20, + ) + custom_data = serializers.JSONField(required=False, default=None, allow_null=True) + webhook_url = serializers.URLField( + required=False, allow_blank=True, default="", max_length=1024 + ) + + #: ``{extractor_name: compiled schema}`` -- populated during validation. + compiled = None + pages_total = None + + def validate_file(self, f): + name = (f.name or "").lower() + ext = "." + name.rsplit(".", 1)[-1] if "." in name else "" + if ext not in ALLOWED_EXTENSIONS: + raise serializers.ValidationError( + f"Unsupported file type '{ext}'; allowed: {sorted(ALLOWED_EXTENSIONS)}" + ) + max_bytes = settings.AGENT_KV_MAX_FILE_SIZE_MB * 1024 * 1024 + if f.size > max_bytes: + raise serializers.ValidationError( + f"File exceeds {settings.AGENT_KV_MAX_FILE_SIZE_MB}MB limit" + ) + return f + + def validate_timeout(self, v): + if v > settings.AGENT_KV_MAX_TIMEOUT_SECONDS: + raise serializers.ValidationError( + f"timeout must be 0..{settings.AGENT_KV_MAX_TIMEOUT_SECONDS}" + ) + return v + + def validate_extractors(self, raw): + # §7.1: `extractors` may arrive as an inline JSON string OR a file part; + # SubmitView reads a file-typed part into a string before constructing + # the serializer, exactly as it did for the old `keys` field. + if len(raw.encode("utf-8")) > settings.AGENT_KV_MAX_SCHEMA_BYTES: + raise serializers.ValidationError("extractors payload too large") + try: + entries = json.loads(raw) + except (ValueError, TypeError) as e: + raise serializers.ValidationError(f"extractors is not valid JSON: {e}") + if not isinstance(entries, list) or not entries: + raise serializers.ValidationError("extractors must be a non-empty JSON array") + if len(entries) > 1: + # The FORMAT is being fixed before launch; the fan-out execution is + # not built (one executor exists, and page images are not shared). + # Refusing loudly beats accepting a request we would silently run + # single-extractor. + raise serializers.ValidationError( + "multiple extractors are not supported yet; pass exactly one" + ) + + validated, compiled = [], {} + for i, entry in enumerate(entries): + if not isinstance(entry, dict): + raise serializers.ValidationError(f"extractors[{i}] must be an object") + ser = ExtractorSerializer(data=entry) + if not ser.is_valid(): + raise serializers.ValidationError({f"extractors[{i}]": ser.errors}) + validated.append(ser.validated_data) + compiled[ser.validated_data["name"]] = ser.compiled + self.compiled = compiled + return validated + + def validate(self, data): + start, end = data.get("page_start", 1), data.get("page_end") + if end is not None and end < start: + raise serializers.ValidationError( + {"page_end": "page_end must be >= page_start"} + ) + f = data["file"] + ext = "." + f.name.lower().rsplit(".", 1)[-1] + if ext in PDF_LIKE: + try: + with pdfplumber.open(f) as pdf: + self.pages_total = len(pdf.pages) + except Exception: + raise serializers.ValidationError({"file": "Unreadable PDF"}) + finally: + f.seek(0) + if self.pages_total > settings.AGENT_KV_MAX_PAGES: + raise serializers.ValidationError( + { + "file": f"Document has {self.pages_total} pages; " + f"max is {settings.AGENT_KV_MAX_PAGES} (§6.1)" + } + ) + # Excel: no page concept pre-OCR (spec §6.1); pages_total stays None, + # size cap already enforced; the engine enforces the post-OCR cap. + return data diff --git a/backend/agent_kv/execution_urls.py b/backend/agent_kv/execution_urls.py new file mode 100644 index 0000000000..eff4807722 --- /dev/null +++ b/backend/agent_kv/execution_urls.py @@ -0,0 +1,29 @@ +"""Public Agent-KV execution routes. + +``ValidateView`` is **deliberately not routed here.** It compiles a `kv` keys +schema through ``unstract.agent_kv_schema.compile_schema`` and does nothing +else; the `table` extractor's ``keys`` is ``{"target_table": ...}``, validated +by ``TableKeysSerializer`` at submit. Publishing an endpoint that validates +schemas for an extractor this deployment refuses with a 400 would be an +incoherent public contract. + +The view class and the schema package both stay in the tree. Deleting them +would force a content merge in the files the branch carrying the KV engine +rewrites most; leaving them makes restoring this endpoint one line. +""" + +from django.urls import path + +from agent_kv.execution_views import ( + JobCancelView, + JobResultView, + JobStatusView, + SubmitView, +) + +urlpatterns = [ + path("", SubmitView.as_view(), name="agent_kv_submit"), + path("", JobStatusView.as_view(), name="agent_kv_status"), + path("/result", JobResultView.as_view(), name="agent_kv_result"), + path("/cancel", JobCancelView.as_view(), name="agent_kv_cancel"), +] diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py new file mode 100644 index 0000000000..f2fbb324fc --- /dev/null +++ b/backend/agent_kv/execution_views.py @@ -0,0 +1,373 @@ +import logging +import time +from datetime import timedelta + +from django.conf import settings +from django.utils import timezone +from plugins import get_plugin +from rest_framework.response import Response +from rest_framework.views import APIView + +from agent_kv.constants import STAGE_NAMES_BY_EXTRACTOR +from agent_kv.dispatch import DispatchError, dispatch_job +from agent_kv.exceptions import EngineUnavailable, JobNotFound, RateLimited +from agent_kv.execution_serializers import SubmitSerializer +from agent_kv.execution_views_result import result_payload +from agent_kv.key_validator import AgentKVKeyValidator +from agent_kv.models import AgentKVJob, JobStatus +from agent_kv.rate_limiter import AgentKVConcurrencyLimiter, check_key_rate +from agent_kv.storage import delete_job_files, stage_input +from unstract.agent_kv_schema.compile import SchemaError, compile_schema + +logger = logging.getLogger(__name__) + + +def _get_job(agent_kv_key, job_id): + """Org-scoped lookup used by every job-scoped endpoint (spec §5.4). + + Unknown job_id and a job that belongs to a different org must be + indistinguishable to the caller, so both funnel through the same + ``DoesNotExist`` -> ``JobNotFound`` (404) path. + """ + try: + return AgentKVJob.objects.get( + id=job_id, organization_id=agent_kv_key.organization_id + ) + except AgentKVJob.DoesNotExist: + raise JobNotFound() + + +def _status_document(job) -> dict: + """Build the status document per spec §7.2.""" + stages_json = job.stages or {} + # `.get`, not a subscript. No row can hold an out-of-dict value today (the + # sole creation site is serializer-validated and the migration backfills + # "kv"), but a RETIRED extractor name with surviving rows would 500 every + # `GET status` for those jobs while `GET result` kept working -- the result + # payload keys by `job.extractor` without consulting this table at all. An + # empty stage list degrades to "no stages reported", which is honest for an + # extractor this build no longer knows how to describe. + stage_names = STAGE_NAMES_BY_EXTRACTOR.get(job.extractor) + if stage_names is None: + logger.warning( + "agent-kv job %s ran extractor %r, which has no stage list in " + "STAGE_NAMES_BY_EXTRACTOR; reporting an empty stage array", + job.id, + job.extractor, + ) + stage_names = [] + doc = { + "job_id": str(job.id), + # The JOB's state, not an extractor's: a job is not complete until every + # extractor is, so this stays top level (spec §7.2). + "status": job.status.lower(), + # Stage names are extractor-specific (`qa`/`challenge`/`codegen` mean + # nothing to the table extractor) and they ARE returned to clients, so + # they are wire format and namespaced with everything else. + "extractors": { + job.extractor: { + "stage": job.stage, + "stages": [ + {"name": name, **stages_json[name]} + for name in stage_names + if name in stages_json + ], + } + }, + "created_at": job.created_at.isoformat() if job.created_at else None, + "started_at": job.dispatched_at.isoformat() if job.dispatched_at else None, + "completed_at": job.completed_at.isoformat() if job.completed_at else None, + "pages_total": job.pages_total, + } + if job.status == JobStatus.FAILED: + doc["error"] = job.error + return doc + + +def _fail_job_response(job, org_id: str, message: str, *, job_saved: bool) -> Response: + """Shared cleanup for any post-acquire submit failure (spec §5.3/§5.4). + + Always releases the concurrency slot acquired earlier in the request. + Only calls ``mark_terminal`` when a job row may actually exist — calling + it against a job_id with no row is harmless (the guarded UPDATE just + matches zero rows), but ``job_saved`` keeps the write guard's intent + (only terminalize rows that exist) obvious at the call site. + """ + if job_saved: + AgentKVJob.mark_terminal( + job.id, job.organization_id, JobStatus.FAILED, error=message + ) + AgentKVConcurrencyLimiter.release(org_id, str(job.id)) + return Response( + {"job_id": str(job.id), "status": JobStatus.FAILED.lower(), "error": message}, + status=500, + ) + + +class SubmitView(APIView): + authentication_classes: list = [] + permission_classes: list = [] + + @AgentKVKeyValidator.validate_api_key + def post(self, request, *args, agent_kv_key=None, **kwargs): + plugin = get_plugin("agent_kv") + if not plugin: + raise EngineUnavailable() + if not check_key_rate(str(agent_kv_key.id)): + raise RateLimited() + + # Subscription admission (§6.6). A submit dispatches paid work, so it is + # gated exactly as an API deployment execute is -- same policy, same 402 + # bodies -- via the cloud plugin's gate, which calls the very + # `SubscriptionHelper` that cloud's `SubscriptionMiddleware` calls. + # + # Why here and not in that middleware: it resolves the org from the URL + # (`/deployment/api/{org_name}/...`). Agent-KV's URL carries no org + # segment -- the org is inside the Bearer key -- so the middleware's + # `get_organization_id` returns None for these requests, finds no + # subscription row, and admits every one of them. This is the first + # point where the org is actually known. + # + # `organization.organization_id` is the org SLUG -- the CharField + # `Subscription.organization_id` is keyed on, and what the deployment + # URL supplies as `org_name`. NOT `agent_kv_key.organization_id`, which + # is the Organization FK primary key: that matches no row, and the + # shared policy reads "no row" as "nothing to enforce", so the gate + # would silently admit everything while looking correctly wired. + gate_factory = plugin.get("service_class") + if gate_factory: # absent on a cloud build predating the gate + denied = gate_factory().check( + agent_kv_key.organization.organization_id, request + ) + if denied is not None: + return denied + + data = request.data.copy() + part = data.get("extractors") + if hasattr(part, "read"): # `extractors` uploaded as a file part (§7.1) + data["extractors"] = part.read().decode("utf-8", errors="replace") + serializer = SubmitSerializer(data=data) + serializer.is_valid(raise_exception=True) + v = serializer.validated_data + org_id = str(agent_kv_key.organization_id) + + job = AgentKVJob( + api_key=agent_kv_key, + organization_id=agent_kv_key.organization_id, + extractor=v["extractors"][0]["name"], + pages_total=serializer.pages_total, + tags=v["tags"], + custom_data=v["custom_data"], + webhook_url=v["webhook_url"], + expires_at=timezone.now() + timedelta(days=settings.AGENT_KV_RESULT_TTL_DAYS), + ) + if not AgentKVConcurrencyLimiter.check_and_acquire(org_id, str(job.id)): + raise RateLimited("Concurrent job limit reached") + + job_saved = False + try: + job.input_ref = stage_input(org_id, str(job.id), v["file"]) + job.save() + job_saved = True + except Exception: + logger.exception("agent-kv staging/save failed for job %s", job.id) + return _fail_job_response( + job, + org_id, + "Job could not be accepted; nothing was billed.", + job_saved=job_saved, + ) + + # Unpack the single extractor entry into the FROZEN OSS<->cloud + # executor_params contract (`schema` + `options`). The wire format + # changed at the API edge only: the engine still receives exactly the + # option names it always did, so no cloud change is required. + entry = v["extractors"][0] + options = dict(entry["options"]) + # Job-level, but the engine reads them from `options` (§7.1: the page + # range drives the shared OCR pass, so it cannot be per-extractor). + options["page_start"] = v["page_start"] + options["page_end"] = v["page_end"] + try: + dispatch_job( + job, extractor=entry["name"], schema=entry["keys"], options=options + ) + except DispatchError: + logger.exception("agent-kv dispatch failed for job %s", job.id) + return _fail_job_response( + job, + org_id, + "Job could not be dispatched; nothing was billed.", + job_saved=True, + ) + except Exception: + # Belt-and-braces: dispatch_job wraps its own internal failures + # as DispatchError, but nothing here may rely on that alone — + # any other exception must still terminalize the job and + # release the slot rather than escape as an unhandled 500. + logger.exception( + "agent-kv dispatch raised unexpectedly for job %s", + job.id, + ) + return _fail_job_response( + job, + org_id, + "Job could not be dispatched; nothing was billed.", + job_saved=True, + ) + + wait = v["timeout"] + if wait: + deadline = time.monotonic() + wait + while time.monotonic() < deadline: + job.refresh_from_db() + if job.status in AgentKVJob.TERMINAL: + from agent_kv.execution_views_result import result_payload + + return Response(result_payload(job), status=200) + time.sleep(1) + + return Response( + { + "job_id": str(job.id), + # Lowercased for cross-endpoint consistency (spec §7.2) -- the + # 202 body used to leak the raw uppercase status. + "status": job.status.lower(), + "status_url": f"/{settings.AGENT_KV_PATH_PREFIX}/{job.id}", + "created_at": job.created_at.isoformat(), + }, + status=202, + ) + + +class ValidateView(APIView): + authentication_classes: list = [] + permission_classes: list = [] + + @AgentKVKeyValidator.validate_api_key + def post(self, request, *args, agent_kv_key=None, **kwargs): + if not check_key_rate(str(agent_kv_key.id)): + raise RateLimited() + spec = request.data.get("keys") + if spec is None: + return Response({"detail": "body must include 'keys'"}, status=400) + try: + compiled = compile_schema(spec) + except SchemaError as e: + return Response({"valid": False, "error": str(e)}, status=200) + return Response( + { + "valid": True, + "leaves": len(compiled.key_specs), + "arrays": len(compiled.array_specs), + "constraints": len(compiled.constraints), + }, + status=200, + ) + + +class JobStatusView(APIView): + """GET status document; DELETE purges the job's staged/result files. + + DELETE is merged onto this class (rather than a standalone + ``JobDeleteView``) because both share the ```` URL — Django + matches a URL pattern once per request regardless of HTTP method, so two + separate ``path()`` entries for the same literal path can't coexist. + ``JobDeleteView`` below is kept as a name alias for this same class. + """ + + authentication_classes: list = [] + permission_classes: list = [] + + @AgentKVKeyValidator.validate_api_key + def get(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): + job = _get_job(agent_kv_key, job_id) + return Response(_status_document(job), status=200) + + @AgentKVKeyValidator.validate_api_key + def delete(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): + job = _get_job(agent_kv_key, job_id) + if job.status not in AgentKVJob.TERMINAL: + # Cancel BEFORE deleting files: a still-running job would + # otherwise keep running after its files are gone, and its + # eventual finalize call would write a fresh result_ref onto a + # job the caller already asked to delete -- resurrecting a + # result they explicitly discarded. Terminalizing first closes + # that window; a finalize call that still lands late loses the + # terminal-state guard and, on the success path, cleans up its + # own now-orphaned write (FinalizeView, storage.delete_result_file). + won = AgentKVJob.mark_terminal( + job.id, job.organization_id, JobStatus.CANCELLED + ) + if won: + # Release the concurrency slot, exactly as JobCancelView does + # and for the same reason: the slot is taken at submit and + # released by _fail_job_response, the finalize callback and the + # sweep -- but a job terminalized HERE before dispatch gets no + # finalize callback, and the sweep's phase-1 only targets + # PENDING, never CANCELLED. Without this, deleting in-flight + # jobs leaks a slot each until the 6h TTL, and enough deletes + # exhaust the org's allowance and start rejecting new submits. + # + # Guarded on `won` so a lost race (a concurrent cancel or + # finalize terminalized it first) does not release a slot that + # the winner is still accounting for; release() is idempotent + # (zrem), so the winner's own release stays correct. + AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + # Blank only the refs whose files are confirmed gone, so a ref whose + # delete failed survives as the handle TTL cleanup retries from. 204 + # either way: the job IS terminal and the caller's intent is recorded, + # and a sync 5xx here would only invite a retry of a DELETE that already + # did everything it could. + cleared = delete_job_files(job) + if cleared: + for field in cleared: + setattr(job, field, "") + job.save(update_fields=cleared) + return Response(status=204) + + +# Alias kept for a descriptive import name; there is no separate URL route +# (see the JobStatusView docstring above) — DELETE rides JobStatusView's URL. +JobDeleteView = JobStatusView + + +class JobResultView(APIView): + authentication_classes: list = [] + permission_classes: list = [] + + @AgentKVKeyValidator.validate_api_key + def get(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): + job = _get_job(agent_kv_key, job_id) + if job.status not in AgentKVJob.TERMINAL: + return Response({"status": job.status.lower()}, status=409) + # A job's row outlives its result by design (audit trail after TTL + # cleanup blanks the refs) -- expired or a COMPLETED job whose + # result was already swept both mean "nothing left to serve". + if (job.expires_at and job.expires_at < timezone.now()) or ( + job.status == JobStatus.COMPLETED and not job.result_ref + ): + raise JobNotFound() + return Response(result_payload(job), status=200) + + +class JobCancelView(APIView): + authentication_classes: list = [] + permission_classes: list = [] + + @AgentKVKeyValidator.validate_api_key + def post(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): + job = _get_job(agent_kv_key, job_id) + won = AgentKVJob.mark_terminal(job.id, job.organization_id, JobStatus.CANCELLED) + if won: + # The slot is acquired at submit and released by _fail_job_response, + # the finalize callback, and the sweep. A job cancelled BEFORE + # dispatch gets no finalize callback, and the sweep's phase-1 only + # targets PENDING (not CANCELLED) -- so without this release its + # slot would leak until the 6h TTL (pre-Greptile important #4). + # release() is idempotent (zrem), so a later finalize-callback + # release on a cancel-mid-run is harmless. + AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + return Response({"status": "cancelled"}, status=200) + # Lowercased for cross-endpoint consistency (spec §7.2). + return Response({"status": job.status.lower()}, status=409) diff --git a/backend/agent_kv/execution_views_result.py b/backend/agent_kv/execution_views_result.py new file mode 100644 index 0000000000..5ec465a604 --- /dev/null +++ b/backend/agent_kv/execution_views_result.py @@ -0,0 +1,63 @@ +"""Result/outcome payload for a TERMINAL agent-kv job (spec §7.3). + +Single source of truth for "what does the result endpoint hand back for a +job that's done", shared by ``JobResultView`` (``execution_views.py``) and +``SubmitView``'s synchronous-wait branch (Task 8, lazily imported to avoid a +circular import at module load time) -- both call this only once they've +already established the job is terminal, so it is total over +``AgentKVJob.TERMINAL`` and never raises for a terminal job (spec §7.3: +"Failed jobs: {success: false, error, timing} with a user-safe error"). + +Everything that depends on the caller's own status code (409 for a +non-terminal job at ``JobResultView``, vs. a plain 200 for any terminal +status at ``SubmitView``'s wait loop) -- and the expired/blank-``result_ref`` +404 check -- lives in the caller instead; this function's only job is +building the right body once "terminal" is already a given. +""" + +from agent_kv.models import JobStatus +from agent_kv.storage import read_result + + +def result_payload(job) -> dict: + """Return the result/outcome payload for a terminal ``job``. + + - COMPLETED: ``{success, status, extractors, usage_summary}`` -- the stored + engine result namespaced per extractor (spec §7.3), not the bare blob. + - FAILED: ``{"success": False, "status": "failed", "error": }``. + - CANCELLED: ``{"success": False, "status": "cancelled"}``. + + Callers are responsible for confirming ``job.status`` is terminal (and, + for COMPLETED, that ``result_ref`` is non-blank and unexpired) before + calling this -- it never raises for a job in any of the three states + above. + """ + if job.status == JobStatus.COMPLETED: + # Wrapped at READ time, not stored wrapped: the stored blob is the + # engine's own result object, and namespacing it here keeps the cloud + # engine and every already-written result untouched (spec §7.3). + usage = job.usage_summary or {} + return { + # `success`/`status` stay at TOP level on every terminal payload -- + # completed, failed and cancelled alike -- so a client branches on + # one key rather than having to know that only the failure shapes + # carry it. They describe the JOB; each extractor keeps its own + # `success` inside its own block. + "success": True, + "status": JobStatus.COMPLETED.lower(), + "extractors": {job.extractor: read_result(job.result_ref)}, + # `total` stays the authoritative billing figure; `by_extractor` + # exists so a multi-extractor job can be attributed. With one + # extractor they are necessarily the same numbers. + "usage_summary": { + "total": usage, + "by_extractor": {job.extractor: usage}, + }, + } + if job.status == JobStatus.FAILED: + return { + "success": False, + "status": "failed", + "error": job.error or "Job failed", + } + return {"success": False, "status": "cancelled"} diff --git a/backend/agent_kv/internal_urls.py b/backend/agent_kv/internal_urls.py new file mode 100644 index 0000000000..2ebffc2b21 --- /dev/null +++ b/backend/agent_kv/internal_urls.py @@ -0,0 +1,31 @@ +from django.urls import path + +from agent_kv.internal_views import ( + FinalizeView, + StageReportView, + SweepView, + TTLCleanupView, +) + +urlpatterns = [ + path( + "jobs//stage/", + StageReportView.as_view(), + name="agent_kv_internal_stage", + ), + path( + "jobs//finalize/", + FinalizeView.as_view(), + name="agent_kv_internal_finalize", + ), + path( + "sweep/", + SweepView.as_view(), + name="agent_kv_internal_sweep", + ), + path( + "ttl-cleanup/", + TTLCleanupView.as_view(), + name="agent_kv_internal_ttl_cleanup", + ), +] diff --git a/backend/agent_kv/internal_views.py b/backend/agent_kv/internal_views.py new file mode 100644 index 0000000000..744a571bf9 --- /dev/null +++ b/backend/agent_kv/internal_views.py @@ -0,0 +1,267 @@ +"""Internal APIs for the agent-kv cloud executor (spec §5.4). + +Mounted under ``/internal/v1/agent-kv/`` and guarded ambiently by +``InternalAPIAuthMiddleware`` -- these views take empty auth/permission +classes. Most require ``org_id`` in the body of every request (they act on +one job belonging to one org); the two maintenance views, ``SweepView`` and +``TTLCleanupView``, are the exception -- they are platform-wide and take no +``org_id`` at all, since they sweep across every org in one call. +""" + +import logging + +from django.db.models import F, JSONField, Value +from django.db.models.expressions import CombinedExpression +from rest_framework.response import Response +from rest_framework.views import APIView + +from agent_kv.maintenance import run_sweep, run_ttl_cleanup +from agent_kv.models import AgentKVJob, JobStatus +from agent_kv.rate_limiter import AgentKVConcurrencyLimiter +from agent_kv.storage import delete_input, delete_result_file, write_result + +logger = logging.getLogger(__name__) + +_VALID_STAGE_STATUSES = frozenset({"running", "done"}) +_RESERVED_STAGE_ENTRY_KEYS = frozenset({"status", "seconds"}) +_SCALAR_COUNTER_TYPES = (str, int, float, bool) + + +def _sanitize_counters(counters) -> dict: + """Keep only scalar counters that can't clobber the reserved fields. + + Task-11-review ruling: ``counters`` is untrusted executor payload -- + a key of ``status``/``seconds`` must not be able to override the + endpoint's own reserved fields, and a nested dict/list value must not + be admitted into the persisted stage entry (only scalars are). + """ + if not isinstance(counters, dict): + return {} + return { + key: value + for key, value in counters.items() + if key not in _RESERVED_STAGE_ENTRY_KEYS + and isinstance(value, _SCALAR_COUNTER_TYPES) + } + + +def _stage_merge_expression(stage: str, entry: dict) -> CombinedExpression: + """Build a DB-side jsonb merge expression for one stage's entry. + + Compiles (on Postgres) to ``"stages" || %s``, the jsonb ``||`` + concatenation operator, which does a shallow top-level merge: only the + ``stage`` key in the JSON column is touched, every other key is left + exactly as the current row has it. This is what makes the merge safe + against two concurrent stage reports for two *different* stages racing + each other -- a Python-side ``dict(job.stages or {})`` read, merge, and + full-column ``.update(stages=...)`` would let whichever write lands + second silently clobber the first (task-11-review ruling). + """ + return CombinedExpression( + F("stages"), + "||", + Value({stage: entry}, output_field=JSONField()), + output_field=JSONField(), + ) + + +class StageReportView(APIView): + """Merge one stage's progress into ``job.stages`` (spec §5.4). + + This endpoint is the sole write gate for ``job.stages``: only the + defined shape (``status``, optional ``seconds``, plus sanitized flat + ``counters``) is ever persisted -- unexpected top-level body keys, and + any counter that collides with a reserved key or isn't a scalar, are + dropped rather than stored, so a later status-endpoint spread of a + stage entry can't leak arbitrary payload content (task-9 review). + """ + + authentication_classes: list = [] + permission_classes: list = [] + + def post(self, request, *args, job_id=None, **kwargs): + body = request.data + org_id = body.get("org_id") + if not org_id: + return Response({"detail": "org_id is required"}, status=400) + stage = body.get("stage") + if not stage: + return Response({"detail": "stage is required"}, status=400) + status_value = body.get("status") + if status_value not in _VALID_STAGE_STATUSES: + return Response({"detail": "status must be 'running' or 'done'"}, status=400) + + job_qs = AgentKVJob.objects.filter(id=job_id, organization_id=org_id).exclude( + status__in=list(AgentKVJob.TERMINAL) + ) + job = job_qs.first() + if job is None: + # Late report for a job that already reached a terminal state + # (completed/failed/cancelled) -- a no-op, not an error. + return Response({"ok": True, "noop": True}) + + entry = {"status": status_value} + if "seconds" in body: + entry["seconds"] = body["seconds"] + entry.update(_sanitize_counters(body.get("counters"))) + + updates = { + "stages": _stage_merge_expression(stage, entry), + "stage": stage, + } + if job.status in (JobStatus.PENDING, JobStatus.DISPATCHED): + updates["status"] = JobStatus.RUNNING + # Re-guarded at update time: job_qs still excludes TERMINAL, so if + # the job raced to terminal between the read above and this + # UPDATE's WHERE clause, this becomes a silent no-op too. + job_qs.update(**updates) + return Response({"ok": True}) + + +class FinalizeView(APIView): + """Terminalize a job: write its result (on success) then mark it done. + + Idempotent: a job already in a terminal state short-circuits before any + write -- ``write_result`` is only ever called for a job that is still + non-terminal at read time, so a duplicate/late finalize call can never + rewrite an already-written result. The concurrency slot is always + released, in a ``finally``, on every path that actually attempts to + finalize (success, duplicate no-op, or an exception raised while + finalizing) -- but never on a 400 for a malformed body, since nothing + was finalized (and therefore nothing had a slot to release yet). + + On the success path, ``write_result`` unavoidably runs *before* the + ``mark_terminal`` guard (the result has to exist to get a ``result_ref`` + to pass into it) -- so a guard-loss right after the write (a concurrent + cancel/duplicate finalize won instead) leaves a result file on disk with + no ref anywhere pointing at it. That orphan is cleaned up immediately via + ``storage.delete_result_file`` rather than left to leak past even TTL + cleanup (which only ever acts on a job's *stored* ``result_ref``). + + Also deletes the staged input the moment ``mark_terminal`` actually + wins (spec D10: "uploaded document deleted on job completion") -- the + run is over either way, whether it completed or failed, so the input is + no longer needed; the result file this same request may have just + *successfully* written (guard won) is left completely untouched. Never + runs on the duplicate/guard-lost path, since either another writer + already owns cleanup or there's nothing new to terminalize. + """ + + authentication_classes: list = [] + permission_classes: list = [] + + def post(self, request, *args, job_id=None, **kwargs): + body = request.data + org_id = body.get("org_id") + if not org_id: + return Response({"detail": "org_id is required"}, status=400) + success = body.get("success") + if not isinstance(success, bool): + # Strict bool check (not truthy/falsy): a malformed/missing + # `success` must 400 rather than silently fall through to the + # failure branch and persist a FAILED job with an empty error. + return Response({"detail": "success must be a boolean"}, status=400) + + job = AgentKVJob.objects.filter(id=job_id, organization_id=org_id).first() + finalized = False + try: + if job is not None and job.status not in AgentKVJob.TERMINAL: + if success: + result_ref = write_result( + org_id, str(job_id), body.get("result") or {} + ) + finalized = AgentKVJob.mark_terminal( + job_id, + org_id, + JobStatus.COMPLETED, + result_ref=result_ref, + usage_summary=body.get("usage_summary"), + ) + if finalized: + job.status = JobStatus.COMPLETED + else: + # Guard lost after the write above (e.g. a cancel + # raced in between the read and mark_terminal's + # guarded UPDATE): the result we just wrote has no + # ref anywhere -- unreachable forever unless cleaned + # up here. Best-effort; never blocks the response. + delete_result_file(result_ref) + else: + finalized = AgentKVJob.mark_terminal( + job_id, + org_id, + JobStatus.FAILED, + error=body.get("error") or "", + ) + if finalized: + job.status = JobStatus.FAILED + + if finalized: + # A CANCELLED job never reaches here: cancel goes + # through JobCancelView/mark_terminal directly, not + # this finalize path, and a late finalize call against + # an already-CANCELLED job loses the terminal guard + # above -- so a cancelled job's input intentionally + # rides the normal TTL sweep instead of being deleted + # here. + # Blank the ref only when the file is confirmed gone -- + # otherwise it is the only handle TTL cleanup can retry + # from, and blanking it orphans the uploaded document. + if delete_input(job): + AgentKVJob.objects.filter( + id=job_id, organization_id=org_id + ).update(input_ref="") + finally: + AgentKVConcurrencyLimiter.release(org_id, str(job_id)) + + return Response( + { + "finalized": finalized, + "webhook_url": job.webhook_url if job else "", + "status": job.status.lower() if job else "", + } + ) + + +class SweepView(APIView): + """Terminalize PENDING-never-dispatched AND stuck jobs (spec §5.4). + + Thin wrapper: the two-phase sweep logic itself -- never-dispatched + PENDING rows past ``AGENT_KV_SWEEP_GRACE_SECONDS`` and stuck + DISPATCHED/RUNNING rows past ``AGENT_KV_STUCK_JOB_GRACE_SECONDS``, each + idempotent and batch-capped -- lives in + :func:`agent_kv.maintenance.run_sweep`, shared with the + ``agent_kv_sweep`` management command (the Kubernetes CronJob mechanism + for the cloud deployment). See that function's docstring for the full + rationale. + + Platform-wide by design (no ``org_id`` in the body, unlike every other + view in this module) -- it is invoked by a periodic maintenance + mechanism (spec §5.4), not the cloud executor acting on one job/org. + """ + + authentication_classes: list = [] + permission_classes: list = [] + + def post(self, request, *args, **kwargs): + return Response(run_sweep()) + + +class TTLCleanupView(APIView): + """Delete staged files for expired jobs and blank their refs (spec §5.4). + + Thin wrapper: the cleanup logic itself -- deleting object-store paths + for expired jobs and blanking their refs, idempotent and batch-capped -- + lives in :func:`agent_kv.maintenance.run_ttl_cleanup`, shared with the + ``agent_kv_ttl_cleanup`` management command (the Kubernetes CronJob + mechanism for the cloud deployment). See that function's docstring for + the full rationale. + + Platform-wide by design, same as :class:`SweepView`. + """ + + authentication_classes: list = [] + permission_classes: list = [] + + def post(self, request, *args, **kwargs): + return Response(run_ttl_cleanup()) diff --git a/backend/agent_kv/key_validator.py b/backend/agent_kv/key_validator.py new file mode 100644 index 0000000000..2daa729cbc --- /dev/null +++ b/backend/agent_kv/key_validator.py @@ -0,0 +1,45 @@ +import logging +import uuid + +from api_v2.api_key_validator import BaseAPIKeyValidator +from api_v2.exceptions import Forbidden + +from agent_kv.models import AgentKVKey + +logger = logging.getLogger(__name__) + +# One message for every rejection reason, deliberately. A caller holding a bad +# key learns only that it is bad -- not whether it was malformed, unknown, +# inactive, or missing an organization -- so the response cannot be used to +# probe which keys exist. Named once so the three raise sites cannot drift +# apart and start leaking that distinction. +_INVALID_KEY = "Invalid api key" + + +class AgentKVKeyValidator(BaseAPIKeyValidator): + @staticmethod + def validate_and_process(self, request, func, api_key, *args, **kwargs): + try: + uuid.UUID(api_key) + except (ValueError, AttributeError): + raise Forbidden(_INVALID_KEY) + try: + key_obj = AgentKVKey.objects.get(key=api_key, is_active=True) + except AgentKVKey.DoesNotExist: + raise Forbidden(_INVALID_KEY) + if key_obj.organization_id is None: + # `organization` is nullable on the model (DefaultOrganizationMixin + # fills it from UserContext at save time), but EVERY downstream use + # of a key is org-scoped: the subscription gate, the concurrency + # limiter, the storage prefix and every job lookup. A key with no + # organization cannot scope any of them, so it is not a usable key. + # Refused once here, at the auth boundary, rather than surfacing as + # a null dereference inside whichever view happens to touch the + # organization first. + logger.error( + "agent-kv key %s has no organization; refusing the request", + key_obj.id, + ) + raise Forbidden(_INVALID_KEY) + kwargs["agent_kv_key"] = key_obj + return func(self, request, *args, **kwargs) diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py new file mode 100644 index 0000000000..a448030849 --- /dev/null +++ b/backend/agent_kv/maintenance.py @@ -0,0 +1,249 @@ +"""Agent-KV platform-wide maintenance logic (spec §5.4). + +Two independent, idempotent, batch-capped jobs -- the never-dispatched/ +stuck-job sweep and the TTL cleanup of expired staged files -- live here so +there is exactly one implementation shared by both invocation paths: + +* the internal HTTP endpoints (``agent_kv/internal_views.py::SweepView``/ + ``TTLCleanupView``, ``POST /internal/v1/agent-kv/sweep/`` and + ``.../ttl-cleanup/``), driven by the OSS/self-hosted PG-scheduler periodic + task mechanism (``workers/scheduler/agent_kv_tasks.py``); and +* the ``agent_kv_sweep``/``agent_kv_ttl_cleanup`` Django management commands + (``agent_kv/management/commands/``), driven by a Kubernetes CronJob in the + cloud deployment. + +Both callers get the identical dict shape back (``{"swept": N, "timed_out": +M}`` / ``{"cleaned": N}``), and both entrypoints are equally safe to call +more often than needed, or concurrently with each other -- everything below +is either a guarded UPDATE (``AgentKVJob.mark_terminal``) or a targeted +single-row write, never a queryset-wide one. +""" + +from datetime import timedelta + +from django.conf import settings +from django.db.models import Q +from django.utils import timezone + +from agent_kv.models import AgentKVJob, JobStatus +from agent_kv.rate_limiter import AgentKVConcurrencyLimiter +from agent_kv.storage import delete_job_files + +_MAINTENANCE_BATCH_LIMIT = 500 +# Slots in each TTL-cleanup batch held for rows whose file delete failed before. +# Caps the retry lane (so failures cannot crowd out new expirations) and +# guarantees it (so new expirations cannot crowd out retries) -- review found +# the implementation starving each side in turn when one ordering tried to do +# both. 20% leaves 400 slots for the normal case, where nothing is retrying. +_TTL_RETRY_RESERVE = 100 +_NEVER_DISPATCHED_ERROR = "Job was never dispatched" +_STUCK_JOB_ERROR = "Job timed out" + + +def run_sweep() -> dict: + """Terminalize PENDING-never-dispatched AND stuck jobs (spec §5.4). + + Two independent phases, run every call, each capped and counted + separately: + + **Phase 1 -- never dispatched.** Mirrors ``workflow_manager``'s + undispatched-execution sweep: the submit endpoint commits a PENDING row + before dispatch runs, and an abort in between (client disconnect, worker + crash, pod eviction) can leave it stranded with no owner -- ``PENDING`` + is not a terminal state, and nothing else recovers a job that was never + queued. ``dispatched_at`` is stamped as a positive fact at dispatch + time, so PENDING + older than the grace + ``dispatched_at IS NULL`` + means the dispatch never happened. + + **Phase 2 -- stuck in flight.** A job that *did* dispatch can still + never terminalize: the executor pod can be killed, its callback queue + can be lost, or the cloud engine itself can hang -- none of which + ``mark_terminal`` ever sees, so a DISPATCHED/RUNNING row can sit + forever holding a concurrency slot with no path back to terminal. + ``dispatched_at`` (stamped positively at dispatch, spec §5.3/Fix 1) older + than ``AGENT_KV_STUCK_JOB_GRACE_SECONDS`` is the only signal available -- + there is no heartbeat -- so this phase force-fails anything past that + grace, same as the workflow reaper's stuck-execution recovery. + + Platform-wide by design (no ``org_id`` parameter) -- invoked by a + periodic maintenance mechanism (spec §5.4), not by something acting on + one job/org. + + Idempotent: ``mark_terminal``'s guarded UPDATE only terminalizes a row + still in a non-terminal state, so a job already swept (or one that + legitimately dispatched/finalized/was cancelled since the candidate + query ran) is left alone by a repeat call. ``swept``/``timed_out`` each + count guard successes, not candidates, so a race against a concurrent + finalize/cancel/duplicate sweep is reflected accurately instead of + double-counted. + + Each phase is independently batch-capped at ``_MAINTENANCE_BATCH_LIMIT`` + (oldest-first by its own ordering key) so a large backlog in either + phase -- exactly what a dispatch-path or executor-fleet infra incident + produces, which is also when this sweep matters most -- can't load + unbounded into memory or hold a caller open through a long loop. + Idempotency (above) is what makes this safe to cap: whatever a call + doesn't reach is still there, unchanged, for the next tick. + """ + now = timezone.now() + + never_dispatched_cutoff = now - timedelta( + seconds=settings.AGENT_KV_SWEEP_GRACE_SECONDS + ) + never_dispatched_candidates = AgentKVJob.objects.filter( + status=JobStatus.PENDING, + created_at__lt=never_dispatched_cutoff, + dispatched_at__isnull=True, + ).order_by("created_at")[:_MAINTENANCE_BATCH_LIMIT] + + swept = 0 + for job in never_dispatched_candidates: + # Per-job isolation is structural, not a try/except here: + # mark_terminal is a guarded UPDATE that can't raise on a + # normal outcome, and release() has its own internal + # try/except (rate_limiter.py) -- so one job's failure can't + # abort the loop for the rest of the batch. + org_id = job.organization_id + won = AgentKVJob.mark_terminal( + job.id, + org_id, + JobStatus.FAILED, + error=_NEVER_DISPATCHED_ERROR, + ) + if won: + swept += 1 + # Only a job this call actually terminalized held a slot + # worth releasing here -- one a concurrent finalize/cancel + # won instead already released its own slot on that path. + AgentKVConcurrencyLimiter.release(str(org_id), str(job.id)) + + stuck_cutoff = now - timedelta(seconds=settings.AGENT_KV_STUCK_JOB_GRACE_SECONDS) + # `dispatched_at__lt` OR `dispatched_at IS NULL`, not just the former. + # A DISPATCHED/RUNNING row whose `dispatched_at` is NULL is unreachable by + # a `__lt` filter alone -- SQL `NULL < x` is never true -- so such a row + # would hang in a non-terminal state forever. dispatch.py now stamps + # `dispatched_at` for any non-terminal row precisely so this cannot + # normally happen; this arm is the backstop for the window that remains + # (the worker dying between the enqueue and that stamp), because the cost + # of missing one is a job that never terminalizes at all. Such a row falls + # back to `created_at` for the age test, which is the only timestamp it + # has. + stuck_candidates = ( + AgentKVJob.objects.filter( + status__in=[JobStatus.DISPATCHED, JobStatus.RUNNING], + ) + .filter( + Q(dispatched_at__lt=stuck_cutoff) + | Q(dispatched_at__isnull=True, created_at__lt=stuck_cutoff) + ) + .order_by("dispatched_at")[:_MAINTENANCE_BATCH_LIMIT] + ) + + timed_out = 0 + for job in stuck_candidates: + org_id = job.organization_id + won = AgentKVJob.mark_terminal( + job.id, + org_id, + JobStatus.FAILED, + error=_STUCK_JOB_ERROR, + ) + if won: + timed_out += 1 + AgentKVConcurrencyLimiter.release(str(org_id), str(job.id)) + + return {"swept": swept, "timed_out": timed_out} + + +def run_ttl_cleanup() -> dict: + """Delete staged files for expired jobs and blank their refs (spec §5.4). + + The job row itself is retained (audit trail) -- only the object-store + paths are dropped, once nothing can read them any more (the status/ + result endpoints already 404 past ``expires_at`` -- Task 9). Blanking a ref + after its file is deleted is what makes a repeat call a no-op: the filters + below only match rows still carrying a non-blank ref, so a job already + cleaned (or one that never staged an input/produced a result) drops out of + the candidate set on the next pass. + + Platform-wide by design, same as :func:`run_sweep`. + + **Two queries, not one ordering.** Retaining a ref whose delete failed is + what makes a retry possible at all, but it also means failed rows compete + with new expirations for a capped batch, and either side can starve the + other. Both directions were observed in review: + + - Oldest-expiry-first alone: 500 permanently-failing rows refill every + batch and nothing newer is ever reached. + - ``cleanup_failed_at NULLS FIRST`` (the first attempt at a fix): the + mirror image -- 500 fresh expirations per tick fill every batch and the + failures are never retried, so their files sit in storage indefinitely. + + One ordering cannot express "neither side starves the other", so the batch + is split instead: retries get up to ``_TTL_RETRY_RESERVE`` slots, fresh + rows get whatever is left. Each side is capped, so each side is guaranteed + capacity whenever it has work. + + The split also buys a cheaper plan. Each query now sorts by ONE column + ascending with a plain equality/IS NULL predicate on the index's leading + column, so the ``(cleanup_failed_at, expires_at)`` index serves both + directly. ``NULLS FIRST`` could not use it at all -- a btree index is + ``NULLS LAST`` by default -- so Postgres had to sort every matching expired + row before applying the limit, which grows with the backlog. + """ + now = timezone.now() + expired = AgentKVJob.objects.filter(expires_at__lt=now).filter( + Q(input_ref__gt="") | Q(result_ref__gt="") + ) + + # Retries first, capped at the reserve so they cannot crowd out fresh work. + # Oldest failure first, so failures rotate rather than one row absorbing + # every retry. + retries = list( + expired.filter(cleanup_failed_at__isnull=False).order_by("cleanup_failed_at")[ + :_TTL_RETRY_RESERVE + ] + ) + # Fresh rows take the remaining capacity -- the full batch when there is + # nothing to retry, which is the normal case. + fresh = list( + expired.filter(cleanup_failed_at__isnull=True).order_by("expires_at")[ + : _MAINTENANCE_BATCH_LIMIT - len(retries) + ] + ) + + cleaned = 0 + retained = 0 + for job in retries + fresh: + # Blank only the refs whose files are CONFIRMED gone. Targeted + # single-row update (not a queryset-wide `.update()`) for the same + # reason it always was: one job's refs must never be blanked off the + # back of another job's delete. + # + # A ref left set is the retry handle -- this loop used to blank both + # unconditionally, so a transient object-store failure orphaned the file + # permanently (the candidate filters above only match rows that still + # carry a non-blank ref, so a blanked row can never be reconsidered). + cleared = delete_job_files(job) + fields: dict = dict.fromkeys(cleared, "") + if len(cleared) == 2: + cleaned += 1 + # Fully cleaned rows drop out of the candidate filter anyway (both + # refs blank), so this only matters for a row that failed before and + # succeeded now -- it must not keep a stale failure marker, or it + # would consume a reserve slot it no longer needs. + fields["cleanup_failed_at"] = None + else: + retained += 1 + # Stamped on every failed attempt, not just the first: this is what + # moves the row into the retry lane, and refreshing it rotates the + # retry order among failures instead of letting the earliest-stamped + # row be retried forever. + fields["cleanup_failed_at"] = timezone.now() + AgentKVJob.objects.filter(id=job.id).update(**fields) + # `retained` is reported, not just logged: these rows keep their refs and + # are retried from the reserve lane on a later tick. A `retained` that + # stays high across ticks is the signal that something is wrong with the + # object store rather than with one job -- the split stops either side + # blocking the other, it does not make a persistent fault harmless. + return {"cleaned": cleaned, "retained": retained} diff --git a/backend/agent_kv/management/__init__.py b/backend/agent_kv/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/agent_kv/management/commands/__init__.py b/backend/agent_kv/management/commands/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/agent_kv/management/commands/agent_kv_sweep.py b/backend/agent_kv/management/commands/agent_kv_sweep.py new file mode 100644 index 0000000000..9c091b9403 --- /dev/null +++ b/backend/agent_kv/management/commands/agent_kv_sweep.py @@ -0,0 +1,24 @@ +"""CronJob entrypoint for the Agent-KV never-dispatched/stuck-job sweep. + +Thin wrapper around :func:`agent_kv.maintenance.run_sweep` -- see that +function's docstring for the two-phase sweep logic itself (spec §5.4). This +command is the cloud mechanism for driving it: a Kubernetes CronJob runs +``python manage.py agent_kv_sweep`` on a schedule. The internal +``SweepView``/``POST /internal/v1/agent-kv/sweep/`` endpoint +(``agent_kv/internal_views.py``) delegates to the same function and remains +the OSS/self-hosted mechanism, driven by the PG-scheduler periodic task +(``workers/scheduler/agent_kv_tasks.py``). +""" + +import json + +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Terminalize never-dispatched and stuck Agent-KV jobs (CronJob entrypoint)" + + def handle(self, *args, **options): + from agent_kv.maintenance import run_sweep + + self.stdout.write(json.dumps(run_sweep())) diff --git a/backend/agent_kv/management/commands/agent_kv_ttl_cleanup.py b/backend/agent_kv/management/commands/agent_kv_ttl_cleanup.py new file mode 100644 index 0000000000..c8025c40c9 --- /dev/null +++ b/backend/agent_kv/management/commands/agent_kv_ttl_cleanup.py @@ -0,0 +1,24 @@ +"""CronJob entrypoint for the Agent-KV TTL cleanup of expired staged files. + +Thin wrapper around :func:`agent_kv.maintenance.run_ttl_cleanup` -- see that +function's docstring for the cleanup logic itself (spec §5.4). This command +is the cloud mechanism for driving it: a Kubernetes CronJob runs +``python manage.py agent_kv_ttl_cleanup`` on a schedule. The internal +``TTLCleanupView``/``POST /internal/v1/agent-kv/ttl-cleanup/`` endpoint +(``agent_kv/internal_views.py``) delegates to the same function and remains +the OSS/self-hosted mechanism, driven by the PG-scheduler periodic task +(``workers/scheduler/agent_kv_tasks.py``). +""" + +import json + +from django.core.management.base import BaseCommand + + +class Command(BaseCommand): + help = "Delete staged files for expired Agent-KV jobs (CronJob entrypoint)" + + def handle(self, *args, **options): + from agent_kv.maintenance import run_ttl_cleanup + + self.stdout.write(json.dumps(run_ttl_cleanup())) diff --git a/backend/agent_kv/migrations/0001_initial.py b/backend/agent_kv/migrations/0001_initial.py new file mode 100644 index 0000000000..0fa24c9eac --- /dev/null +++ b/backend/agent_kv/migrations/0001_initial.py @@ -0,0 +1,157 @@ +# Generated by Django 4.2.30 on 2026-08-28 12:16 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("account_v2", "0007_organization_restrict_connector_creation"), + ] + + operations = [ + migrations.CreateModel( + name="AgentKVKey", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("modified_at", models.DateTimeField(auto_now=True)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("name", models.CharField(max_length=128)), + ("description", models.CharField(blank=True, default="", max_length=512)), + ("key", models.UUIDField(default=uuid.uuid4, unique=True)), + ("is_active", models.BooleanField(default=True)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="agent_kv_keys_created", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "modified_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + db_comment="Foreign key reference to the Organization model.", + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="account_v2.organization", + ), + ), + ], + options={ + "db_table": "agent_kv_key", + }, + ), + migrations.CreateModel( + name="AgentKVJob", + fields=[ + ("created_at", models.DateTimeField(auto_now_add=True)), + ("modified_at", models.DateTimeField(auto_now=True)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("task_id", models.UUIDField(blank=True, null=True)), + ( + "status", + models.CharField( + choices=[ + ("PENDING", "Pending"), + ("DISPATCHED", "Dispatched"), + ("RUNNING", "Running"), + ("COMPLETED", "Completed"), + ("FAILED", "Failed"), + ("CANCELLED", "Cancelled"), + ], + default="PENDING", + max_length=16, + ), + ), + ("stage", models.CharField(blank=True, default="", max_length=32)), + ("stages", models.JSONField(blank=True, default=dict)), + ("pages_total", models.IntegerField(blank=True, null=True)), + ("input_ref", models.CharField(blank=True, default="", max_length=512)), + ("result_ref", models.CharField(blank=True, default="", max_length=512)), + ("usage_summary", models.JSONField(blank=True, null=True)), + ("error", models.TextField(blank=True, default="")), + ("dispatched_at", models.DateTimeField(blank=True, null=True)), + ("completed_at", models.DateTimeField(blank=True, null=True)), + ("expires_at", models.DateTimeField(blank=True, null=True)), + ("tags", models.JSONField(blank=True, default=list)), + ("custom_data", models.JSONField(blank=True, null=True)), + ("webhook_url", models.URLField(blank=True, default="", max_length=1024)), + ( + "api_key", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="jobs", + to="agent_kv.agentkvkey", + ), + ), + ( + "organization", + models.ForeignKey( + blank=True, + db_comment="Foreign key reference to the Organization model.", + editable=False, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="account_v2.organization", + ), + ), + ], + options={ + "db_table": "agent_kv_job", + }, + ), + migrations.AddConstraint( + model_name="agentkvkey", + constraint=models.UniqueConstraint( + fields=("name", "organization"), name="unique_agent_kv_key_name_per_org" + ), + ), + migrations.AddIndex( + model_name="agentkvjob", + index=models.Index( + fields=["organization", "status"], name="agent_kv_jo_organiz_9b6b97_idx" + ), + ), + migrations.AddIndex( + model_name="agentkvjob", + index=models.Index( + fields=["expires_at"], name="agent_kv_jo_expires_5966b4_idx" + ), + ), + ] diff --git a/backend/agent_kv/migrations/0002_agentkvjob_extractor.py b/backend/agent_kv/migrations/0002_agentkvjob_extractor.py new file mode 100644 index 0000000000..8889c7cd36 --- /dev/null +++ b/backend/agent_kv/migrations/0002_agentkvjob_extractor.py @@ -0,0 +1,21 @@ +"""Record which extractor each job ran. + +Defaulting to "kv" is the historical truth, not a guess: before this column +existed the API accepted exactly one extractor and it was always `kv`. +""" + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("agent_kv", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="agentkvjob", + name="extractor", + field=models.CharField(default="kv", max_length=32), + ), + ] diff --git a/backend/agent_kv/migrations/0003_agentkvjob_cleanup_failed_at.py b/backend/agent_kv/migrations/0003_agentkvjob_cleanup_failed_at.py new file mode 100644 index 0000000000..d67b837ffb --- /dev/null +++ b/backend/agent_kv/migrations/0003_agentkvjob_cleanup_failed_at.py @@ -0,0 +1,41 @@ +"""Record when TTL cleanup last failed on a job, and index the ordering. + +Review follow-up on UN-4044. ``run_ttl_cleanup`` retains a ref whose file +delete failed, because the ref is the only handle a retry can work from. Under +the previous plain ``expires_at`` ordering those rows refilled the capped batch +on every tick, so a persistent object-store fault on 500 rows stalled cleanup +outright and every later expired job kept its files past TTL. + +Ordering is now ``(cleanup_failed_at NULLS FIRST, expires_at)``: a job that has +never failed is always processed ahead of one that has, so failures cannot +block the backlog, and the stamp is refreshed on each failed attempt so +failures rotate rather than one row absorbing every retry. + +A plain (non-CONCURRENT) AddIndex is correct here, unlike the concurrent builds +used elsewhere in this codebase: ``agent_kv_job`` is created by this app's own +``0001_initial`` and has never been deployed, so the table is empty when this +runs and the ACCESS EXCLUSIVE lock has nothing to block. +""" + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("agent_kv", "0002_agentkvjob_extractor"), + ] + + operations = [ + migrations.AddField( + model_name="agentkvjob", + name="cleanup_failed_at", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddIndex( + model_name="agentkvjob", + index=models.Index( + fields=["cleanup_failed_at", "expires_at"], + name="agent_kv_jo_cleanup_f38edc_idx", + ), + ), + ] diff --git a/backend/agent_kv/migrations/__init__.py b/backend/agent_kv/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/agent_kv/models.py b/backend/agent_kv/models.py new file mode 100644 index 0000000000..fa28b4d68e --- /dev/null +++ b/backend/agent_kv/models.py @@ -0,0 +1,142 @@ +import uuid + +from account_v2.models import User +from django.db import models +from django.utils import timezone +from utils.models.base_model import BaseModel +from utils.models.organization_mixin import DefaultOrganizationMixin + +from agent_kv.constants import V1_EXTRACTOR_NAME + + +class AgentKVKey(DefaultOrganizationMixin, BaseModel): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=128) + description = models.CharField(max_length=512, blank=True, default="") + key = models.UUIDField(default=uuid.uuid4, unique=True) + is_active = models.BooleanField(default=True) + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name="agent_kv_keys_created", + ) + modified_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name="+", + ) + + class Meta: + db_table = "agent_kv_key" + constraints = [ + models.UniqueConstraint( + fields=["name", "organization"], + name="unique_agent_kv_key_name_per_org", + ), + ] + + def __str__(self): + return f"{self.name} ({self.organization})" + + +class JobStatus(models.TextChoices): + PENDING = "PENDING" + DISPATCHED = "DISPATCHED" + RUNNING = "RUNNING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + CANCELLED = "CANCELLED" + + +class AgentKVJob(DefaultOrganizationMixin, BaseModel): + TERMINAL = frozenset({JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED}) + + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + api_key = models.ForeignKey( + AgentKVKey, + on_delete=models.SET_NULL, + null=True, + related_name="jobs", + ) + task_id = models.UUIDField(null=True, blank=True) + # Which extractor this job ran. v1 dispatches exactly one per job, but + # WHICH one is now a choice, and status/result key their payloads by it -- + # without this column a table job's output would be filed under `kv`. + extractor = models.CharField(max_length=32, default=V1_EXTRACTOR_NAME) + status = models.CharField( + max_length=16, + choices=JobStatus.choices, + default=JobStatus.PENDING, + ) + stage = models.CharField(max_length=32, blank=True, default="") + stages = models.JSONField(default=dict, blank=True) + pages_total = models.IntegerField(null=True, blank=True) + input_ref = models.CharField(max_length=512, blank=True, default="") + result_ref = models.CharField(max_length=512, blank=True, default="") + usage_summary = models.JSONField(null=True, blank=True) + error = models.TextField(blank=True, default="") + dispatched_at = models.DateTimeField(null=True, blank=True) + completed_at = models.DateTimeField(null=True, blank=True) + expires_at = models.DateTimeField(null=True, blank=True) + # When TTL cleanup last failed to delete one of this job's files. NULL means + # "never attempted, or last attempt succeeded", and it is what separates + # the two lanes run_ttl_cleanup processes: NOT NULL rows are retries, which + # get a reserved slice of each batch, and NULL rows are new expirations, + # which get the rest. Capping both is what stops either side starving the + # other. See run_ttl_cleanup. + cleanup_failed_at = models.DateTimeField(null=True, blank=True) + tags = models.JSONField(default=list, blank=True) + custom_data = models.JSONField(null=True, blank=True) + webhook_url = models.URLField(max_length=1024, blank=True, default="") + + class Meta: + db_table = "agent_kv_job" + indexes = [ + models.Index(fields=["organization", "status"]), + models.Index(fields=["expires_at"]), + # Serves BOTH of run_ttl_cleanup's lanes, each of which sorts by a + # single column ascending behind a predicate on this index's + # leading column: + # retries: WHERE cleanup_failed_at IS NOT NULL ORDER BY cleanup_failed_at + # fresh: WHERE cleanup_failed_at IS NULL ORDER BY expires_at + # Deliberately a plain ascending index. The first version of that + # query asked for `cleanup_failed_at ASC NULLS FIRST`, which a + # btree index cannot serve (btree is NULLS LAST ascending), so + # Postgres sorted every matching expired row before applying the + # 500-row limit -- work that grew with the backlog. Splitting the + # query removed the NULLS FIRST rather than adding a second index + # with a non-default null order. + models.Index(fields=["cleanup_failed_at", "expires_at"]), + ] + + @classmethod + def mark_terminal( + cls, + job_id, + organization_id, + new_status, + *, + error="", + result_ref="", + usage_summary=None, + ) -> bool: + """The ONLY way to reach a terminal state (spec §5.4 write guard). + + Guarded UPDATE: at-least-once callbacks, cancel, and the sweep can all + race; whoever lands first wins and everyone else no-ops. + """ + fields = {"status": new_status, "completed_at": timezone.now()} + if error: + fields["error"] = error + if result_ref: + fields["result_ref"] = result_ref + if usage_summary is not None: + fields["usage_summary"] = usage_summary + updated = ( + cls.objects.filter(id=job_id, organization_id=organization_id) + .exclude(status__in=list(cls.TERMINAL)) + .update(**fields) + ) + return updated == 1 diff --git a/backend/agent_kv/permissions.py b/backend/agent_kv/permissions.py new file mode 100644 index 0000000000..d9ce287517 --- /dev/null +++ b/backend/agent_kv/permissions.py @@ -0,0 +1,8 @@ +from platform_api.permissions import IsOrganizationAdmin as PlatformIsOrganizationAdmin + + +class IsOrganizationAdmin(PlatformIsOrganizationAdmin): + message = "Only organization admins can manage Agent-KV API keys." + + +__all__ = ["IsOrganizationAdmin"] diff --git a/backend/agent_kv/rate_limiter.py b/backend/agent_kv/rate_limiter.py new file mode 100644 index 0000000000..85796ef5d2 --- /dev/null +++ b/backend/agent_kv/rate_limiter.py @@ -0,0 +1,109 @@ +import logging +import time + +from django.conf import settings +from django_redis import get_redis_connection + +logger = logging.getLogger(__name__) + + +def _limiter_failure_allows_request() -> bool: + """Whether a request proceeds when the limiter backend is unreachable. + + Fails CLOSED unless `AGENT_KV_LIMITER_FAIL_OPEN` is explicitly set. Both + limiters previously returned True on any Redis exception, which is the one + choice that cannot be observed from the outside: a Sentinel failover + silently removed the concurrency ceiling AND the per-key rate ceiling at the + same time, and the API went on accepting billable LLM work as if both still + held. Closed is the safer default for a paid, concurrency-capped API -- a + 429 is recoverable by the caller, an unbounded fan-out is not. + + The waiver remains available because availability-over-accounting is a + legitimate operational choice, but it now has to be made deliberately, in + config, where it is visible -- rather than being the implicit behaviour of + an `except` block. + """ + return bool(getattr(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False)) + + +_SLOT_TTL_SECONDS = 6 * 3600 + + +def _redis(): + # Same handle acquisition as api_v2.rate_limiter (`redis_cache = + # get_redis_connection("default")`); api_v2 has no reusable helper to + # call into, so the construction line is copied here rather than + # modifying api_v2. + return get_redis_connection("default") + + +class AgentKVConcurrencyLimiter: + @staticmethod + def _key(organization_id: str) -> str: + return f"agent_kv:inflight:{organization_id}" + + # Trim stale slots, count, and claim a slot in ONE atomic server-side + # step. A client-side ``ZCARD`` followed by ``ZADD`` is a check-then-act + # race: N simultaneous submits all observe ``count < limit`` and all get + # accepted (caught live in the Task 13b run: 6 concurrent submits against + # a limit of 5 produced six 202s). Redis runs a script atomically, so at + # most ``limit`` members can ever be added. + _ACQUIRE_SCRIPT = """ +local key = KEYS[1] +redis.call('ZREMRANGEBYSCORE', key, 0, ARGV[3]) +if redis.call('ZCARD', key) >= tonumber(ARGV[4]) then + return 0 +end +redis.call('ZADD', key, ARGV[2], ARGV[1]) +redis.call('EXPIRE', key, ARGV[5]) +return 1 +""" + + @classmethod + def check_and_acquire(cls, organization_id: str, job_id: str) -> bool: + try: + r = _redis() + now = time.time() + key = cls._key(organization_id) + acquired = r.eval( + cls._ACQUIRE_SCRIPT, + 1, + key, + job_id, + now, + now - _SLOT_TTL_SECONDS, + settings.AGENT_KV_CONCURRENT_LIMIT, + _SLOT_TTL_SECONDS, + ) + return bool(int(acquired)) + except Exception: + # Fail CLOSED by default. Both this ceiling and the per-key rate + # ceiling used to `return True` on any Redis error, so a single + # Sentinel failover or pool exhaustion removed BOTH at once while + # the API kept returning 202s for billable LLM work -- with nothing + # but a per-request `logger.warning` to show for it. + # + # `logger.exception` (not warning): the limiter being unavailable is + # an error, and the old level is what let this sit unnoticed. + logger.exception("agent-kv concurrency limiter unavailable") + return _limiter_failure_allows_request() + + @classmethod + def release(cls, organization_id: str, job_id: str) -> None: + try: + _redis().zrem(cls._key(organization_id), job_id) + except Exception: + logger.warning("agent-kv slot release failed", exc_info=True) + + +def check_key_rate(key_id: str) -> bool: + try: + r = _redis() + window = int(time.time() // 60) + key = f"agent_kv:rate:{key_id}:{window}" + count = r.incr(key) + r.expire(key, 120) + return count <= settings.AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE + except Exception: + logger.exception("agent-kv key rate limiter unavailable") + return _limiter_failure_allows_request() diff --git a/backend/agent_kv/serializers.py b/backend/agent_kv/serializers.py new file mode 100644 index 0000000000..081aeabb6b --- /dev/null +++ b/backend/agent_kv/serializers.py @@ -0,0 +1,15 @@ +from rest_framework import serializers + +from agent_kv.models import AgentKVKey + + +class AgentKVKeySerializer(serializers.ModelSerializer): + class Meta: + model = AgentKVKey + fields = ["id", "name", "description", "key", "is_active", "created_at"] + + +class AgentKVKeyWriteSerializer(serializers.ModelSerializer): + class Meta: + model = AgentKVKey + fields = ["name", "description", "is_active"] diff --git a/backend/agent_kv/storage.py b/backend/agent_kv/storage.py new file mode 100644 index 0000000000..b19adce545 --- /dev/null +++ b/backend/agent_kv/storage.py @@ -0,0 +1,165 @@ +"""Object-store staging/results for agent-kv (spec §5.4, §6.4). + +Paths are the contract: {AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}/input{ext} +and .../result.json. No document bytes ever ride the broker. + +The prefix is bucket-rooted (default ``unstract/agent_kv``) like +``WORKFLOW_EXECUTION_DIR_PREFIX``/``API_EXECUTION_DIR_PREFIX``: s3fs/gcsfs read +the first path segment as the *bucket*, so a bucket-less root (the old +``org/{org_id}/...``) makes every write fail with ``NoSuchBucket``. The cloud +executor keys its OCR cache under the same root +(``{prefix}/{org_id}/cache/...``), so both sides must be configured alike. +""" + +import logging +import os +import uuid + +from django.conf import settings + +from unstract.filesystem import FileStorageType, FileSystem + +logger = logging.getLogger(__name__) + + +def _fs(): + return FileSystem(FileStorageType.AGENT_KV).get_file_storage() + + +def _base(org_id: str, job_id: str) -> str: + return f"{settings.AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}" + + +def stage_input(org_id: str, job_id: str, uploaded_file) -> str: + ext = os.path.splitext(uploaded_file.name or "")[1].lower() or ".bin" + ref = f"{_base(org_id, job_id)}/input{ext}" + data = b"".join(uploaded_file.chunks()) + _fs().write(path=ref, mode="wb", data=data) + return ref + + +def write_result( + org_id: str, job_id: str, result: dict, *, nonce: str | None = None +) -> str: + """Write a job result and return its object-store ref. + + The path is UNIQUE per call (``.../result-.json``, nonce defaulting + to a fresh uuid4 hex) rather than a deterministic ``.../result.json``. + ``FinalizeView`` writes the result BEFORE it attempts the terminal-state + guard; on a concurrent duplicate-SUCCESS finalize, the guard loser must + clean up ONLY the orphan it just wrote -- with a shared deterministic path + it would instead delete the exact file the winning row's ``result_ref`` + points at, 500-ing the completed job's result endpoint / losing data + (pre-Greptile critical #3). A unique ref per attempt makes the loser's + ``delete_result_file`` target its own file and nothing else. + + Backward-tolerant: ``read_result``/``delete_job_files``/TTL cleanup all use + the ref STORED on the job row, so any previously written ``result.json`` + ref remains readable and removable unchanged. + """ + ref = f"{_base(org_id, job_id)}/result-{nonce or uuid.uuid4().hex}.json" + _fs().json_dump(path=ref, data=result) + return ref + + +def read_result(result_ref: str) -> dict: + return _fs().json_load(path=result_ref) + + +def delete_job_files(job) -> list[str]: + """Delete the job's staged input and result, returning the ref FIELD names + that are now confirmed to point at nothing. + + The return value exists so a caller can blank only the refs whose files are + actually gone. Blanking unconditionally -- what both callers used to do -- + loses the only handle to a file whose delete FAILED: TTL cleanup selects + candidates by `input_ref > "" OR result_ref > ""`, so a blanked row drops + out of the candidate set permanently and the object sits in the bucket + forever, unreferenced and unbillable-to-anyone (Greptile review #3). + + A ref is reported clear when it was already empty (nothing was ever staged) + or when the delete succeeded. ``FileNotFoundError`` counts as success: the + goal is "the file is not there", and a ref whose object is already gone has + nothing left to retry. Any other exception leaves the ref OUT of the + returned list, so the caller keeps it and the next pass retries. + """ + fh = _fs() + cleared: list[str] = [] + for field in ("input_ref", "result_ref"): + ref = getattr(job, field, "") or "" + if not ref: + cleared.append(field) + continue + try: + fh.rm(path=ref) + except FileNotFoundError: + cleared.append(field) + except Exception: + # Left out of `cleared` deliberately -- the ref is the retry handle. + logger.warning( + "agent-kv cleanup: could not remove %s (ref kept for retry)", + ref, + exc_info=True, + ) + else: + cleared.append(field) + return cleared + + +def delete_result_file(result_ref: str) -> None: + """Best-effort delete of a result file that was just written but never + got a ref persisted onto the job row. + + ``FinalizeView`` writes the result *before* attempting the terminal- + state guard (spec §5.4); if the guard loses the race -- a concurrent + cancel or a duplicate finalize won instead -- the file this call just + wrote is orphaned: nothing anywhere points at it, so it would otherwise + sit in object storage forever, past even TTL cleanup (which only acts on + a job's *stored* ``result_ref``). Tolerant of the file being missing + already, mirroring ``delete_input``/``delete_job_files``. + """ + if not result_ref: + return + try: + _fs().rm(path=result_ref) + except Exception: + logger.warning("agent-kv cleanup: could not remove orphaned %s", result_ref) + + +def delete_input(job) -> bool: + """Delete only the staged input file (spec D10: "uploaded document + deleted on job completion"), returning whether ``input_ref`` is now + confirmed to point at nothing. + + Deliberately narrower than ``delete_job_files``: it never touches + ``result_ref``/the result file. ``FinalizeView`` calls this right after + a job is fully terminalized, in the same request that may have just + written the result -- that file must be left completely alone. + + Same confirmed-clear contract as ``delete_job_files``, and for the same + reason: this swallowed every exception while its caller blanked + ``input_ref`` unconditionally, so a transient object-store error orphaned + the customer's uploaded document permanently -- TTL cleanup only selects + rows that still carry a non-blank ref, so a blanked row can never be + reconsidered. ``delete_job_files`` and its two callers were fixed for this + in the Greptile round; THIS third site was missed, because the fix was + applied by following the one function's callers rather than by grepping for + every site that blanks a ref. + + True when there was nothing to delete or the file is gone (including + already-missing); False when it is still there and the ref must be kept. + """ + if not job.input_ref: + return True + try: + _fs().rm(path=job.input_ref) + except FileNotFoundError: + return True + except Exception: + logger.warning( + "agent-kv cleanup: could not remove %s (ref kept for retry)", + job.input_ref, + exc_info=True, + ) + return False + return True diff --git a/backend/agent_kv/tests/__init__.py b/backend/agent_kv/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/agent_kv/tests/_factories.py b/backend/agent_kv/tests/_factories.py new file mode 100644 index 0000000000..2596355e8f --- /dev/null +++ b/backend/agent_kv/tests/_factories.py @@ -0,0 +1,26 @@ +"""Shared test object factories for the Agent-KV suites.""" + +from account_v2.models import Organization + +from agent_kv.models import AgentKVKey + + +def kv_key(*, slug: str = "acme-slug", org_pk: int = 1, **overrides) -> AgentKVKey: + """A USABLE AgentKVKey — one that carries an organization. + + `AgentKVKey.organization` is nullable on the model, but a key without one is + not usable: every downstream consumer is org-scoped (the subscription gate, + the concurrency limiter, the storage prefix, every job lookup), so + `AgentKVKeyValidator` refuses it. Tests that build a bare + `AgentKVKey(name=...)` are therefore constructing a key the auth layer would + reject, and any behaviour they assert past that point is unreachable in + production. + + `org_pk` and `slug` are deliberately DIFFERENT kinds of value: + `Subscription.organization_id` is a CharField holding the slug, while the + key's own `organization_id` is the FK primary key. Keeping them distinct is + what lets a test catch code that reaches for the wrong one. + """ + key = AgentKVKey(name="k", is_active=True, **overrides) + key.organization = Organization(id=org_pk, organization_id=slug) + return key diff --git a/backend/agent_kv/tests/fixtures/two_page.pdf b/backend/agent_kv/tests/fixtures/two_page.pdf new file mode 100644 index 0000000000000000000000000000000000000000..92ee5755ea654f06f7fc0997be78496215d5dea3 GIT binary patch literal 551 zcmcJM!Ab)$5Qgvb6mwb7gH5t)fl*}ebnhQ9odV0fGu_&HylD1~DKB9*16vz RUNNING on +# the executor's first stage report, which can land before this post-enqueue +# bookkeeping. The PENDING-guarded UPDATE then matches 0 rows and +# `dispatched_at` stays NULL -- and a non-terminal row with a NULL +# `dispatched_at` is invisible to BOTH sweep phases (phase 1 requires PENDING; +# phase 2's `dispatched_at__lt` can never match a NULL), so the job reports +# `running` forever and `GET result` 409s for the life of the row. +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_dispatch_stamps_dispatched_at_when_the_row_already_moved_to_running( + m_objects, m_disp, m_key +): + m_objects.filter.return_value.update.return_value = 0 # PENDING guard missed + job = _job() + + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + # Second write is narrowed to a row that still has no dispatch time, and + # excludes terminal rows. + assert m_objects.filter.call_args_list[1].kwargs == { + "id": job.id, + "dispatched_at__isnull": True, + } + fallback = m_objects.filter.return_value.exclude.return_value + update_kwargs = fallback.update.call_args.kwargs + assert update_kwargs["dispatched_at"] == job.dispatched_at + assert update_kwargs["task_id"] == job.task_id + # Crucially does NOT write `status`: the row genuinely was dispatched, but + # moving RUNNING back to DISPATCHED would discard the executor's progress. + assert "status" not in update_kwargs + + +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_dispatch_does_not_attempt_the_fallback_when_the_pending_guard_won( + m_objects, m_disp, m_key +): + m_objects.filter.return_value.update.return_value = 1 # normal path + job = _job() + + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + assert len(m_objects.filter.call_args_list) == 1 + assert not m_objects.filter.return_value.exclude.called + + +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +def test_enqueue_failure_raises_dispatch_error(m_disp, m_key): + m_disp.return_value.dispatch_with_callback.side_effect = RuntimeError("broker down") + job = _job() + with pytest.raises(dispatch.DispatchError): + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + +@mock.patch.object(dispatch, "_dispatcher") +def test_dispatch_job_uses_platform_api_key_lookup(m_disp): + from platform_settings_v2.platform_auth_service import ( + PlatformAuthenticationService, + ) + + with mock.patch.object( + PlatformAuthenticationService, + "get_active_platform_key", + return_value=mock.Mock(key="the-real-key"), + ): + job = _job() + with mock.patch.object(AgentKVJob, "objects"): + dispatch.dispatch_job( + job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={} + ) + ctx = m_disp.return_value.dispatch_with_callback.call_args.args[0] + assert ctx.executor_params["platform_api_key"] == "the-real-key" + # The lookup takes the org's public slug, never the row PK (13b F6). + PlatformAuthenticationService.get_active_platform_key.assert_called_once_with( + "org_slug_1" + ) + assert ctx.organization_id == "7" + + +def test_platform_api_key_raises_dispatch_error_when_absent(): + from platform_settings_v2.platform_auth_service import ( + PlatformAuthenticationService, + ) + + with mock.patch.object( + PlatformAuthenticationService, + "get_active_platform_key", + return_value=None, + ): + job = _job() + with pytest.raises(dispatch.DispatchError): + dispatch._platform_api_key(job) + + +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(dispatch, "_platform_api_key") +def test_raw_exception_from_platform_key_lookup_is_wrapped_as_dispatch_error( + m_key, m_disp +): + """Regression: platform-key lookup and context construction must live + inside dispatch_job's try — a raw (non-DispatchError) exception there + (e.g. a transient DB error) must not escape uncaught. + """ + m_key.side_effect = RuntimeError("platform db down") + + job = _job() + with pytest.raises(dispatch.DispatchError): + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + # Never got far enough to enqueue. + assert not m_disp.return_value.dispatch_with_callback.called + + +def test_dispatcher_factory_call_matches_the_live_signature(): + """Guard the OSS seam that a platform change can silently move. + + UN-4046 removed `get_executor_dispatcher(celery_app=...)`; our call site + kept passing it, so every submit raised TypeError inside dispatch_job and + failed the job. Nothing caught it -- the mismatch is invisible to a mocked + dispatcher and only appears when a real request is made. + + Binding our actual call against the real signature fails loudly the next + time that function's parameters change. + """ + from unittest import mock # noqa: PLC0415 + + from agent_kv import dispatch as d # noqa: PLC0415 + + # autospec=True makes the stub enforce the REAL function's signature, so + # this asserts our call site against it rather than against a permissive + # Mock. Re-adding `celery_app=` here raises TypeError, exactly as production + # did. + with mock.patch( + "pg_queue.executor_rpc.get_executor_dispatcher", autospec=True + ) as m_factory: + d._dispatcher() + m_factory.assert_called_once_with() + + +# Post-enqueue bookkeeping must never fail a dispatch that already succeeded. +# `SubmitView` turns a DispatchError into a FAILED job, so a DB hiccup here +# would terminalize a job whose task is on the queue -- the executor then runs, +# calls back, and finds a terminal row it cannot write to, while the caller was +# told nothing was billed for work that did run. The review round added a +# SECOND update after the enqueue, which widened this window. +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_bookkeeping_failure_does_not_fail_an_already_queued_dispatch( + m_objects, m_disp, m_key +): + m_objects.filter.side_effect = OSError("db gone") + job = _job() + + # Must not raise: raising is what would mark the live job FAILED. + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + # And the enqueue did happen, so the task is genuinely on the queue. + assert m_disp.return_value.dispatch_with_callback.called + + +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_bookkeeping_failure_is_logged_at_error_level(m_objects, m_disp, m_key, caplog): + """Swallowing it silently would trade one bad failure mode for another: + the sweep reconciles the row, but nothing would say why it had to. + """ + m_objects.filter.side_effect = OSError("db gone") + with caplog.at_level("ERROR", logger=dispatch.logger.name): + dispatch.dispatch_job( + _job(), extractor=TABLE_EXTRACTOR_NAME, schema={}, options={} + ) + assert any(r.levelname == "ERROR" and r.exc_info for r in caplog.records) diff --git a/backend/agent_kv/tests/test_internal_views.py b/backend/agent_kv/tests/test_internal_views.py new file mode 100644 index 0000000000..c0c4fbf9ae --- /dev/null +++ b/backend/agent_kv/tests/test_internal_views.py @@ -0,0 +1,686 @@ +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from rest_framework.test import APIRequestFactory # noqa: E402 + +from agent_kv import internal_views as iv # noqa: E402 +from agent_kv import storage # noqa: E402 +from agent_kv.models import AgentKVJob, JobStatus # noqa: E402 + + +def _post(path, body): + return APIRequestFactory().post(path, body, format="json") + + +def _merge_payload(update_call): + """Unwrap a ``stages=`` jsonb-merge expression's {stage: entry} payload. + + Also pins the expression's shape: a ``||`` (jsonb concatenation) + ``CombinedExpression`` over the ``stages`` column, not a plain dict -- + the whole point of the DB-side merge is that ``.update()`` never + receives a Python-computed full-column dict. + """ + expr = update_call.kwargs["stages"] + assert not isinstance(expr, dict), "stages must be a jsonb-merge expression" + assert expr.connector == "||" + assert expr.lhs.name == "stages" + return expr.rhs.value + + +# --------------------------------------------------------------------------- +# _stage_merge_expression / _sanitize_counters (expression-builder unit tests) +# --------------------------------------------------------------------------- + + +# (u1) the expression builder produces a single-entry {stage: entry} jsonb +# merge payload over the `stages` column via the `||` connector. +def test_stage_merge_expression_builds_single_entry_jsonb_merge(): + expr = iv._stage_merge_expression("extraction", {"status": "running"}) + + assert expr.connector == "||" + assert expr.lhs.name == "stages" + assert expr.rhs.value == {"extraction": {"status": "running"}} + + +# (u2) counters colliding with reserved keys are dropped. +def test_sanitize_counters_drops_reserved_keys(): + assert iv._sanitize_counters({"status": "done", "seconds": 999, "pages": 3}) == { + "pages": 3 + } + + +# (u3) non-scalar counter values (dict/list) are dropped; scalars pass through. +def test_sanitize_counters_drops_non_scalar_values(): + assert iv._sanitize_counters( + {"pages": 3, "nested": {"a": 1}, "arr": [1, 2], "ok": "yes", "flag": True} + ) == {"pages": 3, "ok": "yes", "flag": True} + + +# (u4) a non-dict counters payload (e.g. a string or list) sanitizes to {}. +def test_sanitize_counters_non_dict_input_is_empty(): + assert iv._sanitize_counters("not-a-dict") == {} + assert iv._sanitize_counters(None) == {} + + +# --------------------------------------------------------------------------- +# StageReportView +# --------------------------------------------------------------------------- + + +# (1) org_id missing from body -> 400 (ambient-auth views require it in body). +def test_stage_report_missing_org_id_is_400(): + resp = iv.StageReportView.as_view()( + _post("/x", {"stage": "extraction", "status": "running"}), + job_id=uuid.uuid4(), + ) + assert resp.status_code == 400 + + +# (1b) missing `stage` -> 400. +def test_stage_report_missing_stage_is_400(): + resp = iv.StageReportView.as_view()( + _post("/x", {"org_id": "org1", "status": "running"}), + job_id=uuid.uuid4(), + ) + assert resp.status_code == 400 + + +# (1c) missing/invalid `status` -> 400 (must be exactly "running" or "done"). +def test_stage_report_invalid_status_is_400(): + resp = iv.StageReportView.as_view()( + _post("/x", {"org_id": "org1", "stage": "extraction", "status": "bogus"}), + job_id=uuid.uuid4(), + ) + assert resp.status_code == 400 + + resp_missing = iv.StageReportView.as_view()( + _post("/x", {"org_id": "org1", "stage": "extraction"}), + job_id=uuid.uuid4(), + ) + assert resp_missing.status_code == 400 + + +# (2) first stage report on a PENDING job flips it to RUNNING exactly once, +# and merges the stage entry via a DB-side jsonb `||` expression (not a +# plain dict) targeting only the reported stage's key. +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_flips_pending_to_running_once(m_jobs): + job = AgentKVJob(status=JobStatus.PENDING, stages={}) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "extraction", + "status": "running", + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + qs.update.assert_called_once() + call = qs.update.call_args + assert _merge_payload(call) == {"extraction": {"status": "running"}} + assert call.kwargs["stage"] == "extraction" + assert call.kwargs["status"] == JobStatus.RUNNING + + +# (2b) a stage report on an already-RUNNING job does NOT re-flip status -- +# the "once" half of "flips RUNNING once". +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_on_running_job_does_not_touch_status(m_jobs): + job = AgentKVJob( + status=JobStatus.RUNNING, stages={"extraction": {"status": "running"}} + ) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + {"org_id": "org1", "stage": "extraction", "status": "done", "seconds": 1.5}, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + call = qs.update.call_args + assert "status" not in call.kwargs + assert _merge_payload(call) == {"extraction": {"status": "done", "seconds": 1.5}} + + +# (3) stage report on a CANCELLED (terminal) job -- the update queryset +# excludes TERMINAL, so .first() returns None -- is a 200 no-op, never +# reaching .update(). +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_on_cancelled_job_is_200_noop(m_jobs): + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = None + + resp = iv.StageReportView.as_view()( + _post("/x", {"org_id": "org1", "stage": "extraction", "status": "running"}), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + assert not qs.update.called + + +# (4) duplicate stage report (same stage name posted twice) overwrites the +# entry rather than appending. With the DB-side jsonb `||` merge this falls +# out structurally: the merge payload always carries exactly one key (the +# reported stage), so the jsonb `||` operator replaces that key's value on +# the second call instead of stacking a second version alongside it. +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_duplicate_overwrites_not_appends(m_jobs): + job = AgentKVJob( + status=JobStatus.RUNNING, + stages={"extraction": {"status": "running"}}, + ) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "extraction", + "status": "done", + "seconds": 2.0, + "counters": {"pages": 3}, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + call = qs.update.call_args + payload = _merge_payload(call) + assert list(payload.keys()) == ["extraction"] + assert payload == {"extraction": {"status": "done", "seconds": 2.0, "pages": 3}} + assert call.kwargs["stage"] == "extraction" + + +# (5) the stage-report endpoint is the write gate for job.stages -- only the +# defined shape (status, optional seconds, sanitized flat counters) is ever +# persisted; unexpected top-level body keys (e.g. a stray "evil") must not +# leak into the stored entry. +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_ignores_unexpected_top_level_keys(m_jobs): + job = AgentKVJob(status=JobStatus.RUNNING, stages={}) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "extraction", + "status": "running", + "evil": "payload", + "another_bogus_key": 123, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + stored_entry = _merge_payload(qs.update.call_args)["extraction"] + assert stored_entry == {"status": "running"} + + +# (5b) a counter named "status" or "seconds" cannot clobber the endpoint's +# own reserved fields. +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_counters_cannot_override_reserved_keys(m_jobs): + job = AgentKVJob(status=JobStatus.RUNNING, stages={}) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "extraction", + "status": "running", + "counters": {"status": "done", "seconds": 999, "pages": 3}, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + stored_entry = _merge_payload(qs.update.call_args)["extraction"] + assert stored_entry == {"status": "running", "pages": 3} + + +# (5c) a nested-dict (or list) counter value is dropped rather than stored. +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_counters_drops_nested_values(m_jobs): + job = AgentKVJob(status=JobStatus.RUNNING, stages={}) + qs = m_jobs.filter.return_value.exclude.return_value + qs.first.return_value = job + + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "extraction", + "status": "running", + "counters": {"pages": 3, "nested": {"a": 1}, "arr": [1, 2]}, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + stored_entry = _merge_payload(qs.update.call_args)["extraction"] + assert stored_entry == {"status": "running", "pages": 3} + + +# --------------------------------------------------------------------------- +# FinalizeView +# --------------------------------------------------------------------------- + + +# (6) org_id missing from body -> 400. +def test_finalize_missing_org_id_is_400(): + resp = iv.FinalizeView.as_view()(_post("/x", {"success": True}), job_id=uuid.uuid4()) + assert resp.status_code == 400 + + +# (6b) missing `success` -> 400, and no slot is released (nothing was +# finalized). +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +def test_finalize_missing_success_is_400(m_release): + resp = iv.FinalizeView.as_view()(_post("/x", {"org_id": "org1"}), job_id=uuid.uuid4()) + assert resp.status_code == 400 + assert not m_release.called + + +# (6c) a non-bool `success` (e.g. a truthy string) -> 400 rather than +# silently falling through to the failure branch and persisting a FAILED +# job with an empty error. +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +def test_finalize_non_bool_success_is_400(m_release, m_mark_terminal): + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": "true"}), job_id=uuid.uuid4() + ) + assert resp.status_code == 400 + assert not m_release.called + assert not m_mark_terminal.called + + +# (7) finalize success: writes the result THEN marks the job terminal (order +# matters -- a duplicate must never rewrite a result nobody asked to change). +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_success_writes_result_then_marks_terminal( + m_jobs, m_write, m_mark_terminal, m_release +): + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="https://example.com/hook") + m_jobs.filter.return_value.first.return_value = job + m_write.return_value = "org/o/agent_kv/j/result.json" + m_mark_terminal.return_value = True + + manager = mock.Mock() + manager.attach_mock(m_write, "write_result") + manager.attach_mock(m_mark_terminal, "mark_terminal") + + job_id = uuid.uuid4() + resp = iv.FinalizeView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "success": True, + "result": {"foo": "bar"}, + "usage_summary": {"tokens": 10}, + }, + ), + job_id=job_id, + ) + + assert resp.status_code == 200 + assert resp.data == { + "finalized": True, + "webhook_url": "https://example.com/hook", + "status": "completed", + } + assert [c[0] for c in manager.mock_calls] == ["write_result", "mark_terminal"] + m_write.assert_called_once_with("org1", str(job_id), {"foo": "bar"}) + m_mark_terminal.assert_called_once_with( + job_id, + "org1", + JobStatus.COMPLETED, + result_ref="org/o/agent_kv/j/result.json", + usage_summary={"tokens": 10}, + ) + + +# (7b) finalize success deletes the staged input and blanks input_ref (spec +# D10: "uploaded document deleted on job completion") -- the result file +# itself is untouched by this. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(iv, "delete_input") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_success_deletes_input_and_blanks_ref( + m_jobs, m_write, m_delete_input, m_mark_terminal, m_release +): + job = AgentKVJob( + status=JobStatus.RUNNING, + webhook_url="", + input_ref="org/o/agent_kv/j/input.pdf", + ) + m_jobs.filter.return_value.first.return_value = job + m_write.return_value = "org/o/agent_kv/j/result.json" + + job_id = uuid.uuid4() + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": True, "result": {}}), + job_id=job_id, + ) + + assert resp.status_code == 200 + m_delete_input.assert_called_once_with(job) + m_jobs.filter.return_value.update.assert_called_once_with(input_ref="") + + +# (7c) success finalize whose guard is LOST right after the write (e.g. a +# cancel raced in between the read and mark_terminal's guarded UPDATE) -- +# the just-written result file has no ref anywhere pointing at it, so it's +# cleaned up immediately rather than orphaned forever. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(iv, "delete_result_file") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_success_guard_loss_deletes_orphaned_result( + m_jobs, m_write, m_mark_terminal, m_delete_result, m_release +): + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="") + m_jobs.filter.return_value.first.return_value = job + m_write.return_value = "org/o/agent_kv/j/result.json" + + job_id = uuid.uuid4() + resp = iv.FinalizeView.as_view()( + _post( + "/x", + {"org_id": "org1", "success": True, "result": {"foo": "bar"}}, + ), + job_id=job_id, + ) + + assert resp.status_code == 200 + assert resp.data["finalized"] is False + m_delete_result.assert_called_once_with("org/o/agent_kv/j/result.json") + + +# (7d) success finalize that WINS the guard never deletes the result it just +# wrote -- only a guard-LOSS orphan triggers cleanup. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(iv, "delete_result_file") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_success_guard_win_does_not_delete_result( + m_jobs, m_write, m_mark_terminal, m_delete_result, m_release +): + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="") + m_jobs.filter.return_value.first.return_value = job + m_write.return_value = "org/o/agent_kv/j/result.json" + + iv.FinalizeView.as_view()( + _post( + "/x", + {"org_id": "org1", "success": True, "result": {"foo": "bar"}}, + ), + job_id=uuid.uuid4(), + ) + + assert not m_delete_result.called + + +# (7e) concurrent duplicate-SUCCESS finalize race, exercised through the REAL +# storage layer (only the object-store FileSystem is faked): two finalize +# attempts land for one job, the first WINS the terminal guard and the second +# LOSES. Because write_result now returns a UNIQUE ref per attempt, the loser +# cleans up only ITS OWN orphaned result file -- the winner's result_ref +# target is left intact (pre-Greptile critical #3: the old deterministic +# result.json path let the loser delete the winner's live result). +@mock.patch.object(storage, "FileSystem") +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", side_effect=[True, False]) +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_duplicate_success_race_loser_deletes_only_its_own_result( + m_jobs, m_mark, m_release, m_fs +): + # Two separate reads that both observed the job as still RUNNING (the race + # window before either guarded UPDATE ran). + winner_job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="", input_ref="") + loser_job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="", input_ref="") + m_jobs.filter.return_value.first.side_effect = [winner_job, loser_job] + + fh = m_fs.return_value.get_file_storage.return_value + written: list[str] = [] + removed: list[str] = [] + fh.json_dump.side_effect = lambda path, data: written.append(path) + fh.rm.side_effect = lambda path: removed.append(path) + + job_id = uuid.uuid4() + body = {"org_id": "org1", "success": True, "result": {"foo": "bar"}} + r1 = iv.FinalizeView.as_view()(_post("/x", body), job_id=job_id) # winner + r2 = iv.FinalizeView.as_view()(_post("/x", body), job_id=job_id) # loser + + assert r1.data["finalized"] is True + assert r2.data["finalized"] is False + + # Both attempts wrote a result; the two refs are DISTINCT. + assert len(written) == 2 + assert written[0] != written[1] + + # The winner's result file (written[0]) is the one mark_terminal stored as + # result_ref -- it must NOT be deleted. Only the loser's orphan is removed. + assert removed == [written[1]] + assert written[0] not in removed + + # The winner stored its OWN specific ref, not a shared deterministic path. + winner_ref = m_mark.call_args_list[0].kwargs["result_ref"] + assert winner_ref == written[0] + + +# (8) duplicate finalize: the job is already terminal (guard excludes it), +# so this is a no-op -- finalized:false and the result is NOT rewritten. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_duplicate_does_not_rewrite_result( + m_jobs, m_write, m_mark_terminal, m_release +): + job = AgentKVJob(status=JobStatus.COMPLETED, webhook_url="https://example.com/hook") + m_jobs.filter.return_value.first.return_value = job + + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": True, "result": {"foo": "bar"}}), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + assert resp.data == { + "finalized": False, + "webhook_url": "https://example.com/hook", + "status": "completed", + } + assert not m_write.called + assert not m_mark_terminal.called + + +# (8b) duplicate finalize (guard-lost) never calls delete_input either -- +# either another writer already owns cleanup, or there's nothing new to +# terminalize. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "delete_input") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_duplicate_does_not_call_delete_input( + m_jobs, m_delete_input, m_mark_terminal, m_release +): + job = AgentKVJob( + status=JobStatus.COMPLETED, + webhook_url="", + input_ref="org/o/agent_kv/j/input.pdf", + ) + m_jobs.filter.return_value.first.return_value = job + + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": True, "result": {}}), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + assert not m_delete_input.called + assert not m_jobs.filter.return_value.update.called + + +# (9) finalize failure records the error via mark_terminal(FAILED, ...). +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_failure_records_error(m_jobs, m_write, m_mark_terminal, m_release): + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="") + m_jobs.filter.return_value.first.return_value = job + m_mark_terminal.return_value = True + + job_id = uuid.uuid4() + resp = iv.FinalizeView.as_view()( + _post( + "/x", + {"org_id": "org1", "success": False, "error": "LLM provider timed out"}, + ), + job_id=job_id, + ) + + assert resp.status_code == 200 + assert resp.data == {"finalized": True, "webhook_url": "", "status": "failed"} + assert not m_write.called + m_mark_terminal.assert_called_once_with( + job_id, "org1", JobStatus.FAILED, error="LLM provider timed out" + ) + + +# (9b) finalize failure also deletes the staged input and blanks input_ref +# -- the run is over whether it completed or failed. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(iv, "delete_input") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_failure_deletes_input_and_blanks_ref( + m_jobs, m_delete_input, m_mark_terminal, m_release +): + job = AgentKVJob( + status=JobStatus.RUNNING, + webhook_url="", + input_ref="org/o/agent_kv/j/input.pdf", + ) + m_jobs.filter.return_value.first.return_value = job + + job_id = uuid.uuid4() + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": False, "error": "boom"}), + job_id=job_id, + ) + + assert resp.status_code == 200 + m_delete_input.assert_called_once_with(job) + m_jobs.filter.return_value.update.assert_called_once_with(input_ref="") + + +# (10) the concurrency slot is released on every finalize path -- including +# when the write/finalize work raises -- because release() sits in a +# finally. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_releases_slot_even_when_write_result_raises( + m_jobs, m_write, m_mark_terminal, m_release +): + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="") + m_jobs.filter.return_value.first.return_value = job + m_write.side_effect = RuntimeError("storage exploded") + + job_id = uuid.uuid4() + try: + iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": True, "result": {}}), + job_id=job_id, + ) + except RuntimeError: + pass + + m_release.assert_called_once_with("org1", str(job_id)) + assert not m_mark_terminal.called + + +# (10b) slot released on the duplicate (already-terminal, no-op) path too. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(iv, "write_result") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_releases_slot_on_duplicate_noop_path( + m_jobs, m_write, m_mark_terminal, m_release +): + job = AgentKVJob(status=JobStatus.CANCELLED, webhook_url="") + m_jobs.filter.return_value.first.return_value = job + + job_id = uuid.uuid4() + iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": False, "error": "late"}), + job_id=job_id, + ) + + m_release.assert_called_once_with("org1", str(job_id)) + + +# --------------------------------------------------------------------------- +# URL wiring +# --------------------------------------------------------------------------- + + +# (11) regression pin for the frozen paths (spec Interfaces block): the +# cloud executor calls these exact URLs, so a dropped/renamed include in +# internal_base_urls.py must fail loudly here rather than 404 in prod. +def test_frozen_internal_urls_resolve_to_the_right_views(): + from django.urls import resolve + + job_id = uuid.uuid4() + stage = resolve(f"/internal/v1/agent-kv/jobs/{job_id}/stage/") + assert stage.func.cls is iv.StageReportView + assert stage.kwargs == {"job_id": job_id} + + finalize = resolve(f"/internal/v1/agent-kv/jobs/{job_id}/finalize/") + assert finalize.func.cls is iv.FinalizeView + assert finalize.kwargs == {"job_id": job_id} diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py new file mode 100644 index 0000000000..a070af8d76 --- /dev/null +++ b/backend/agent_kv/tests/test_job_views.py @@ -0,0 +1,513 @@ +import os +import uuid +from datetime import timedelta +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.utils import timezone # noqa: E402 +from rest_framework.test import APIRequestFactory # noqa: E402 + +from agent_kv import execution_views as ev # noqa: E402 +from agent_kv import execution_views_result as evr # noqa: E402 +from agent_kv.models import AgentKVJob, AgentKVKey, JobStatus # noqa: E402 +from agent_kv.tests._factories import kv_key # noqa: E402 + + +def _authed(method="get", path="/agent-kv/x"): + req = getattr(APIRequestFactory(), method)(path) + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + return req + + +# --------------------------------------------------------------------------- +# (1) status for foreign-org job -> 404 (indistinguishable from unknown). +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_foreign_org_job_is_404(m_keys, m_jobs): + m_keys.get.return_value = kv_key() + m_jobs.get.side_effect = AgentKVJob.DoesNotExist + resp = ev.JobStatusView.as_view()(_authed(), job_id=uuid.uuid4()) + assert resp.status_code == 404 + + +# --------------------------------------------------------------------------- +# (2) status running -> stages list ordered per STAGE_NAMES, lowercased +# status, only the stages actually present are included. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_status_running_builds_ordered_stages_and_lowercases_status(m_keys, m_jobs): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.RUNNING, + stage="extraction", + stages={ + "extraction": {"status": "running"}, + "document_processing": {"status": "done", "seconds": 0.5}, + }, + pages_total=3, + ) + job.created_at = timezone.now() + m_jobs.get.return_value = job + + resp = ev.JobStatusView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + # `status` is the JOB's, so it stays top level; stage reporting is + # extractor-scoped (spec §7.2). + assert resp.data["status"] == "running" + kv = resp.data["extractors"]["kv"] + assert kv["stage"] == "extraction" + # STAGE_NAMES order is document_processing, extraction, ... -- "qa" and + # every other configured stage name is absent from job.stages, so only + # these two appear, in that order. + assert [s["name"] for s in kv["stages"]] == [ + "document_processing", + "extraction", + ] + assert resp.data["pages_total"] == 3 + assert "error" not in resp.data + + +# --------------------------------------------------------------------------- +# (3) result before completion (non-terminal, e.g. RUNNING) -> 409 with the +# current status, lowercased (spec §7.3 controller ruling). +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_before_completion_is_409_with_current_status(m_keys, m_jobs): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 409 + assert resp.data == {"status": "running"} + + +# --------------------------------------------------------------------------- +# (4) result after expires_at -> 404. +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_after_expiry_is_404(m_keys, m_jobs, m_read): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.COMPLETED, + result_ref="org/o/agent_kv/j/result.json", + expires_at=timezone.now() - timedelta(days=1), + ) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 404 + assert not m_read.called + + +# --------------------------------------------------------------------------- +# (5) result happy path -> returns read_result payload unchanged. +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result", return_value={"success": True, "fields": {}}) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_happy_path_returns_read_result_payload(m_keys, m_jobs, m_read): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.COMPLETED, + result_ref="org/o/agent_kv/j/result.json", + expires_at=timezone.now() + timedelta(days=1), + ) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + # The stored blob is the engine's own result; the response namespaces it + # per extractor and adds per-extractor usage attribution (spec §7.3). + # `success`/`status` at top level on every terminal payload, so a client + # branches the same way for completed, failed and cancelled. + assert resp.data["success"] is True + assert resp.data["status"] == "completed" + assert resp.data["extractors"] == {"kv": {"success": True, "fields": {}}} + assert set(resp.data["usage_summary"]) == {"total", "by_extractor"} + assert list(resp.data["usage_summary"]["by_extractor"]) == ["kv"] + m_read.assert_called_once_with(job.result_ref) + + +# --------------------------------------------------------------------------- +# (5b) result for a FAILED job -> 200 with a success:false body carrying the +# job's own (user-safe) error -- spec §7.3: "Failed jobs: {success: false, +# error, timing} with a user-safe error". This also covers the SubmitView +# sync-wait fix: that branch reuses this exact function unconditionally for +# any terminal job, so it now gets a correct 200 body instead of a 404. +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_for_failed_job_is_200_with_success_false_and_error( + m_keys, m_jobs, m_read +): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.FAILED, + error="LLM provider timed out", + expires_at=timezone.now() + timedelta(days=1), + ) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert resp.data == { + "success": False, + "status": "failed", + "error": "LLM provider timed out", + } + assert not m_read.called + + +# --------------------------------------------------------------------------- +# (5c) result for a CANCELLED job -> 200 with a fixed success:false/cancelled +# body (spec §7.3 controller ruling). +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_for_cancelled_job_is_200_with_cancelled_body(m_keys, m_jobs, m_read): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.CANCELLED, + expires_at=timezone.now() + timedelta(days=1), + ) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert resp.data == {"success": False, "status": "cancelled"} + assert not m_read.called + + +# --------------------------------------------------------------------------- +# (5d) result for a COMPLETED job with a blank result_ref (files already +# swept by TTL cleanup, row not yet expired) -> 404, same as the expired +# case -- exercises the new blank-ref branch distinctly from expiry. +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_result_completed_with_blank_ref_is_404(m_keys, m_jobs, m_read): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.COMPLETED, + result_ref="", + expires_at=timezone.now() + timedelta(days=1), + ) + m_jobs.get.return_value = job + + resp = ev.JobResultView.as_view()(_authed(), job_id=uuid.uuid4()) + + assert resp.status_code == 404 + assert not m_read.called + + +# --------------------------------------------------------------------------- +# (6) cancel on RUNNING -> mark_terminal called with CANCELLED, 200. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_on_running_marks_terminal_and_200s( + m_keys, m_jobs, m_mark_terminal, m_release +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert resp.data == {"status": "cancelled"} + m_mark_terminal.assert_called_once_with( + job.id, job.organization_id, JobStatus.CANCELLED + ) + + +# --------------------------------------------------------------------------- +# (6b) cancel that WINS the terminal guard releases the concurrency slot -- +# a job cancelled BEFORE dispatch gets no finalize callback and the sweep's +# phase-1 only targets PENDING (not CANCELLED), so without this its slot +# would leak until the 6h TTL (pre-Greptile important #4). release() is +# idempotent (zrem), so a later finalize-callback release is harmless. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_win_releases_concurrency_slot(m_keys, m_jobs, m_mark_terminal, m_release): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.PENDING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + m_release.assert_called_once_with("org1", str(job.id)) + + +# --------------------------------------------------------------------------- +# (6c) cancel that LOSES the guard (job already terminal) must NOT release -- +# whoever terminalized it (finalize callback / a prior cancel) owns the slot. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_loss_does_not_release_slot(m_keys, m_jobs, m_mark_terminal, m_release): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.COMPLETED) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 409 + assert not m_release.called + + +# --------------------------------------------------------------------------- +# (7) cancel on COMPLETED -> 409 with current status; result untouched +# (the guard lost, so nothing about the stored result is read or written). +# --------------------------------------------------------------------------- +@mock.patch.object(evr, "read_result") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_on_completed_is_409_and_result_untouched( + m_keys, m_jobs, m_mark_terminal, m_read +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.COMPLETED) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 409 + # Status is lowercased consistently across every endpoint (spec §7.2) -- + # the 409 body used to leak the raw uppercase value (pre-Greptile #5). + assert resp.data == {"status": "completed"} + assert m_mark_terminal.called + assert not m_read.called + + +# --------------------------------------------------------------------------- +# (8) delete calls delete_job_files and blanks both refs. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_calls_delete_job_files_and_blanks_refs( + m_keys, m_jobs, m_delete_files, m_save +): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.COMPLETED, + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + m_jobs.get.return_value = job + m_delete_files.return_value = ["input_ref", "result_ref"] + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + m_delete_files.assert_called_once_with(job) + assert job.input_ref == "" + assert job.result_ref == "" + # update_fields carries exactly the blanked refs, so a ref left set is not + # written back as "" by a wider save(). + assert m_save.call_args.kwargs["update_fields"] == ["input_ref", "result_ref"] + + +# --------------------------------------------------------------------------- +# (8b) delete on a non-terminal (RUNNING) job cancels it FIRST, before the +# files are deleted -- a still-running job that finalizes late would +# otherwise write a fresh result onto a job the caller just deleted. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_on_running_job_cancels_before_deleting_files( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + m_delete_files.return_value = ["input_ref", "result_ref"] + + manager = mock.Mock() + manager.attach_mock(m_mark_terminal, "mark_terminal") + manager.attach_mock(m_delete_files, "delete_job_files") + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + m_mark_terminal.assert_called_once_with(job.id, "org1", JobStatus.CANCELLED) + m_delete_files.assert_called_once_with(job) + assert [c[0] for c in manager.mock_calls] == ["mark_terminal", "delete_job_files"] + + +# --------------------------------------------------------------------------- +# (8e) DELETE on an in-flight job RELEASES the concurrency slot, exactly as +# JobCancelView does (Greptile review #2). The slot is taken at submit and +# released by _fail_job_response, the finalize callback and the sweep -- a job +# terminalized here before dispatch hits none of those, and the sweep's phase-1 +# only targets PENDING, never CANCELLED. Without the release each such delete +# leaked a slot until the 6h TTL, and enough of them exhaust the org's +# allowance and start rejecting new submits. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_on_running_job_releases_the_concurrency_slot( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + m_release.assert_called_once_with("org1", str(job.id)) + + +# --------------------------------------------------------------------------- +# (8f) ...but only when THIS request won the terminal-state race. A concurrent +# cancel or finalize that terminalized first is the one accounting for the +# slot; releasing on a lost race would hand a slot back twice. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_does_not_release_the_slot_when_it_loses_the_terminal_race( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert not m_release.called + + +# --------------------------------------------------------------------------- +# (8g) a ref whose file could NOT be deleted is left set (Greptile review #3). +# It is the only handle TTL cleanup can retry from: its candidate query matches +# rows by `input_ref > "" OR result_ref > ""`, so blanking a ref whose object is +# still in the bucket orphans that object permanently. Still 204 -- the job is +# terminal and the caller's intent is recorded. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_keeps_the_ref_whose_file_delete_failed( + m_keys, m_jobs, m_delete_files, m_save +): + m_keys.get.return_value = kv_key() + job = AgentKVJob( + status=JobStatus.COMPLETED, + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + m_jobs.get.return_value = job + # Input gone; the result delete raised inside delete_job_files. + m_delete_files.return_value = ["input_ref"] + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert job.input_ref == "" + assert job.result_ref == "org/o/agent_kv/j/result.json" + assert m_save.call_args.kwargs["update_fields"] == ["input_ref"] + + +# --------------------------------------------------------------------------- +# (8c) delete on an already-terminal job never attempts to cancel it again -- +# unchanged behavior for the terminal case. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_on_terminal_job_does_not_call_mark_terminal( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.COMPLETED) + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert not m_mark_terminal.called + m_delete_files.assert_called_once_with(job) + + +# --------------------------------------------------------------------------- +# (9) every job-scoped endpoint 401s (403, per Forbidden.status_code) +# without a key (spec §6.8 regression). +# +# The brief's shown snippet for this test wraps the call in +# ``pytest.raises(Forbidden)``, mirroring test_auth.py -- but that suite +# calls the ``@validate_api_key``-decorated function directly, bypassing +# DRF's dispatch(). Routed through the real ``.as_view()()`` cycle (as here, +# and as every other view in this module is exercised), DRF's dispatch() +# catches the raised ``Forbidden`` (an APIException) via +# ``drf_standardized_errors``'s exception handler and renders it as a normal +# Response -- exactly like ``test_foreign_org_job_is_404`` above asserts +# ``resp.status_code`` for ``JobNotFound`` rather than expecting a raise. +# Verified empirically (see task-9-report.md); asserting the response here +# instead keeps the same regression coverage without a spurious failure. +# --------------------------------------------------------------------------- +def test_all_job_views_401_without_key(): + for view, method in [ + (ev.JobStatusView, "get"), + (ev.JobResultView, "get"), + (ev.JobCancelView, "post"), + (ev.JobDeleteView, "delete"), + ]: + req = getattr(APIRequestFactory(), method)("/agent-kv/x") + resp = view.as_view()(req, job_id=uuid.uuid4()) + assert resp.status_code == 403 diff --git a/backend/agent_kv/tests/test_key_management.py b/backend/agent_kv/tests/test_key_management.py new file mode 100644 index 0000000000..f092f53bdd --- /dev/null +++ b/backend/agent_kv/tests/test_key_management.py @@ -0,0 +1,95 @@ +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from utils.filters.organization_filter import OrganizationFilterBackend # noqa: E402 + +from agent_kv.models import AgentKVKey # noqa: E402 +from agent_kv.serializers import AgentKVKeyWriteSerializer # noqa: E402 +from agent_kv.views import AgentKVKeyViewSet # noqa: E402 + + +def test_write_serializer_rejects_blank_name(): + s = AgentKVKeyWriteSerializer(data={"name": "", "description": "x"}) + assert not s.is_valid() + assert "name" in s.errors + + +def test_rotate_assigns_fresh_key_and_saves(): + key_obj = AgentKVKey(name="k", key=uuid.uuid4()) + old = key_obj.key + with mock.patch.object(AgentKVKey, "save") as m_save: + view = AgentKVKeyViewSet() + view.get_object = lambda: key_obj + view.format_kwarg = None + view.request = mock.Mock() + resp = view.rotate(view.request, pk=str(key_obj.id)) + assert key_obj.key != old + assert m_save.called + assert resp.status_code == 200 + + +@mock.patch.object(AgentKVKey, "objects") +def test_org_scoping_via_filter_backend_is_engaged(m_objects): + """Org-scoping is enforced by the global filter backend, not the manager. + + Two things must hold for that guard to stay real: (1) the viewset must + keep ``OrganizationFilterBackend`` in its resolved ``filter_backends`` — + so nobody can quietly swap it out later — and (2) ``get_queryset`` must + hand back the *unscoped* base queryset (``AgentKVKey.objects.all()``, + no ``organization=`` kwarg of its own) rather than pre-filtering, which + would keep working (and hide the loss of protection) even if the backend + were ever removed from ``DEFAULT_FILTER_BACKENDS``. + """ + view = AgentKVKeyViewSet() + + assert OrganizationFilterBackend in view.filter_backends + + result = view.get_queryset() + + m_objects.all.assert_called_once_with() + m_objects.filter.assert_not_called() + assert result is m_objects.all.return_value + + +def test_create_returns_id_and_key(): + """POST .../keys/ must echo the read serializer, not the write one. + + A caller who just created a key has no other way to learn its ``id`` or + the raw ``key`` value -- ``AgentKVKeyWriteSerializer`` (used to validate + the request) carries neither. ``serializer.save`` is stubbed rather than + hitting the real ORM: this file has no DB fixture, and ``ModelSerializer + .save()``'s default create path resolves ``AgentKVKey._default_manager`` + independently of any ``mock.patch.object(AgentKVKey, "objects")`` swap + (a different manager reference), so mocking ``.objects`` the way the + other tests in this file do would silently fall through to a real + database call here. + """ + key_obj = AgentKVKey( + id=uuid.uuid4(), name="k", description="", key=uuid.uuid4(), is_active=True + ) + + def _fake_save(self, **kwargs): + self.instance = key_obj + return key_obj + + view = AgentKVKeyViewSet() + view.action = "create" + view.format_kwarg = None + view.kwargs = {} + view.request = mock.Mock(data={"name": "k"}, user=mock.Mock()) + + with mock.patch.object(AgentKVKeyWriteSerializer, "save", _fake_save): + resp = view.create(view.request) + + assert resp.status_code == 201, resp.data + # DRF serializes UUID fields as strings. + assert resp.data["id"] == str(key_obj.id), resp.data + assert resp.data["key"] == str(key_obj.key), resp.data diff --git a/backend/agent_kv/tests/test_maintenance_commands.py b/backend/agent_kv/tests/test_maintenance_commands.py new file mode 100644 index 0000000000..b527b15a6c --- /dev/null +++ b/backend/agent_kv/tests/test_maintenance_commands.py @@ -0,0 +1,54 @@ +"""Management-command entrypoints for the Agent-KV maintenance jobs. + +Same no-test-DB style as the rest of ``agent_kv/tests``: real Django app +registry via ``django.setup()``, no database. ``agent_kv.maintenance``'s two +entrypoints are patched so no query ever runs -- their own behaviour +(candidate queries, guard semantics, batching) is covered where it lives now, +``agent_kv/maintenance.py``, exercised indirectly through the views in +``test_sweeps.py``. What's pinned here is only the command<->function wiring: +each command calls its maintenance function exactly once, with no arguments, +and writes the returned dict to stdout as JSON -- the shape a Kubernetes +CronJob log line needs. +""" + +import json +import os +from io import StringIO +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.core.management import call_command # noqa: E402 + +from agent_kv import maintenance # noqa: E402 + + +@mock.patch.object(maintenance, "run_sweep") +def test_agent_kv_sweep_command_calls_run_sweep_once_and_prints_its_result( + m_run_sweep, +): + m_run_sweep.return_value = {"swept": 2, "timed_out": 1} + out = StringIO() + + call_command("agent_kv_sweep", stdout=out) + + m_run_sweep.assert_called_once_with() + assert json.loads(out.getvalue()) == {"swept": 2, "timed_out": 1} + + +@mock.patch.object(maintenance, "run_ttl_cleanup") +def test_agent_kv_ttl_cleanup_command_calls_run_ttl_cleanup_once_and_prints_its_result( + m_run_ttl_cleanup, +): + m_run_ttl_cleanup.return_value = {"cleaned": 5} + out = StringIO() + + call_command("agent_kv_ttl_cleanup", stdout=out) + + m_run_ttl_cleanup.assert_called_once_with() + assert json.loads(out.getvalue()) == {"cleaned": 5} diff --git a/backend/agent_kv/tests/test_models.py b/backend/agent_kv/tests/test_models.py new file mode 100644 index 0000000000..6966fac515 --- /dev/null +++ b/backend/agent_kv/tests/test_models.py @@ -0,0 +1,54 @@ +"""Terminal-state write guard: the invariant everything else leans on (spec §5.4).""" + +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from agent_kv.models import AgentKVJob, JobStatus # noqa: E402 + + +def test_terminal_set_is_exactly_the_three_states(): + assert AgentKVJob.TERMINAL == frozenset( + {JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED} + ) + + +@mock.patch.object(AgentKVJob, "objects") +def test_mark_terminal_excludes_terminal_rows_and_reports_success(m_objects): + m_qs = m_objects.filter.return_value.exclude.return_value + m_qs.update.return_value = 1 + job_id = uuid.uuid4() + ok = AgentKVJob.mark_terminal( + job_id=job_id, + organization_id="org1", + new_status=JobStatus.FAILED, + error="boom", + ) + assert ok is True + filter_kwargs = m_objects.filter.call_args.kwargs + assert filter_kwargs["id"] == job_id + assert filter_kwargs["organization_id"] == "org1" + _, exclude_kwargs = m_objects.filter.return_value.exclude.call_args + assert set(exclude_kwargs["status__in"]) == set(AgentKVJob.TERMINAL) + update_kwargs = m_qs.update.call_args.kwargs + assert update_kwargs["status"] == JobStatus.FAILED + assert update_kwargs["error"] == "boom" + assert "completed_at" in update_kwargs + + +@mock.patch.object(AgentKVJob, "objects") +def test_mark_terminal_on_already_terminal_row_is_noop_false(m_objects): + m_objects.filter.return_value.exclude.return_value.update.return_value = 0 + ok = AgentKVJob.mark_terminal( + job_id=uuid.uuid4(), + organization_id="org1", + new_status=JobStatus.COMPLETED, + ) + assert ok is False diff --git a/backend/agent_kv/tests/test_rate_limiter.py b/backend/agent_kv/tests/test_rate_limiter.py new file mode 100644 index 0000000000..58fa295fd0 --- /dev/null +++ b/backend/agent_kv/tests/test_rate_limiter.py @@ -0,0 +1,155 @@ +import os +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.conf import settings # noqa: E402 + +from agent_kv import rate_limiter as rl # noqa: E402 + + +@mock.patch.object(rl, "_redis") +@mock.patch.object(rl.time, "time", return_value=1_700_000_000.0) +def test_acquire_under_limit(m_time, m_redis): + """Acquire is ONE atomic server-side script call (trim, count, add, + expire) -- never a client-side ``ZCARD`` followed by ``ZADD``, which is a + check-then-act race that let 6 concurrent submits through a limit of 5 + in the 13b integration run. The e2e concurrency scenario is the real + proof; this pins the script's inputs. + """ + mock_redis = m_redis.return_value + mock_redis.eval.return_value = 1 + + assert rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") is True + + key = "agent_kv:inflight:org1" + mock_redis.eval.assert_called_once() + args = mock_redis.eval.call_args[0] + script, numkeys, called_key, member, now, ttl_cut, limit, expire = args + assert script is rl.AgentKVConcurrencyLimiter._ACQUIRE_SCRIPT + assert (numkeys, called_key, member) == (1, key, "job1") + assert now == 1_700_000_000.0 + assert ttl_cut == 1_700_000_000.0 - rl._SLOT_TTL_SECONDS + assert limit == settings.AGENT_KV_CONCURRENT_LIMIT + assert expire == rl._SLOT_TTL_SECONDS + # No client-side check-then-act calls remain. + assert not mock_redis.zcard.called + assert not mock_redis.zadd.called + # Self-heal-before-check-before-acquire ordering lives inside the script. + body = script + assert body.index("ZREMRANGEBYSCORE") < body.index("ZCARD") < body.index("ZADD") + + +@mock.patch.object(rl, "_redis") +def test_acquire_at_limit_refused(m_redis): + m_redis.return_value.eval.return_value = 0 + assert rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") is False + + +@mock.patch.object(rl, "_redis") +def test_release_removes_member(m_redis): + rl.AgentKVConcurrencyLimiter.release("org1", "job1") + m_redis.return_value.zrem.assert_called_once_with("agent_kv:inflight:org1", "job1") + + +@mock.patch.object(rl, "_redis") +def test_redis_error_during_the_script_fails_closed(m_redis): + """Was `test_redis_error_fails_open`, asserting `is True`. + + It pinned the defect as the contract: an error mid-script removed the + concurrency ceiling and the API kept accepting billable work. The failure + now surfaces as a 429 at the caller (`RateLimited`), which is recoverable; + an unbounded fan-out is not. See `AGENT_KV_LIMITER_FAIL_OPEN` for the + deliberate waiver. + """ + m_redis.return_value.eval.side_effect = ConnectionError("down") + with mock.patch.object(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False): + assert rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") is False + + +@mock.patch.object(rl, "_redis") +@mock.patch.object(rl.time, "time", return_value=1_700_000_000.0) +def test_key_rate_over_limit(m_time, m_redis): + mock_redis = m_redis.return_value + mock_redis.incr.return_value = 61 + + assert rl.check_key_rate("key1") is False + + expected_window = int(1_700_000_000.0 // 60) + expected_key = f"agent_kv:rate:key1:{expected_window}" + mock_redis.incr.assert_called_once_with(expected_key) + mock_redis.expire.assert_called_once_with(expected_key, 120) + + +@mock.patch.object(rl, "_redis") +@mock.patch.object(rl.time, "time", return_value=1_700_000_000.0) +def test_key_rate_under_limit(m_time, m_redis): + mock_redis = m_redis.return_value + mock_redis.incr.return_value = 3 + + assert rl.check_key_rate("key1") is True + + expected_window = int(1_700_000_000.0 // 60) + expected_key = f"agent_kv:rate:key1:{expected_window}" + mock_redis.incr.assert_called_once_with(expected_key) + mock_redis.expire.assert_called_once_with(expected_key, 120) + + +# --------------------------------------------------------------------------- +# Backend-unavailable behaviour. Both limiters used to `return True` on ANY +# Redis exception, which is the one failure mode invisible from outside: a +# Sentinel failover or pool exhaustion removed the concurrency ceiling AND the +# per-key rate ceiling simultaneously, while the API went on returning 202s for +# billable LLM work with only a per-request `logger.warning` to show for it. +# --------------------------------------------------------------------------- + + +@mock.patch.object(rl, "_redis", side_effect=OSError("redis down")) +def test_concurrency_limiter_fails_closed_by_default(m_redis): + with mock.patch.object(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False): + assert rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") is False + + +@mock.patch.object(rl, "_redis", side_effect=OSError("redis down")) +def test_key_rate_limiter_fails_closed_by_default(m_redis): + with mock.patch.object(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False): + assert rl.check_key_rate("key1") is False + + +@mock.patch.object(rl, "_redis", side_effect=OSError("redis down")) +def test_limiters_fail_open_only_when_the_setting_says_so(m_redis): + """The waiver stays available -- availability-over-accounting is a real + operational choice -- but it has to be made deliberately, in config, rather + than being the implicit behaviour of an `except` block. + """ + with mock.patch.object(settings, "AGENT_KV_LIMITER_FAIL_OPEN", True): + assert rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") is True + assert rl.check_key_rate("key1") is True + + +@mock.patch.object(rl, "_redis", side_effect=OSError("redis down")) +def test_limiter_unavailability_is_logged_at_error_level(m_redis, caplog): + """`logger.exception`, not `logger.warning`. The old level is part of why + this sat unnoticed -- a limiter being gone is an error, not a warning. + """ + with mock.patch.object(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False): + with caplog.at_level("ERROR", logger=rl.logger.name): + rl.AgentKVConcurrencyLimiter.check_and_acquire("org1", "job1") + rl.check_key_rate("key1") + assert [r.levelname for r in caplog.records] == ["ERROR", "ERROR"] + # The traceback is attached, so the actual Redis failure is diagnosable. + assert all(r.exc_info for r in caplog.records) + + +@mock.patch.object(rl, "_redis", side_effect=OSError("redis down")) +def test_release_still_tolerates_an_unreachable_backend(m_redis): + """Release must stay best-effort regardless: it is called on terminal + paths (finalize, cancel, sweep) where raising would abort the caller's own + work, and a slot it cannot free expires on its own TTL. + """ + rl.AgentKVConcurrencyLimiter.release("org1", "job1") diff --git a/backend/agent_kv/tests/test_storage.py b/backend/agent_kv/tests/test_storage.py new file mode 100644 index 0000000000..257a179dea --- /dev/null +++ b/backend/agent_kv/tests/test_storage.py @@ -0,0 +1,304 @@ +import os +import re +from unittest import mock + +import django +import pytest +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.conf import settings # noqa: E402 +from django.test import override_settings # noqa: E402 + +from agent_kv import storage # noqa: E402 +from agent_kv.models import AgentKVJob # noqa: E402 + + +@mock.patch.object(storage, "FileSystem") +def test_stage_input_path_and_write(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + up = mock.Mock() + up.name = "invoice.PDF" + up.chunks.return_value = [b"a", b"b"] + ref = storage.stage_input("org1", "job1", up) + assert ref == "unstract/agent_kv/org1/job1/input.pdf" + assert fh.write.called + kwargs = fh.write.call_args.kwargs + assert kwargs["path"] == "unstract/agent_kv/org1/job1/input.pdf" + assert kwargs["data"] == b"ab" + + +@mock.patch.object(storage, "FileSystem") +def test_stage_input_defaults_extension_when_missing(m_fs): + up = mock.Mock() + up.name = "noext" + up.chunks.return_value = [b"x"] + ref = storage.stage_input("org1", "job1", up) + assert ref == "unstract/agent_kv/org1/job1/input.bin" + + +@mock.patch.object(storage, "FileSystem") +def test_write_and_read_result_roundtrip_path(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + ref = storage.write_result("org1", "job1", {"success": True}) + # The result path is UNIQUE per finalize attempt (a nonce suffix) so a + # guard-losing concurrent finalize deletes only its own orphan, never the + # winning row's result_ref target (pre-Greptile critical #3). It still + # lives under the deterministic job dir. + assert re.fullmatch( + r"unstract/agent_kv/org1/job1/result-[0-9a-f]{32}\.json", ref + ), ref + fh.json_dump.assert_called_once() + assert fh.json_dump.call_args.kwargs["path"] == ref + assert fh.json_dump.call_args.kwargs["data"] == {"success": True} + + +@mock.patch.object(storage, "FileSystem") +def test_write_result_produces_unique_ref_per_call(m_fs): + # Two writes for the SAME job must yield two DISTINCT refs -- this is what + # makes a concurrent duplicate-success finalize safe: the guard loser's + # delete targets its own ref, not the winner's. + a = storage.write_result("org1", "job1", {"n": 1}) + b = storage.write_result("org1", "job1", {"n": 2}) + assert a != b + assert a.startswith("unstract/agent_kv/org1/job1/result-") + assert b.startswith("unstract/agent_kv/org1/job1/result-") + + +@mock.patch.object(storage, "FileSystem") +def test_write_result_accepts_explicit_nonce(m_fs): + # A caller may pin the nonce (deterministic ref for a given attempt). + ref = storage.write_result("org1", "job1", {"n": 1}, nonce="abc123") + assert ref == "unstract/agent_kv/org1/job1/result-abc123.json" + + +@mock.patch.object(storage, "FileSystem") +def test_read_result_returns_parsed_json(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + fh.json_load.return_value = {"success": True} + out = storage.read_result("unstract/agent_kv/org1/job1/result.json") + assert out == {"success": True} + fh.json_load.assert_called_once_with(path="unstract/agent_kv/org1/job1/result.json") + + +@mock.patch.object(storage, "FileSystem") +def test_delete_job_files_removes_both_refs(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + job = AgentKVJob( + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + cleared = storage.delete_job_files(job) + removed = {c.kwargs["path"] for c in fh.rm.call_args_list} + assert removed == {job.input_ref, job.result_ref} + assert cleared == ["input_ref", "result_ref"] + + +@mock.patch.object(storage, "FileSystem") +def test_delete_job_files_tolerates_missing_files(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + fh.rm.side_effect = FileNotFoundError("gone") + job = AgentKVJob( + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + # Must not raise, and both count as CLEARED: the goal is "the file is not + # there", and a ref whose object is already gone has nothing to retry. + cleared = storage.delete_job_files(job) + assert fh.rm.call_count == 2 + assert cleared == ["input_ref", "result_ref"] + + +@mock.patch.object(storage, "FileSystem") +def test_delete_job_files_skips_blank_refs(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + job = AgentKVJob(input_ref="", result_ref="") + cleared = storage.delete_job_files(job) + assert not fh.rm.called + # Reported clear: nothing was ever staged, so there is nothing to retry and + # the caller is free to blank (already-blank) refs. + assert cleared == ["input_ref", "result_ref"] + + +# The Greptile-review contract (#3): a ref is reported clear ONLY when its file +# is confirmed gone. A storage error that is not FileNotFoundError leaves the +# ref out, and the ref is what TTL cleanup retries from -- its candidate query +# matches rows by `input_ref > "" OR result_ref > ""`, so a blanked ref whose +# object is still in the bucket orphans that object permanently. +@mock.patch.object(storage, "FileSystem") +def test_delete_job_files_omits_the_ref_whose_delete_raised(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + job = AgentKVJob( + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + + def rm(*, path): + if path == job.result_ref: + raise OSError("object store unreachable") + + fh.rm.side_effect = rm + + cleared = storage.delete_job_files(job) + + # Still attempted both -- one failure must not abort the other's cleanup. + assert fh.rm.call_count == 2 + assert cleared == ["input_ref"] + + +@mock.patch.object(storage, "FileSystem") +def test_delete_job_files_reports_nothing_clear_when_both_deletes_raise(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + fh.rm.side_effect = OSError("object store unreachable") + job = AgentKVJob( + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + assert storage.delete_job_files(job) == [] + + +# delete_input carries the same confirmed-clear contract as delete_job_files. +# It did not when the Greptile round fixed delete_job_files and its two +# callers: this third ref-blanking site (FinalizeView) was missed, because the +# fix followed one function's callers instead of grepping for every site that +# blanks a ref. A failed delete here orphaned the customer's uploaded document +# permanently -- TTL cleanup only selects rows that still carry a non-blank +# ref, so a blanked row can never be reconsidered. +@mock.patch.object(storage, "FileSystem") +def test_delete_input_reports_clear_on_success(m_fs): + job = AgentKVJob(input_ref="org/o/agent_kv/j/input.pdf") + assert storage.delete_input(job) is True + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_reports_clear_when_the_file_is_already_gone(m_fs): + m_fs.return_value.get_file_storage.return_value.rm.side_effect = FileNotFoundError + job = AgentKVJob(input_ref="org/o/agent_kv/j/input.pdf") + # Already missing is the goal state -- nothing left to retry. + assert storage.delete_input(job) is True + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_reports_NOT_clear_when_the_delete_raised(m_fs): + m_fs.return_value.get_file_storage.return_value.rm.side_effect = OSError("down") + job = AgentKVJob(input_ref="org/o/agent_kv/j/input.pdf") + # Must not raise, and must not claim the ref is clear. + assert storage.delete_input(job) is False + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_reports_clear_for_a_blank_ref(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + assert storage.delete_input(AgentKVJob(input_ref="")) is True + assert not fh.rm.called + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_removes_only_input_ref(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + job = AgentKVJob( + input_ref="org/o/agent_kv/j/input.pdf", + result_ref="org/o/agent_kv/j/result.json", + ) + storage.delete_input(job) + fh.rm.assert_called_once_with(path=job.input_ref) + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_tolerates_missing_file(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + fh.rm.side_effect = FileNotFoundError("gone") + job = AgentKVJob(input_ref="org/o/agent_kv/j/input.pdf") + # Must not raise. + storage.delete_input(job) + assert fh.rm.call_count == 1 + + +@mock.patch.object(storage, "FileSystem") +def test_delete_input_skips_blank_ref(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + job = AgentKVJob(input_ref="") + storage.delete_input(job) + assert not fh.rm.called + + +@mock.patch.object(storage, "FileSystem") +def test_delete_result_file_removes_the_given_ref(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + storage.delete_result_file("org/o/agent_kv/j/result.json") + fh.rm.assert_called_once_with(path="org/o/agent_kv/j/result.json") + + +@mock.patch.object(storage, "FileSystem") +def test_delete_result_file_tolerates_missing_file(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + fh.rm.side_effect = FileNotFoundError("gone") + # Must not raise. + storage.delete_result_file("org/o/agent_kv/j/result.json") + assert fh.rm.call_count == 1 + + +@mock.patch.object(storage, "FileSystem") +def test_delete_result_file_skips_blank_ref(m_fs): + fh = m_fs.return_value.get_file_storage.return_value + storage.delete_result_file("") + assert not fh.rm.called + + +def test_default_storage_prefix_is_bucket_rooted(): + """The shipped default roots every path in a real bucket (13b F1). + + s3fs/gcsfs read the first path segment as the bucket, so a prefix without + one (the old ``org/{org_id}/...``) makes every write fail ``NoSuchBucket``. + The default must therefore stay ``unstract/agent_kv`` -- bucket ``unstract`` + (created by the MinIO dev bootstrap), directory ``agent_kv`` -- and must + match the cloud executor's ``AGENT_KV_STORAGE_DIR_PREFIX``. + """ + if "AGENT_KV_STORAGE_DIR_PREFIX" in os.environ: + pytest.skip("AGENT_KV_STORAGE_DIR_PREFIX is overridden in this environment") + assert settings.AGENT_KV_STORAGE_DIR_PREFIX == "unstract/agent_kv" + bucket, _, rest = settings.AGENT_KV_STORAGE_DIR_PREFIX.partition("/") + # Split: a composite assert cannot say WHICH half failed, and these two + # mean different things -- no bucket segment vs. a bucket with no path. + assert bucket + assert rest + + +@mock.patch.object(storage, "FileSystem") +def test_paths_are_rooted_at_the_configured_prefix(m_fs): + up = mock.Mock() + up.name = "invoice.pdf" + up.chunks.return_value = [b"x"] + with override_settings(AGENT_KV_STORAGE_DIR_PREFIX="mybucket/kv-root"): + input_ref = storage.stage_input("org1", "job1", up) + result_ref = storage.write_result("org1", "job1", {"success": True}) + assert input_ref.startswith("mybucket/kv-root/") + assert result_ref.startswith("mybucket/kv-root/") + assert input_ref == "mybucket/kv-root/org1/job1/input.pdf" + assert re.fullmatch( + r"mybucket/kv-root/org1/job1/result-[0-9a-f]{32}\.json", result_ref + ), result_ref + + +def test_storage_prefix_normalisation_matches_executor(monkeypatch): + """A sloppy operator value must resolve to the same root on both sides: + the cloud executor strips whitespace and edge slashes before keying its + OCR cache, so the backend must too, else inputs and cache split roots. + """ + import importlib + + monkeypatch.setenv("AGENT_KV_STORAGE_DIR_PREFIX", " /unstract/agent_kv/ ") + from backend.settings import base + + importlib.reload(base) + try: + assert base.AGENT_KV_STORAGE_DIR_PREFIX == "unstract/agent_kv" + monkeypatch.setenv("AGENT_KV_STORAGE_DIR_PREFIX", " ") + importlib.reload(base) + assert base.AGENT_KV_STORAGE_DIR_PREFIX == "unstract/agent_kv" + finally: + monkeypatch.delenv("AGENT_KV_STORAGE_DIR_PREFIX", raising=False) + importlib.reload(base) diff --git a/backend/agent_kv/tests/test_submit_serializer.py b/backend/agent_kv/tests/test_submit_serializer.py new file mode 100644 index 0000000000..179bd00916 --- /dev/null +++ b/backend/agent_kv/tests/test_submit_serializer.py @@ -0,0 +1,365 @@ +"""Submit-serializer rules for the table extractor. + +This deployment routes `table` and nothing else (`EXTRACTOR_ROUTES`), so every +submit built here names `table`. The request-level rules -- file type, size, +page cap, page range, timeout, the extractor-scoped wire format -- are the same +for any extractor and are exercised through that one. + +`KVOptionsSerializer` is still in the tree and still tested, but it is tested +*directly* rather than through a submit: with `kv` out of `EXTRACTOR_ROUTES` a +`kv` entry is refused at `validate_name` before any options validator runs, so +driving those rules through `SubmitSerializer` would assert nothing about them. +""" + +import json +import os +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.core.files.uploadedfile import SimpleUploadedFile # noqa: E402 + +from agent_kv.execution_serializers import ( # noqa: E402 + KVOptionsSerializer, + SubmitSerializer, +) + +FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") + +#: The table extractor's `keys`: the thing being asked for is a table, named. +TABLE_KEYS = {"target_table": "Rent roll"} + + +def _pdf_upload(name="doc.pdf"): + with open(os.path.join(FIXTURES, "two_page.pdf"), "rb") as f: + return SimpleUploadedFile(name, f.read(), content_type="application/pdf") + + +# Fields that describe the REQUEST rather than an extractor (spec §7.1). +# Anything else passed to _data() is routed into the table extractor's options, +# so each test still reads as "submit with this one thing changed". +_JOB_LEVEL = { + "file", + "extractors", + "page_start", + "page_end", + "timeout", + "tags", + "custom_data", + "webhook_url", +} + + +def _data(**over): + """Build a submit payload in the extractor-scoped wire format (§7.0).""" + keys = over.pop("keys", TABLE_KEYS) + options = {k: over.pop(k) for k in list(over) if k not in _JOB_LEVEL} + d = { + "file": _pdf_upload(), + "extractors": json.dumps([{"name": "table", "keys": keys, "options": options}]), + } + d.update(over) + return d + + +def _errs(s): + """All validation errors as one string. + + Per-extractor failures surface nested under `extractors`, so asserting on a + specific top-level key would just be asserting on DRF's nesting shape rather + than on the rejection actually happening. + """ + return str(s.errors) + + +def _defaults(m): + """Set every AGENT_KV_* attribute the serializer reads to its production default.""" + m.AGENT_KV_MAX_FILE_SIZE_MB = 50 + m.AGENT_KV_MAX_PAGES = 100 + m.AGENT_KV_MAX_SCHEMA_BYTES = 262_144 + m.AGENT_KV_MAX_CALCULATIONS_BYTES = 20_000 + m.AGENT_KV_MAX_TIMEOUT_SECONDS = 300 + m.AGENT_KV_CALCULATIONS_ENABLED = False + m.AGENT_KV_STRUCTURED_OUTPUT_ENABLED = False + + +# --------------------------------------------------------------------------- +# The routing this deployment actually ships +# --------------------------------------------------------------------------- +def test_a_kv_submit_is_refused_at_the_serializer(): + """The guard on the single most important line in this carve-out. + + `agentic_kv` is not deployed here, so nothing consumes + `celery_executor_agentic_kv`. If `kv` were routable, this submit would + return 202, dispatch into a queue with no consumer, and sit in DISPATCHED + forever -- no error at the producer, nothing in any log to find. A 400 at + the serializer is the whole difference between a clear refusal and a job + that silently never runs. + """ + s = SubmitSerializer( + data=_data( + extractors=json.dumps( + [{"name": "kv", "keys": {"total": {"description": "Grand total"}}}] + ) + ) + ) + assert not s.is_valid() + assert "unknown extractor" in _errs(s) + assert "'kv'" in _errs(s) + + +def test_table_is_the_only_supported_extractor(): + from agent_kv.execution_serializers import SUPPORTED_EXTRACTORS + + assert SUPPORTED_EXTRACTORS == ("table",) + + +# --------------------------------------------------------------------------- +# Request-level rules, exercised through the extractor this deployment serves +# --------------------------------------------------------------------------- +def test_valid_submit_counts_pages_and_defaults_its_options(): + s = SubmitSerializer(data=_data()) + assert s.is_valid(), s.errors + assert s.pages_total == 2 + entry = s.validated_data["extractors"][0] + assert entry["name"] == "table" + assert entry["keys"]["target_table"] == "Rent roll" + assert entry["options"]["number_format"] == "US" + assert entry["options"]["enable_header_mapping"] is False + + +def test_a_table_entry_without_a_target_table_is_rejected(): + """`target_table` is the engine's one required extraction parameter; the + caller's actual mistake is not naming the table. + """ + s = SubmitSerializer(data=_data(keys={})) + assert not s.is_valid() + assert "target_table" in _errs(s) + + +def test_disallowed_extension_rejected(): + bad = SimpleUploadedFile("doc.exe", b"MZ", content_type="application/x-dos") + s = SubmitSerializer(data=_data(file=bad)) + assert not s.is_valid() + assert "file" in s.errors + + +def test_oversize_file_rejected(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_MAX_FILE_SIZE_MB = 0 + s = SubmitSerializer(data=_data()) + assert not s.is_valid() + assert "file" in s.errors + + +def test_page_cap_rejected(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_MAX_PAGES = 1 + s = SubmitSerializer(data=_data()) + assert not s.is_valid() + assert "pages" in str(s.errors).lower() + + +def test_extractors_not_json_rejected(): + s = SubmitSerializer(data=_data(extractors="{not json")) + assert not s.is_valid() + assert "extractors" in s.errors + + +def test_timeout_bounds(): + s = SubmitSerializer(data=_data(timeout=301)) + assert not s.is_valid() + s2 = SubmitSerializer(data=_data(timeout=0)) + assert s2.is_valid(), s2.errors + + +def test_page_range_validation(): + s = SubmitSerializer(data=_data(page_start=5, page_end=2)) + assert not s.is_valid() + + +def test_unreadable_pdf_is_field_error_and_stream_is_rewound(): + bad = SimpleUploadedFile("doc.pdf", b"not a pdf", content_type="application/pdf") + s = SubmitSerializer(data=_data(file=bad)) + assert not s.is_valid() + assert "file" in s.errors + # The `finally: f.seek(0)` in validate() must run on the error path too, + # so a downstream reader (e.g. the view persisting the upload) still sees + # the whole stream from the start. + assert bad.tell() == 0 + assert bad.read() == b"not a pdf" + + +# --------------------------------------------------------------------------- +# Extractor-scoped wire format (spec §7.0/§7.1). These rules exist so a caller +# learns immediately that something is unsupported, rather than having the +# request quietly run as something other than what they asked for. +# --------------------------------------------------------------------------- +def test_unknown_extractor_name_rejected(): + s = SubmitSerializer( + data=_data(extractors=json.dumps([{"name": "bogus", "keys": TABLE_KEYS}])) + ) + assert not s.is_valid() + assert "unknown extractor" in _errs(s) + + +def test_multiple_extractors_rejected_until_fan_out_exists(): + """The FORMAT is fixed before launch; the fan-out execution is not built. + + Accepting two entries and running only the first would be the silent kind + of wrong -- the caller is billed for a job that ignored half the request. + """ + two = [ + {"name": "table", "keys": TABLE_KEYS}, + {"name": "table", "keys": TABLE_KEYS}, + ] + s = SubmitSerializer(data=_data(extractors=json.dumps(two))) + assert not s.is_valid() + assert "multiple extractors are not supported yet" in _errs(s) + + +def test_empty_or_non_list_extractors_rejected(): + for bad in ("[]", '{"name": "table"}', '"table"'): + s = SubmitSerializer(data=_data(extractors=bad)) + assert not s.is_valid(), bad + assert "non-empty JSON array" in _errs(s) + + +def test_unknown_option_is_rejected_not_silently_dropped(): + """DRF drops unknown fields by default; for per-extractor options that would + mean a typo'd or misaddressed knob silently changing what the job runs. + """ + s = SubmitSerializer(data=_data(number_formatt="EU")) # typo for `number_format` + assert not s.is_valid() + assert "unknown options for extractor 'table'" in _errs(s) + assert "number_formatt" in _errs(s) + + +def test_a_kv_option_on_a_table_entry_is_rejected(): + """The cross-extractor case the per-extractor serializers exist for: `qa` + is a KV pipeline knob and means nothing to the table engine. Dropped + silently, the caller would believe they had turned something off. + """ + s = SubmitSerializer(data=_data(qa=False)) + assert not s.is_valid() + assert "unknown options for extractor 'table'" in _errs(s) + assert "qa" in _errs(s) + + +def test_old_flat_format_is_no_longer_accepted(): + """Hard switch (§7.1): the pre-§7.0 shape has no alias. A caller still + sending the flat form must get a clear 400, not a job that silently ran + with default options. + """ + s = SubmitSerializer( + data={ + "file": _pdf_upload(), + "keys": json.dumps(TABLE_KEYS), + "number_format": "EU", + } + ) + assert not s.is_valid() + assert "extractors" in s.errors # the now-required field is missing + + +def test_unknown_key_on_the_extractor_entry_is_rejected(): + """`options` rejects unknowns; the entry itself must too. + + DRF drops unrecognised keys, so a near-miss like `option` (singular) for + `options` would be discarded whole: `options` defaults to {}, the job runs + with every default, and the caller gets a 202 with no sign their + configuration was ignored. + """ + s = SubmitSerializer( + data=_data( + extractors=json.dumps( + [{"name": "table", "keys": TABLE_KEYS, "option": {"number_format": "EU"}}] + ) + ) + ) + assert not s.is_valid() + assert "unknown keys on extractor entry" in _errs(s) + assert "option" in _errs(s) + + +# --------------------------------------------------------------------------- +# KVOptionsSerializer, tested directly. +# +# The class is dormant on this deployment -- `kv` is not routable, so nothing +# reaches it through a submit -- but it stays in the tree so the branch that +# carries the KV engine re-enables the extractor with one line rather than a +# content merge. Dormant and untested is how a one-line re-enable turns into a +# regression, so its rules are asserted here against the serializer itself. +# --------------------------------------------------------------------------- +def _kv_options(**over): + s = KVOptionsSerializer(data=over) + return s + + +def test_kv_options_default_qa_and_challenge_on(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + s = _kv_options() + assert s.is_valid(), s.errors + assert s.validated_data["qa"] is True + assert s.validated_data["challenge"] is True + assert s.validated_data["extraction_mode"] == "whole-doc" + + +def test_kv_calculations_rejected_when_disabled(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_CALCULATIONS_ENABLED = False + s = _kv_options(calculations="annualize rent") + assert not s.is_valid() + assert "not available" in str(s.errors) + + +def test_kv_calculations_accepted_when_enabled(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_CALCULATIONS_ENABLED = True + assert _kv_options(calculations="annualize rent").is_valid() + + +def test_kv_calculations_cap(): + # AGENT_KV_CALCULATIONS_ENABLED must be mocked True here: real settings + # default it False, so an unmocked run hits the feature-gate branch (a + # different error) instead of the byte-size cap this test is named for. + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_CALCULATIONS_ENABLED = True + s = _kv_options(calculations="x" * 30_000) + assert not s.is_valid() + assert "calculations" in str(s.errors) + assert "20000 bytes" in str(s.errors) + + +def test_kv_structured_output_rejected_when_disabled(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_STRUCTURED_OUTPUT_ENABLED = False + s = _kv_options(structured_output=True) + assert not s.is_valid() + assert "structured_output" in str(s.errors) + + +def test_kv_empty_calculations_and_false_structured_output_pass_when_disabled(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + assert _kv_options(calculations="", structured_output=False).is_valid() + + +def test_kv_unknown_option_is_rejected(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + s = _kv_options(qaa=True) # typo for `qa` + assert not s.is_valid() + assert "unknown options for extractor 'kv'" in str(s.errors) diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py new file mode 100644 index 0000000000..3e5b821114 --- /dev/null +++ b/backend/agent_kv/tests/test_submit_view.py @@ -0,0 +1,733 @@ +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.conf import settings # noqa: E402 +from django.utils import timezone # noqa: E402 +from rest_framework.test import APIRequestFactory # noqa: E402 + +from agent_kv import execution_views as ev # noqa: E402 +from agent_kv.models import AgentKVJob, AgentKVKey, JobStatus # noqa: E402 +from agent_kv.tests._factories import kv_key # noqa: E402 + + +def _post(data=None): + return APIRequestFactory().post("/agent-kv/", data or {}, format="multipart") + + +def _authed_post(data=None): + req = _post(data) + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + return req + + +# Fields describing the REQUEST rather than an extractor (spec §7.1); anything +# else an override names is routed into the kv extractor's options, so each test +# still reads as "submit with this one thing changed". +_JOB_LEVEL = { + "file", + "extractors", + "page_start", + "page_end", + "timeout", + "tags", + "custom_data", + "webhook_url", +} + + +def _valid_validated_data(**overrides): + """`SubmitSerializer.validated_data` in the extractor-scoped shape (§7.0).""" + keys = overrides.pop("keys", {"total": {"description": "Grand total"}}) + options = { + "qa": True, + "challenge": True, + "extraction_mode": "whole-doc", + "structured_output": False, + "calculations": "", + "document_class": "", + "key_notes": "", + } + options.update({k: overrides.pop(k) for k in list(overrides) if k not in _JOB_LEVEL}) + data = { + "file": mock.Mock(name="uploaded_file"), + "extractors": [{"name": "kv", "keys": keys, "options": options}], + "page_start": 1, + "page_end": None, + "timeout": 0, + "tags": [], + "custom_data": None, + "webhook_url": "", + } + data.update(overrides) + return data + + +def _mock_serializer(m_cls, **overrides): + instance = m_cls.return_value + instance.is_valid.return_value = True + instance.validated_data = _valid_validated_data(**overrides) + instance.pages_total = 3 + return instance + + +def _stamp_created_at(job, *args, **kwargs): + """save() side effect mimicking auto_now_add for a fully-mocked save(). + + A plain ``mock.patch.object(AgentKVJob, "save")`` never runs Django's real + save() machinery, so ``created_at`` (auto_now_add) is never stamped. The + view reads ``job.created_at.isoformat()`` on the 202 path, so tests that + reach it need ``autospec=True`` (to get ``self``) plus this side effect. + """ + if job.created_at is None: + job.created_at = timezone.now() + + +# --------------------------------------------------------------------------- +# 501 before anything: the agent-kv plugin probe fails first, ahead of any +# staging/dispatch/DB work. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "get_plugin", return_value=None) +@mock.patch.object(AgentKVKey, "objects") +def test_absent_plugin_501s_before_anything(m_keys, m_plugin): + m_keys.get.return_value = kv_key() + resp = ev.SubmitView.as_view()(_authed_post()) + assert resp.status_code == 501 + + +# --------------------------------------------------------------------------- +# 429: per-key rate limit refusal. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "check_key_rate", return_value=False) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_key_rate_limited_429s(m_keys, m_plugin, m_rate): + m_keys.get.return_value = kv_key() + resp = ev.SubmitView.as_view()(_authed_post()) + assert resp.status_code == 429 + assert m_rate.called + + +# --------------------------------------------------------------------------- +# 429: concurrency-slot refusal. No job row must be persisted (save() never +# called) and staging must never run. +# --------------------------------------------------------------------------- +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_concurrency_limited_429s_with_no_job_row( + m_keys, m_plugin, m_rate, m_serializer_cls, m_limiter, m_stage, m_save +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = False + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 429 + assert not m_save.called + assert not m_stage.called + + +# --------------------------------------------------------------------------- +# Staging/save failure: the concurrency slot must be released and the +# client gets a user-safe 500 — no leaked exception text, no stuck slot. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_stage_input_failure_releases_slot_and_500s_with_safe_body( + m_keys, m_plugin, m_rate, m_serializer_cls, m_stage, m_limiter +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = True + m_stage.side_effect = OSError("object store unreachable: leaked-secret-bucket-key") + + with mock.patch.object(AgentKVJob, "mark_terminal") as m_mark_terminal: + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 500 + # The internal exception text must never reach the client. + assert "leaked-secret-bucket-key" not in str(resp.data) + assert resp.data["status"] == JobStatus.FAILED.lower() + assert resp.data["error"] == "Job could not be accepted; nothing was billed." + assert "job_id" in resp.data + + # stage_input raised before job.save() ever ran: no row exists, so + # mark_terminal must not be called against a nonexistent job. + assert not m_mark_terminal.called + assert m_limiter.release.called + released_job_id = m_limiter.release.call_args.args[1] + assert released_job_id == resp.data["job_id"] + + +# --------------------------------------------------------------------------- +# Happy path: 202 with job_id/status/status_url; staging + dispatch happen; +# job is persisted. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_happy_path_returns_202_with_job_id_status_and_status_url( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_limiter, + m_stage, + m_save, + m_dispatch, +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + def _side_effect(job, *, extractor, schema, options): + job.status = JobStatus.DISPATCHED + job.dispatched_at = timezone.now() + + m_dispatch.side_effect = _side_effect + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 202 + assert {"job_id", "status", "status_url"} <= set(resp.data.keys()) + # job_id must be a real UUID string. + uuid.UUID(resp.data["job_id"]) + assert ( + resp.data["status_url"] + == f"/{settings.AGENT_KV_PATH_PREFIX}/{resp.data['job_id']}" + ) + assert resp.data["status"] == JobStatus.DISPATCHED.lower() + + assert m_limiter.check_and_acquire.called + assert m_stage.called + assert m_save.called + assert m_dispatch.called + + +# --------------------------------------------------------------------------- +# The 9-key options dict SubmitView.post builds and forwards to dispatch_job +# must map every field correctly and carry the compiled schema -- a +# dropped/typo'd key here would otherwise pass silently since nothing else +# asserts on dispatch_job's call args. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_dispatch_job_called_with_expected_options_and_schema( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_limiter, + m_stage, + m_save, + m_dispatch, +): + m_keys.get.return_value = kv_key() + keys_schema = {"total": {"description": "Grand total"}} + _mock_serializer( + m_serializer_cls, + keys=keys_schema, + qa=False, + challenge=False, + extraction_mode="per-page", + structured_output=True, + page_start=2, + page_end=5, + document_class="invoice", + key_notes="ignore footers", + calculations="annualize rent", + ) + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + ev.SubmitView.as_view()(_authed_post()) + + assert m_dispatch.called + kwargs = m_dispatch.call_args.kwargs + assert kwargs["schema"] == keys_schema + assert kwargs["options"] == { + "qa": False, + "challenge": False, + "extraction_mode": "per-page", + "structured_output": True, + "page_start": 2, + "page_end": 5, + "document_class": "invoice", + "key_notes": "ignore footers", + "calculations": "annualize rent", + } + + +# --------------------------------------------------------------------------- +# Dispatch failure: job marked FAILED via mark_terminal, concurrency slot +# released, 500 response with a user-safe (non-leaking) error message. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_dispatch_failure_marks_job_failed_releases_slot_and_500s( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_stage, + m_save, + m_dispatch, + m_mark_terminal, + m_limiter, +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = True + m_dispatch.side_effect = ev.DispatchError("broker credentials: super-secret-token") + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 500 + # The internal exception text must never reach the client. + assert "super-secret-token" not in str(resp.data) + assert resp.data["status"] == JobStatus.FAILED.lower() + assert "job_id" in resp.data + + assert m_mark_terminal.called + call = m_mark_terminal.call_args + assert call.args[2] == JobStatus.FAILED + assert call.kwargs["error"] == "Job could not be dispatched; nothing was billed." + + assert m_limiter.release.called + released_job_id = m_limiter.release.call_args.args[1] + assert released_job_id == str(call.args[0]) + + +# --------------------------------------------------------------------------- +# Belt-and-braces: a raw (non-DispatchError) exception out of dispatch_job +# must get IDENTICAL cleanup to a DispatchError — nothing may escape +# unhandled. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_dispatch_job_raising_non_dispatch_error_is_still_caught_and_cleaned_up( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_stage, + m_save, + m_dispatch, + m_mark_terminal, + m_limiter, +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = True + m_dispatch.side_effect = RuntimeError("unexpected: leaked-secret-abc") + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 500 + assert "leaked-secret-abc" not in str(resp.data) + assert resp.data["status"] == JobStatus.FAILED.lower() + assert resp.data["error"] == "Job could not be dispatched; nothing was billed." + + assert m_mark_terminal.called + assert m_mark_terminal.call_args.args[2] == JobStatus.FAILED + assert m_limiter.release.called + + +# --------------------------------------------------------------------------- +# End-to-end regression for the widened dispatch.py try: a raw RuntimeError +# from the platform-key lookup deep inside the REAL dispatch_job must still +# result in mark_terminal + release + a safe 500 at the view layer. +# --------------------------------------------------------------------------- +@mock.patch("agent_kv.dispatch._platform_api_key") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_platform_key_lookup_failure_inside_real_dispatch_is_caught_end_to_end( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_stage, + m_save, + m_limiter, + m_mark_terminal, + m_platform_key, +): + # ev.dispatch_job is intentionally left real here — only the platform-key + # lookup deep inside it is mocked to raise, proving the widened + # dispatch.py try (Fix 2a) plus the view's cleanup (Fix 2b) work together. + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + m_platform_key.side_effect = RuntimeError("platform db down: leaked-secret-xyz") + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 500 + assert "leaked-secret-xyz" not in str(resp.data) + assert resp.data["status"] == JobStatus.FAILED.lower() + assert resp.data["error"] == "Job could not be dispatched; nothing was billed." + + assert m_mark_terminal.called + assert m_limiter.release.called + + +# --------------------------------------------------------------------------- +# timeout=0: the wait branch must never run (no sleep, no polling, no +# attempt to import the Task-9 result_payload module). +# --------------------------------------------------------------------------- +@mock.patch("agent_kv.execution_views.time.sleep") +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_timeout_zero_returns_immediately_without_polling( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_limiter, + m_stage, + m_save, + m_dispatch, + m_sleep, +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls, timeout=0) + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + with mock.patch.object(AgentKVJob, "refresh_from_db") as m_refresh: + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 202 + assert not m_sleep.called + assert not m_refresh.called + + +# --------------------------------------------------------------------------- +# Sync-wait regression (spec §7.3 controller ruling on task-9-report.md +# concern 3): if the job fails during the wait window, the inline +# synchronous response must still be a 200 carrying the failure body -- not +# a 404. Before the fix, ``result_payload`` raised ``JobNotFound`` for any +# terminal-but-not-COMPLETED job (blank ``result_ref``), which escaped this +# view as an unhandled 404. Uses the real (unmocked) ``result_payload`` so +# the fix is exercised end-to-end, not just at the unit level. +# --------------------------------------------------------------------------- +@mock.patch("agent_kv.execution_views.time.sleep") +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_sync_wait_returns_200_with_failure_body_when_job_fails_mid_wait( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_limiter, + m_stage, + m_save, + m_dispatch, + m_sleep, +): + m_keys.get.return_value = kv_key() + _mock_serializer(m_serializer_cls, timeout=5) + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + def _refresh_side_effect(job, *args, **kwargs): + job.status = JobStatus.FAILED + job.error = "LLM provider timed out" + + with mock.patch.object( + AgentKVJob, "refresh_from_db", autospec=True, side_effect=_refresh_side_effect + ): + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 200 + assert resp.data == { + "success": False, + "status": "failed", + "error": "LLM provider timed out", + } + assert not m_sleep.called + + +# --------------------------------------------------------------------------- +# Subscription admission (§6.6, following the API deployment path). +# +# Deployments are billed-gated by cloud's SubscriptionMiddleware, which resolves +# the org from the URL (/deployment/api/{org_name}/...). Agent-KV's URL carries +# no org segment -- the org lives in the Bearer key -- so that middleware +# resolves org_id=None for these requests and lets every one of them through. +# The gate is therefore invoked here, after key validation. +# --------------------------------------------------------------------------- +def _key_with_org(slug="acme-slug", pk=42): + """A key whose FK pk and org slug are deliberately DIFFERENT values. + + `Subscription.organization_id` is a CharField holding the slug; the FK pk is + an int. Making them differ is what lets the tests below detect the wrong one + being passed -- with a single shared value the assertion would pass either + way and the gate could silently never match a subscription row. + """ + from account_v2.models import Organization # noqa: PLC0415 + + key = AgentKVKey(name="k", is_active=True) + key.organization = Organization(id=pk, organization_id=slug) + return key + + +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(AgentKVKey, "objects") +def test_subscription_denial_is_returned_verbatim_and_starts_no_work( + m_keys, m_rate, m_serializer_cls, m_limiter, m_stage, m_save +): + """A 402 from the gate must reach the client unchanged -- same status and + body an API deployment returns for the same subscription state -- and must + stop the request before any billable work, slot or row. + """ + from django.http import HttpResponse # noqa: PLC0415 + + m_keys.get.return_value = _key_with_org() + _mock_serializer(m_serializer_cls) + denial = HttpResponse(b'{"errors": "Trial period expired."}', status=402) + gate = mock.Mock() + gate.check.return_value = denial + + with mock.patch.object( + ev, "get_plugin", return_value={"module": object(), "service_class": lambda: gate} + ): + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 402 + assert not m_save.called + assert not m_stage.called + assert not m_limiter.check_and_acquire.called + + +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(AgentKVKey, "objects") +def test_subscription_gate_is_passed_the_org_slug_not_the_fk_pk( + m_keys, m_rate, m_serializer_cls, m_limiter +): + """The gate must receive `key.organization.organization_id` (the slug that + `Subscription.organization_id` is keyed on), NOT `key.organization_id` (the + Organization FK primary key). + + Passing the pk matches no subscription row, and the shared policy reads "no + row" as "nothing to enforce" -- so the gate would admit every request while + looking fully wired. This test is the only thing standing between that + one-attribute slip and a billing gate that never fires. + """ + m_keys.get.return_value = _key_with_org(slug="acme-slug", pk=42) + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = False # stop early; gate already ran + gate = mock.Mock() + gate.check.return_value = None + + with mock.patch.object( + ev, "get_plugin", return_value={"module": object(), "service_class": lambda: gate} + ): + ev.SubmitView.as_view()(_authed_post()) + + assert gate.check.called + passed_org = gate.check.call_args.args[0] + assert passed_org == "acme-slug", f"gate got {passed_org!r}, expected the org slug" + assert passed_org != 42 + + +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_plugin_without_a_service_class_still_proceeds( + m_keys, m_plugin, m_rate, m_serializer_cls, m_limiter +): + """A cloud build predating the gate exposes no `service_class`. The view + must degrade to today's behaviour rather than 500 -- the capability probe + above already 501s an engine-less deployment, so this only covers the + version skew between an older plugin and a newer backend. + """ + m_keys.get.return_value = _key_with_org() + _mock_serializer(m_serializer_cls) + m_limiter.check_and_acquire.return_value = False + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 429 # reached the concurrency limiter, not a 500 + + +# --------------------------------------------------------------------------- +# Integration: the REAL serializer through the REAL view. +# +# Every other test in this module patches SubmitSerializer and feeds the view a +# `validated_data` built by _valid_validated_data() -- so the view is asserted +# against a shape this file makes up, not the one the serializer actually +# emits. The serializer suite has the mirror-image blind spot: it validates +# payloads and never runs a view. Both pass even if the two disagree, which is +# precisely the seam an extractor-scoped wire format (§7.0) moves. +# +# This test builds a real multipart upload, runs it through the real serializer +# and the real view, and asserts what reaches dispatch_job -- the frozen +# OSS<->cloud contract on the far side. +# --------------------------------------------------------------------------- +def _real_multipart_post(extractors, **job_level): + import json as _json # noqa: PLC0415 + import os as _os # noqa: PLC0415 + + from django.core.files.uploadedfile import SimpleUploadedFile # noqa: PLC0415 + + fixture = _os.path.join(_os.path.dirname(__file__), "fixtures", "two_page.pdf") + with open(fixture, "rb") as fh: + upload = SimpleUploadedFile("doc.pdf", fh.read(), content_type="application/pdf") + payload = {"file": upload, "extractors": _json.dumps(extractors), **job_level} + req = APIRequestFactory().post("/agent-kv/", payload, format="multipart") + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + return req + + +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) # autospec: see _stamp_created_at +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_real_serializer_through_real_view_reaches_dispatch_intact_for_table( + m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch +): + """The table path's version of the test above -- and the exact contract + that already broke once: an earlier version had the consumer reading + `target_table` off the top level of `executor_params` while the producer + nested it under `schema`, which would have failed every table job, and it + survived two rounds of unit tests on both sides because each side was + internally consistent on its own. Only a test that runs the real + serializer AND the real view together, and inspects what lands at + `dispatch_job`, can catch that kind of drift. + """ + m_keys.get.return_value = kv_key() + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + schema = {"target_table": "Rent rolls"} + + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": schema, + "options": {"instructions": "skip totals"}, + } + ], + ) + ) + + assert resp.status_code == 202, resp.data + kwargs = m_dispatch.call_args.kwargs + assert kwargs["extractor"] == "table" + # Nested under `schema`, matching exactly what the cloud executor reads + # (`params["schema"]["target_table"]") -- NOT hoisted to the top level. + assert kwargs["schema"] == {"target_table": "Rent rolls"} + assert kwargs["options"]["instructions"] == "skip totals" + + # The job row records which extractor it ran (execution_views.py:142) -- + # asserted here since nothing else in this suite reads it back. + job = m_dispatch.call_args.args[0] + assert job.extractor == "table" + + +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(AgentKVKey, "objects") +def test_real_serializer_rejects_the_old_flat_shape_with_400(m_keys, m_plugin, m_rate): + """End-to-end proof of the hard switch: a caller on the pre-§7.0 format gets + a 400 from the real stack, not a job that quietly ran with defaults. + """ + import json as _json # noqa: PLC0415 + import os as _os # noqa: PLC0415 + + from django.core.files.uploadedfile import SimpleUploadedFile # noqa: PLC0415 + + m_keys.get.return_value = kv_key() + fixture = _os.path.join(_os.path.dirname(__file__), "fixtures", "two_page.pdf") + with open(fixture, "rb") as fh: + upload = SimpleUploadedFile("doc.pdf", fh.read(), content_type="application/pdf") + req = APIRequestFactory().post( + "/agent-kv/", + { + "file": upload, + "keys": _json.dumps({"total": {"description": "T"}}), + "qa": "false", + }, + format="multipart", + ) + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + + resp = ev.SubmitView.as_view()(req) + + assert resp.status_code == 400 + assert "extractors" in str(resp.data) diff --git a/backend/agent_kv/tests/test_sweeps.py b/backend/agent_kv/tests/test_sweeps.py new file mode 100644 index 0000000000..be1cf6b4f0 --- /dev/null +++ b/backend/agent_kv/tests/test_sweeps.py @@ -0,0 +1,646 @@ +"""Agent-KV never-dispatched sweep and TTL cleanup internal endpoints +(spec §5.4, task-14-brief.md). + +Same mock-based style as test_internal_views.py: no real DB, every +``AgentKVJob.objects`` (and, for TTLCleanupView, ``delete_job_files``) call +is mocked and its arguments/ordering are asserted directly. That is also +the mechanism for the two predicate-shaped guarantees in the brief that a +mock can't literally execute against a database: + +* "non-expired jobs untouched" -- proven by asserting the exact + ``expires_at__lt`` filter kwarg the candidate query is built with. +* "blank-ref rows excluded / a second run over the same set is a no-op" -- + proven by asserting the exact ``Q(input_ref__gt="") | Q(result_ref__gt="")`` + filter the candidate query is built with: a row TTLCleanupView just + blanked no longer satisfies that predicate, so it drops out of the next + call's candidate set. + +These tests exercise the two-phase sweep and TTL-cleanup logic through the +(now-thin) ``SweepView``/``TTLCleanupView`` -- the logic itself lives in +``agent_kv.maintenance`` (moved there so the ``agent_kv_sweep``/ +``agent_kv_ttl_cleanup`` management commands can share it), which is why +``delete_job_files`` is patched on the ``maintenance`` module below rather +than on ``internal_views``. +""" + +import os +import uuid +from datetime import timedelta +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.conf import settings # noqa: E402 +from django.db.models import Q # noqa: E402 +from django.utils import timezone # noqa: E402 +from rest_framework.test import APIRequestFactory # noqa: E402 + +from agent_kv import internal_views as iv # noqa: E402 +from agent_kv import maintenance # noqa: E402 +from agent_kv.models import AgentKVJob, JobStatus # noqa: E402 + + +def _post(path, body=None): + return APIRequestFactory().post(path, body or {}, format="json") + + +# --------------------------------------------------------------------------- +# SweepView +# --------------------------------------------------------------------------- +# +# SweepView runs two independent phases per call -- never-dispatched PENDING +# jobs, then stuck DISPATCHED/RUNNING jobs -- each its own +# filter().order_by()[:500] chain against the same (mocked) AgentKVJob.objects +# manager. ``_wire_sweep_phases`` gives each phase call its own Mock object +# (via `side_effect`, keyed on call ORDER: phase 1 first, then phase 2) so a +# test can assert on -- and control the candidates of -- one phase without the +# other phase's identical-shaped chain aliasing it. + + +def _wire_sweep_phases(m_objects, never_dispatched=(), stuck=()): + """Wire the sweep's two phase queries. + + Phase 2 is `filter(status__in=...).filter(Q(...) | Q(...))` -- two levels, + because the age test is an OR: `dispatched_at < cutoff` OR + `dispatched_at IS NULL AND created_at < cutoff`. A `__lt` filter alone can + never match a NULL (SQL `NULL < x` is not true), so without that second arm + a DISPATCHED/RUNNING row with a NULL `dispatched_at` hangs forever. + """ + phase1_qs = mock.MagicMock() + phase1_qs.order_by.return_value.__getitem__.return_value = list(never_dispatched) + phase2_status_qs = mock.MagicMock() + phase2_qs = phase2_status_qs.filter.return_value + phase2_qs.order_by.return_value.__getitem__.return_value = list(stuck) + m_objects.filter.side_effect = [phase1_qs, phase2_status_qs] + return phase1_qs, phase2_qs, phase2_status_qs + + +# (1) the never-dispatched-phase candidate query is exactly PENDING + older +# than the grace + dispatched_at IS NULL -- assert the filter kwargs directly +# (this IS the proof that "only PENDING+old+undispatched" are swept; nothing +# else does a real DB round trip in this suite) -- and, per the task-14-review +# ruling, oldest-created-first and capped at 500: an unbounded queryset would +# load a whole infra-incident backlog into memory and hold the request open +# through it, exactly when the sweep matters most. +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_queries_pending_older_than_grace_and_undispatched( + m_objects, m_mark_terminal +): + frozen_now = timezone.now() + phase1_qs, phase2_qs, phase2_status_qs = _wire_sweep_phases(m_objects) + + with mock.patch.object(timezone, "now", return_value=frozen_now): + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 0, "timed_out": 0} + filter_kwargs = m_objects.filter.call_args_list[0].kwargs + assert filter_kwargs["status"] == JobStatus.PENDING + assert filter_kwargs["dispatched_at__isnull"] is True + assert filter_kwargs["created_at__lt"] == frozen_now - timedelta( + seconds=settings.AGENT_KV_SWEEP_GRACE_SECONDS + ) + phase1_qs.order_by.assert_called_once_with("created_at") + phase1_qs.order_by.return_value.__getitem__.assert_called_once_with( + slice(None, 500, None) + ) + assert not m_mark_terminal.called + + +# (2) each never-dispatched candidate is terminalized via +# mark_terminal(FAILED, "Job was never dispatched") -- the guarded write, not +# a raw .update(). +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_terminalizes_each_candidate_as_failed_never_dispatched( + m_objects, m_mark_terminal, m_release +): + job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + _wire_sweep_phases(m_objects, never_dispatched=[job]) + m_mark_terminal.return_value = True + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + m_mark_terminal.assert_called_once_with( + job.id, "org1", JobStatus.FAILED, error="Job was never dispatched" + ) + + +# (3) a job the guard actually wins gets its concurrency slot released, with +# its own org id and job id. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_releases_the_concurrency_slot_of_each_swept_job( + m_objects, m_mark_terminal, m_release +): + job = AgentKVJob(id=uuid.uuid4(), organization_id="org7") + _wire_sweep_phases(m_objects, never_dispatched=[job]) + m_mark_terminal.return_value = True + + iv.SweepView.as_view()(_post("/x")) + + m_release.assert_called_once_with("org7", str(job.id)) + + +# (4) a candidate that LOSES the mark_terminal guard (raced to terminal by +# a concurrent finalize/cancel/duplicate sweep between the candidate read +# and the guarded write) is not counted as swept and its slot is not +# released here -- whichever path won the race already released it. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_count_reflects_guard_outcomes_not_candidate_count( + m_objects, m_mark_terminal, m_release +): + won_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + lost_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + _wire_sweep_phases(m_objects, never_dispatched=[won_job, lost_job]) + m_mark_terminal.side_effect = [True, False] + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 1, "timed_out": 0} + m_release.assert_called_once_with("org1", str(won_job.id)) + + +# (5) no candidates -> {"swept": 0, "timed_out": 0}, and no terminalize/ +# release side effects. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_with_no_candidates_is_a_pure_noop(m_objects, m_mark_terminal, m_release): + _wire_sweep_phases(m_objects) + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 0, "timed_out": 0} + assert not m_mark_terminal.called + assert not m_release.called + + +# --------------------------------------------------------------------------- +# SweepView -- stuck-job (phase 2) terminalizer (Fix 8) +# --------------------------------------------------------------------------- + + +# (5a) the stuck-job-phase candidate query is exactly +# DISPATCHED/RUNNING + dispatched_at older than the stuck grace, ordered +# oldest-dispatched-first and capped at 500 -- same batch-safety rationale as +# phase 1. +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_stuck_sweep_queries_dispatched_and_running_older_than_stuck_grace( + m_objects, m_mark_terminal +): + frozen_now = timezone.now() + phase1_qs, phase2_qs, phase2_status_qs = _wire_sweep_phases(m_objects) + + with mock.patch.object(timezone, "now", return_value=frozen_now): + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 0, "timed_out": 0} + filter_kwargs = m_objects.filter.call_args_list[1].kwargs + assert set(filter_kwargs["status__in"]) == {JobStatus.DISPATCHED, JobStatus.RUNNING} + cutoff = frozen_now - timedelta(seconds=settings.AGENT_KV_STUCK_JOB_GRACE_SECONDS) + # The age test is an OR, and the second arm is load-bearing: a `__lt` + # filter alone can never match a NULL `dispatched_at`, so such a row would + # be invisible to this phase AND to phase 1 (which requires PENDING), and + # would hang non-terminal forever. A row with no dispatch time falls back + # to `created_at` -- the only timestamp it has. + (age_q,), age_kwargs = phase2_status_qs.filter.call_args + assert age_kwargs == {} + assert age_q == ( + Q(dispatched_at__lt=cutoff) | Q(dispatched_at__isnull=True, created_at__lt=cutoff) + ) + phase2_qs.order_by.assert_called_once_with("dispatched_at") + phase2_qs.order_by.return_value.__getitem__.assert_called_once_with( + slice(None, 500, None) + ) + assert not m_mark_terminal.called + + +# (5b) each stuck candidate is terminalized via mark_terminal(FAILED, +# "Job timed out") -- distinct error text from the never-dispatched phase. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_stuck_sweep_terminalizes_each_candidate_as_failed_timed_out( + m_objects, m_mark_terminal, m_release +): + job = AgentKVJob(id=uuid.uuid4(), organization_id="org1", status=JobStatus.RUNNING) + _wire_sweep_phases(m_objects, stuck=[job]) + m_mark_terminal.return_value = True + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 0, "timed_out": 1} + m_mark_terminal.assert_called_once_with( + job.id, "org1", JobStatus.FAILED, error="Job timed out" + ) + + +# (5c) a stuck job the guard wins gets its concurrency slot released. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_stuck_sweep_releases_the_concurrency_slot_of_each_timed_out_job( + m_objects, m_mark_terminal, m_release +): + job = AgentKVJob(id=uuid.uuid4(), organization_id="org9", status=JobStatus.DISPATCHED) + _wire_sweep_phases(m_objects, stuck=[job]) + m_mark_terminal.return_value = True + + iv.SweepView.as_view()(_post("/x")) + + m_release.assert_called_once_with("org9", str(job.id)) + + +# (5d) a stuck candidate that LOSES the guard (raced to terminal by a +# concurrent finalize/cancel/duplicate sweep) is not counted as timed_out and +# its slot is not released here. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_stuck_sweep_count_reflects_guard_outcomes_not_candidate_count( + m_objects, m_mark_terminal, m_release +): + won_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + lost_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + _wire_sweep_phases(m_objects, stuck=[won_job, lost_job]) + m_mark_terminal.side_effect = [True, False] + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 0, "timed_out": 1} + m_release.assert_called_once_with("org1", str(won_job.id)) + + +# (5e) the two phases' counts are independent -- a hit in one phase doesn't +# affect the other's count, and both run on the same call. +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_reports_both_phase_counts_independently( + m_objects, m_mark_terminal, m_release +): + never_dispatched_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + stuck_job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + _wire_sweep_phases( + m_objects, never_dispatched=[never_dispatched_job], stuck=[stuck_job] + ) + m_mark_terminal.return_value = True + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"swept": 1, "timed_out": 1} + + +# --------------------------------------------------------------------------- +# TTLCleanupView +# --------------------------------------------------------------------------- + + +class _Lane: + """One of run_ttl_cleanup's two candidate queries, recording how it was used. + + A real object rather than a ``Mock``: the lane is sorted and then SLICED, + and a Mock whose ``__getitem__`` is wrapped to record the slice ends up + calling itself (the wrapper re-enters the same mock), which is how the + first version of this helper recursed instead of asserting. + """ + + def __init__(self, rows): + self.rows = list(rows) + self.order_by_args = None + self.slice = None + + def order_by(self, *args): + self.order_by_args = args + return self + + def __getitem__(self, sl): + self.slice = sl + return self.rows[sl] + + +class _Lanes: + """Stand-in for the narrowed expired queryset, dispatching to either lane. + + run_ttl_cleanup narrows the expired set once and queries it TWICE -- + ``cleanup_failed_at__isnull=False`` (retries, a reserved slice) and + ``...=True`` (new expirations, the remainder). A plain ``Mock`` hands back + the same child for both calls regardless of arguments, so the two lanes + would yield identical rows and every job would be processed twice. + """ + + def __init__(self, retries, fresh): + self.retry = _Lane(retries) + self.fresh = _Lane(fresh) + self.filter_kwargs: list[dict] = [] + + def filter(self, *_args, **kwargs): + self.filter_kwargs.append(kwargs) + is_null = kwargs.get("cleanup_failed_at__isnull") + return self.retry if is_null is False else self.fresh + + +def _ttl_lanes(m_objects, *, retries=(), fresh=()): + """Wire a mocked ``AgentKVJob.objects`` to both TTL-cleanup lanes.""" + lanes = _Lanes(retries, fresh) + m_objects.filter.return_value.filter.return_value = lanes + return lanes + + +# (6) the candidate query is exactly expires_at < now AND (non-blank +# input_ref OR non-blank result_ref), capped at 500 across both lanes. +# This is what proves BOTH "non-expired untouched" (the expires_at__lt +# half) and "blank-ref rows excluded / second run is a no-op" (the Q half: +# a row TTLCleanupView just blanked no longer satisfies `__gt=""`). +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_queries_expired_jobs_with_a_nonblank_ref(m_objects, m_delete): + frozen_now = timezone.now() + lanes = _ttl_lanes(m_objects) + + with mock.patch.object(timezone, "now", return_value=frozen_now): + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"cleaned": 0, "retained": 0} + assert m_objects.filter.call_args_list[0].kwargs == {"expires_at__lt": frozen_now} + (q_arg,), q_kwargs = m_objects.filter.return_value.filter.call_args + assert q_kwargs == {} + assert q_arg == (Q(input_ref__gt="") | Q(result_ref__gt="")) + # Two lanes off that one narrowed set: retries first, then new expirations. + assert lanes.filter_kwargs == [ + {"cleanup_failed_at__isnull": False}, + {"cleanup_failed_at__isnull": True}, + ] + assert not m_delete.called + + +# (6b) each lane sorts by a SINGLE named column ascending -- never +# `cleanup_failed_at NULLS FIRST`. A btree index is NULLS LAST ascending, so +# that ordering could not use the (cleanup_failed_at, expires_at) index at all +# and Postgres had to sort every matching expired row before applying the +# limit, work that grows with the backlog. Splitting the query removed it. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_lanes_sort_by_a_plain_ascending_column(m_objects, m_delete): + lanes = _ttl_lanes(m_objects) + + iv.TTLCleanupView.as_view()(_post("/x")) + + assert lanes.retry.order_by_args == ("cleanup_failed_at",) + assert lanes.fresh.order_by_args == ("expires_at",) + + +# (6c) with nothing to retry, the fresh lane still gets the WHOLE batch -- the +# reserve is a cap on the retry lane, not a permanent tax on the normal case. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_fresh_lane_gets_the_whole_batch_when_nothing_is_retrying( + m_objects, m_delete +): + lanes = _ttl_lanes(m_objects) + + iv.TTLCleanupView.as_view()(_post("/x")) + + assert lanes.retry.slice == slice(None, 100, None) + assert lanes.fresh.slice == slice(None, 500, None) + + +# (6d) THE fix for the second starvation direction. With the retry lane full, +# the fresh lane shrinks by exactly that many, so the two together never exceed +# the batch cap -- and crucially the retry lane is served FIRST, so a steady +# stream of new expirations can no longer push retries out of every batch and +# leave their files in storage indefinitely. Both orderings tried before this +# starved one side: oldest-expiry-first starved fresh work, NULLS FIRST starved +# retries. +@mock.patch.object(maintenance, "delete_job_files", return_value=["input_ref"]) +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_reserves_capacity_for_retries_under_a_flood_of_new_expiries( + m_objects, m_delete +): + retries = [ + AgentKVJob(id=uuid.uuid4(), input_ref=f"r{i}", cleanup_failed_at=timezone.now()) + for i in range(100) + ] + fresh = [AgentKVJob(id=uuid.uuid4(), input_ref=f"f{i}") for i in range(500)] + lanes = _ttl_lanes(m_objects, retries=retries, fresh=fresh) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + assert lanes.retry.slice == slice(None, 100, None) + assert lanes.fresh.slice == slice(None, 400, None) + # Every retry was attempted even though 500 fresh rows were queued behind + # them, and the batch cap still held: 100 + 400 == 500. + assert m_delete.call_count == 500 + assert resp.data == {"cleaned": 0, "retained": 500} + + +# (7) files are deleted BEFORE the refs are blanked -- order matters: a +# delete failure must not blank a ref pointing at a file that's still there. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_deletes_files_before_blanking_refs(m_objects, m_delete): + job_id = uuid.uuid4() + job = AgentKVJob( + id=job_id, input_ref="org/o/j/input.pdf", result_ref="org/o/j/result.json" + ) + m_delete.return_value = ["input_ref", "result_ref"] + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[job]) + + manager = mock.Mock() + manager.attach_mock(m_delete, "delete_job_files") + manager.attach_mock(m_qs.update, "update") + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"cleaned": 1, "retained": 0} + assert [c[0] for c in manager.mock_calls] == ["delete_job_files", "update"] + m_delete.assert_called_once_with(job) + + +# (8) both refs are blanked via `.update()` (not `job.save()`), targeting +# exactly this job's row, and the row itself is left in place (no .delete() +# call is ever made on the queryset). +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_blanks_both_refs_for_the_job_row(m_objects, m_delete): + job_id = uuid.uuid4() + job = AgentKVJob(id=job_id, input_ref="org/o/j/input.pdf", result_ref="") + m_delete.return_value = ["input_ref", "result_ref"] + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[job]) + + iv.TTLCleanupView.as_view()(_post("/x")) + + assert m_objects.filter.call_args_list[-1].kwargs == {"id": job_id} + m_qs.update.assert_called_once_with( + input_ref="", result_ref="", cleanup_failed_at=None + ) + assert not m_qs.delete.called + + +# (9) `cleaned` counts jobs actually processed this call, across multiple +# candidates. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_returns_count_of_jobs_cleaned(m_objects, m_delete): + job1 = AgentKVJob(id=uuid.uuid4(), input_ref="a", result_ref="") + job2 = AgentKVJob(id=uuid.uuid4(), input_ref="", result_ref="b") + m_delete.return_value = ["input_ref", "result_ref"] + m_qs = m_objects.filter.return_value + _ttl_lanes( + m_objects, + fresh=[ + job1, + job2, + ], + ) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + assert resp.data == {"cleaned": 2, "retained": 0} + assert m_delete.call_count == 2 + assert m_qs.update.call_count == 2 + + +# (10) no candidates -> {"cleaned": 0}, nothing deleted or updated. This is +# also the concrete shape of a "second run over the same set" once every +# candidate from the first run has had its refs blanked: the (mocked) +# candidate query simply returns nothing. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_with_no_candidates_is_a_pure_noop(m_objects, m_delete): + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[]) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data == {"cleaned": 0, "retained": 0} + assert not m_delete.called + assert not m_qs.update.called + + +# (10b) THE case test (7)'s comment claims ("a delete failure must not blank a +# ref pointing at a file that's still there") but which nothing actually +# asserted until the Greptile review: test (7) only pinned the ORDER of the two +# calls, and ordering is irrelevant when the update blanks both refs regardless +# of what the delete returned. Before the fix this test fails -- `update` is +# called with both refs blanked and the still-present result file loses its only +# handle, since the candidate query below matches only rows with a non-blank +# ref. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_keeps_the_ref_whose_file_delete_failed(m_objects, m_delete): + job_id = uuid.uuid4() + job = AgentKVJob( + id=job_id, input_ref="org/o/j/input.pdf", result_ref="org/o/j/result.json" + ) + # Input gone, result delete raised inside delete_job_files. + m_delete.return_value = ["input_ref"] + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[job]) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + # Only the confirmed-gone ref is blanked; result_ref is NOT in the update, + # so the row still matches `result_ref__gt=""` and the next pass retries. + (), kwargs = m_qs.update.call_args + assert kwargs["input_ref"] == "" + assert "result_ref" not in kwargs + # Stamped so the nulls-first ordering pushes this row behind all never-failed + # work on the next tick -- retried, but unable to block the backlog. + assert kwargs["cleanup_failed_at"] is not None + # Not counted as cleaned -- it is unfinished, and `retained` is what makes a + # permanently-failing backlog visible instead of silently draining to zero. + assert resp.data == {"cleaned": 0, "retained": 1} + + +# (10c) nothing confirmed gone -> no ref is blanked, but the failure is still +# recorded. Skipping the write entirely would leave cleanup_failed_at NULL, and +# a NULL sorts FIRST under the candidate ordering -- so the row would hold the +# head of every batch indefinitely, which is the starvation this ordering +# exists to prevent. +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_blanks_no_ref_but_still_stamps_the_failure(m_objects, m_delete): + job = AgentKVJob( + id=uuid.uuid4(), input_ref="org/o/j/input.pdf", result_ref="org/o/j/r.json" + ) + m_delete.return_value = [] + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[job]) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + # No ref is blanked -- but the failure IS stamped, or the row would sort + # nulls-first forever and keep its place at the head of every batch. + (), kwargs = m_qs.update.call_args + assert kwargs == {"cleanup_failed_at": mock.ANY} + assert kwargs["cleanup_failed_at"] is not None + assert resp.data == {"cleaned": 0, "retained": 1} + + +# (10d) a row that failed before and succeeds now must have its stale failure +# marker cleared. It drops out of the candidate filter anyway (both refs blank), +# so this matters for the audit trail and for anything reading +# cleanup_failed_at as "currently failing" rather than "failed once". +@mock.patch.object(maintenance, "delete_job_files") +@mock.patch.object(AgentKVJob, "objects") +def test_ttl_cleanup_clears_a_stale_failure_marker_on_success(m_objects, m_delete): + job = AgentKVJob( + id=uuid.uuid4(), + input_ref="org/o/j/input.pdf", + result_ref="org/o/j/result.json", + cleanup_failed_at=timezone.now(), + ) + m_delete.return_value = ["input_ref", "result_ref"] + m_qs = m_objects.filter.return_value + _ttl_lanes(m_objects, fresh=[job]) + + resp = iv.TTLCleanupView.as_view()(_post("/x")) + + m_qs.update.assert_called_once_with( + input_ref="", result_ref="", cleanup_failed_at=None + ) + assert resp.data == {"cleaned": 1, "retained": 0} + + +# --------------------------------------------------------------------------- +# URL wiring +# --------------------------------------------------------------------------- + + +# (11) regression pin for the frozen paths (spec Interfaces block): the +# PG-scheduler/reaper periodic mechanism calls these exact URLs, so a +# dropped/renamed include in internal_base_urls.py must fail loudly here +# rather than 404 in prod. +def test_frozen_sweep_and_ttl_cleanup_urls_resolve_to_the_right_views(): + from django.urls import resolve + + sweep = resolve("/internal/v1/agent-kv/sweep/") + assert sweep.func.cls is iv.SweepView + + ttl_cleanup = resolve("/internal/v1/agent-kv/ttl-cleanup/") + assert ttl_cleanup.func.cls is iv.TTLCleanupView diff --git a/backend/agent_kv/tests/test_table_extractor_routing.py b/backend/agent_kv/tests/test_table_extractor_routing.py new file mode 100644 index 0000000000..44c012f5b9 --- /dev/null +++ b/backend/agent_kv/tests/test_table_extractor_routing.py @@ -0,0 +1,241 @@ +"""Routing a `table` extractor entry to the table executor. + +The wire format is extractor-scoped (§7.0): `extractors: [{name, keys, options}]`. +v1 accepts exactly one entry, but WHICH one is now a choice, so the job row has +to record it -- status and result used to key everything under the hardcoded +`kv` name, which would have filed a table job's output under the wrong +extractor. +""" + +import json +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from agent_kv.constants import ( # noqa: E402 + EXTRACTOR_ROUTES, + STAGE_NAMES_BY_EXTRACTOR, + TABLE_EXTRACTOR_NAME, + V1_EXTRACTOR_NAME, +) +from agent_kv.execution_serializers import ( # noqa: E402 + _OPTIONS_SERIALIZERS, + SUPPORTED_EXTRACTORS, + ExtractorSerializer, +) +from agent_kv.models import AgentKVJob, JobStatus # noqa: E402 + + +def test_table_is_the_only_supported_extractor(): + """`kv` is out of EXTRACTOR_ROUTES because this deployment ships no + `agentic_kv` plugin, so nothing drains `celery_executor_agentic_kv`. + SUPPORTED_EXTRACTORS is derived from that table, so the omission is what + turns a `kv` submit into a 400 rather than a 202 for a job that never runs. + """ + assert set(SUPPORTED_EXTRACTORS) == {TABLE_EXTRACTOR_NAME} + assert V1_EXTRACTOR_NAME not in EXTRACTOR_ROUTES + + +def test_every_supported_extractor_has_a_route_stage_list_and_options_serializer(): + """A supported extractor with no route dispatches nowhere; with no stage + list its progress is recorded and then filtered out of the status + document; with no options serializer, a submit for it raises an + uncaught KeyError at `_OPTIONS_SERIALIZERS[data["name"]]` instead of a + validation error -- `validate_name` already passed it, since + `SUPPORTED_EXTRACTORS` is derived from `EXTRACTOR_ROUTES`. + """ + for name in SUPPORTED_EXTRACTORS: + assert name in EXTRACTOR_ROUTES, name + assert name in STAGE_NAMES_BY_EXTRACTOR, name + assert name in _OPTIONS_SERIALIZERS, name + + +def test_the_table_route_targets_the_existing_executor(): + """R1: the queue is derived from the executor name, and + celery_executor_agentic_table is already wired. A new executor name here + would be accepted, dispatch silently, and never drain. + """ + executor, operation = EXTRACTOR_ROUTES[TABLE_EXTRACTOR_NAME] + assert executor == "agentic_table" + assert operation == "table_extract_api" + + +def test_a_table_entry_validates_with_its_own_options(): + data = { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "options": {"instructions": "skip totals rows"}, + } + s = ExtractorSerializer(data=data) + assert s.is_valid(), s.errors + assert s.validated_data["options"]["instructions"] == "skip totals rows" + + +def test_a_table_entry_requires_a_target_table(): + s = ExtractorSerializer(data={"name": "table", "keys": {}, "options": {}}) + assert not s.is_valid() + assert "target_table" in json.dumps(s.errors) + + +def test_kv_options_are_rejected_on_a_table_entry(): + """The whole point of per-extractor options: an option aimed at the wrong + extractor must not be silently dropped. + """ + s = ExtractorSerializer( + data={"name": "table", "keys": {"target_table": "T"}, "options": {"qa": True}} + ) + assert not s.is_valid() + assert "qa" in json.dumps(s.errors) + + +def test_a_kv_entry_is_refused_by_name(): + """Options scoping is moot for `kv` here: `validate_name` refuses the entry + before any options validator runs. What matters is that the refusal names + the extractor, so a caller sending a KV payload to this deployment learns + why rather than seeing a schema complaint. + """ + s = ExtractorSerializer( + data={ + "name": "kv", + "keys": {"total": {"description": "d"}}, + "options": {"qa": False}, + } + ) + assert not s.is_valid() + assert "unknown extractor" in json.dumps(s.errors) + assert "kv" in json.dumps(s.errors) + + +def _job(extractor=TABLE_EXTRACTOR_NAME, **overrides): + """An unsaved job with an unsaved org -- this suite's established pattern. + + `test_dispatch.py` builds jobs exactly this way and the whole `agent_kv` + suite touches no database (there is no `django_db` marker anywhere in it). + Assigning an unsaved `Organization` with an explicit PK caches it on the + job, so `job.organization` never hits the DB, and the slug is deliberately + different from the PK so a test can tell which one the code reached for. + """ + from account_v2.models import Organization + + job = AgentKVJob(id=uuid.uuid4(), input_ref="org/o/agent_kv/j/input.pdf") + job.organization = Organization(id=7, organization_id="org_slug_1") + job.extractor = extractor + job.pages_total = 3 + for key, value in overrides.items(): + setattr(job, key, value) + return job + + +def test_the_kv_route_constants_survive_for_re_enablement(): + """`kv` is unroutable, not deleted. The executor/operation pair, the stage + list and the options serializer all stay so the branch carrying the engine + re-enables the extractor by restoring one dict entry -- not by merging + content back into the files it rewrites most. + """ + from agent_kv.constants import ( + EXECUTOR_NAME, + OPERATION_KV_EXTRACT, + STAGE_NAMES_BY_EXTRACTOR, + ) + from agent_kv.execution_serializers import _OPTIONS_SERIALIZERS + + assert EXECUTOR_NAME == "agentic_kv" + assert OPERATION_KV_EXTRACT == "kv_extract" + assert V1_EXTRACTOR_NAME in STAGE_NAMES_BY_EXTRACTOR + assert V1_EXTRACTOR_NAME in _OPTIONS_SERIALIZERS + + +def test_the_status_document_keys_stages_by_the_extractor_that_ran(): + from agent_kv.execution_views import _status_document + + job = _job( + stage="table_extraction", + stages={"table_extraction": {"status": "done", "seconds": 12.5}}, + ) + doc = _status_document(job) + + assert TABLE_EXTRACTOR_NAME in doc["extractors"] + assert V1_EXTRACTOR_NAME not in doc["extractors"] + # R7: recorded AND visible. StageReportView persists any stage name the + # executor sends, but _status_document filters through the extractor's own + # list -- with the KV list this array would come back empty. + assert doc["extractors"][TABLE_EXTRACTOR_NAME]["stages"] == [ + {"name": "table_extraction", "status": "done", "seconds": 12.5} + ] + + +def test_a_kv_job_still_reports_its_kv_stages(): + from agent_kv.execution_views import _status_document + + job = _job( + extractor=V1_EXTRACTOR_NAME, + stage="extraction", + stages={ + "document_processing": {"status": "done"}, + "extraction": {"status": "running"}, + }, + ) + doc = _status_document(job) + + names = [s["name"] for s in doc["extractors"][V1_EXTRACTOR_NAME]["stages"]] + assert names == ["document_processing", "extraction"] + + +def test_the_result_payload_keys_by_the_extractor_that_ran(): + from agent_kv.execution_views_result import result_payload + + job = _job( + status=JobStatus.COMPLETED, + result_ref="ref.json", + usage_summary={"pages": 3}, + ) + with mock.patch( + "agent_kv.execution_views_result.read_result", + return_value={"tables": [{"unit": "A1"}]}, + ): + payload = result_payload(job) + + assert payload["success"] is True + assert payload["status"] == "completed" + assert payload["extractors"][TABLE_EXTRACTOR_NAME] == {"tables": [{"unit": "A1"}]} + assert payload["usage_summary"]["by_extractor"][TABLE_EXTRACTOR_NAME] == {"pages": 3} + assert V1_EXTRACTOR_NAME not in payload["extractors"] + + +def test_the_extractor_column_defaults_to_kv(): + """The migration's default is the historical truth, not a guess: before + this column existed the API accepted exactly one extractor, always `kv`. + """ + assert AgentKVJob().extractor == V1_EXTRACTOR_NAME + + +def test_a_job_whose_extractor_has_no_stage_list_reports_no_stages(caplog): + """A retired extractor name with surviving rows must not 500 `GET status`. + + `_status_document` used to subscript `STAGE_NAMES_BY_EXTRACTOR`, so such a + row raised `KeyError` on status while `GET result` kept working -- the + result payload keys by `job.extractor` without consulting that table. An + empty stage array is the honest answer for an extractor this build no + longer knows how to describe. + """ + from agent_kv.execution_views import _status_document + + job = _job( + extractor="retired_extractor", + stage="something", + stages={"something": {"status": "done"}}, + ) + + with caplog.at_level("WARNING", logger="agent_kv.execution_views"): + doc = _status_document(job) + + assert doc["extractors"]["retired_extractor"]["stages"] == [] + assert doc["status"] == job.status.lower() + assert "retired_extractor" in caplog.text diff --git a/backend/agent_kv/tests/test_validate_view.py b/backend/agent_kv/tests/test_validate_view.py new file mode 100644 index 0000000000..3d6b5ffcba --- /dev/null +++ b/backend/agent_kv/tests/test_validate_view.py @@ -0,0 +1,130 @@ +import json +import os +import uuid +from unittest import mock + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from rest_framework.test import APIRequestFactory # noqa: E402 + +from agent_kv import execution_views as ev # noqa: E402 +from agent_kv.models import AgentKVKey # noqa: E402 +from agent_kv.tests._factories import kv_key # noqa: E402 + + +def _authed(method="post", path="/agent-kv/validate", data=None): + if data is None: + data = {} + req = getattr(APIRequestFactory(), method)( + path, + json.dumps(data) if data else "", + content_type="application/json", + ) + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + return req + + +# --------------------------------------------------------------------------- +# (1) Valid schema -> returns valid:true with correct counts +# --------------------------------------------------------------------------- +# These three mock `check_key_rate` explicitly, like test_submit_view.py does. +# They previously relied on the limiter's ambient behaviour: it failed OPEN on +# any Redis error, so with no Redis in the unit lane it returned True and the +# tests passed for a reason unrelated to what they assert. The limiter now +# fails CLOSED (a Sentinel failover used to remove the concurrency and rate +# ceilings at once while the API kept accepting billable work), so an +# unreachable Redis is a 429 and these became 429 == 200. The mock is the right +# fix either way: these are schema-validation tests, and the rate limiter +# should not be in their path at all. +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(AgentKVKey, "objects") +def test_validate_valid_schema_returns_counts(m_keys, m_rate): + m_keys.get.return_value = kv_key() + schema = { + "quotation_number": {"description": "The quote number", "required": True}, + "customer": {"name": {"description": "Bill-to name"}}, + "line_items": { + "description": "One row per line", + "_key": "sku", + "_array": { + "sku": {"description": "SKU"}, + "total": {"description": "Line total", "format": "currency"}, + }, + }, + "_constraints": ["count('line_items') >= 1"], + } + req = _authed(data={"keys": schema}) + + resp = ev.ValidateView.as_view()(req) + + assert resp.status_code == 200 + assert resp.data["valid"] is True + assert "leaves" in resp.data + assert "arrays" in resp.data + assert "constraints" in resp.data + assert isinstance(resp.data["leaves"], int) + assert isinstance(resp.data["arrays"], int) + assert isinstance(resp.data["constraints"], int) + + +# --------------------------------------------------------------------------- +# (2) Invalid schema -> returns valid:false with error message verbatim +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(AgentKVKey, "objects") +def test_validate_invalid_schema_returns_error(m_keys, m_rate): + m_keys.get.return_value = kv_key() + # Pass a non-dict as top-level schema + req = _authed(data={"keys": "not a dict"}) + + resp = ev.ValidateView.as_view()(req) + + assert resp.status_code == 200 + assert resp.data["valid"] is False + assert "error" in resp.data + assert resp.data["error"] == "Top-level key schema must be a JSON object" + + +# --------------------------------------------------------------------------- +# (3) No key (no Authorization header) -> 403 Forbidden +# --------------------------------------------------------------------------- +def test_validate_no_key_returns_403(): + req = APIRequestFactory().post("/agent-kv/validate", data={}) + resp = ev.ValidateView.as_view()(req) + assert resp.status_code == 403 + + +# --------------------------------------------------------------------------- +# (4) Over rate limit -> 429 +# --------------------------------------------------------------------------- +@mock.patch("agent_kv.execution_views.check_key_rate", return_value=False) +@mock.patch.object(AgentKVKey, "objects") +def test_validate_over_rate_limit_returns_429(m_keys, m_check_rate): + # A concrete id, because the rate limiter is keyed on it. + key = kv_key(id=uuid.uuid4()) + m_keys.get.return_value = key + req = _authed(data={"keys": {}}) + + resp = ev.ValidateView.as_view()(req) + + assert resp.status_code == 429 + + +# --------------------------------------------------------------------------- +# (5) Missing 'keys' in request body -> 400 +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(AgentKVKey, "objects") +def test_validate_missing_keys_returns_400(m_keys, m_rate): + m_keys.get.return_value = kv_key() + req = _authed(data={}) + + resp = ev.ValidateView.as_view()(req) + + assert resp.status_code == 400 + assert resp.data["detail"] == "body must include 'keys'" diff --git a/backend/agent_kv/urls.py b/backend/agent_kv/urls.py new file mode 100644 index 0000000000..206b7f0d95 --- /dev/null +++ b/backend/agent_kv/urls.py @@ -0,0 +1,23 @@ +from django.urls import path + +from agent_kv.views import AgentKVKeyViewSet + +urlpatterns = [ + path( + "keys/", + AgentKVKeyViewSet.as_view({"get": "list", "post": "create"}), + name="agent_kv_key_list", + ), + path( + "keys//", + AgentKVKeyViewSet.as_view( + {"get": "retrieve", "patch": "partial_update", "delete": "destroy"} + ), + name="agent_kv_key_detail", + ), + path( + "keys//rotate/", + AgentKVKeyViewSet.as_view({"post": "rotate"}), + name="agent_kv_key_rotate", + ), +] diff --git a/backend/agent_kv/views.py b/backend/agent_kv/views.py new file mode 100644 index 0000000000..466a4c7eee --- /dev/null +++ b/backend/agent_kv/views.py @@ -0,0 +1,55 @@ +import uuid + +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.serializers import BaseSerializer + +from agent_kv.models import AgentKVKey +from agent_kv.permissions import IsOrganizationAdmin +from agent_kv.serializers import AgentKVKeySerializer, AgentKVKeyWriteSerializer + + +class AgentKVKeyViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated, IsOrganizationAdmin] + + def get_queryset(self): + # No explicit org filter here — the global ``OrganizationFilterBackend`` + # (in DEFAULT_FILTER_BACKENDS) scopes every DRF operation by the + # current org's ``organization`` FK on this model. + return AgentKVKey.objects.all() + + def get_serializer_class(self): + if self.action in ("create", "update", "partial_update"): + return AgentKVKeyWriteSerializer + return AgentKVKeySerializer + + def perform_create(self, serializer: BaseSerializer) -> None: + serializer.save(created_by=self.request.user) + + def create(self, request, *args, **kwargs): + """Respond with the read serializer, not the write one (DRF's default). + + ``AgentKVKeyWriteSerializer`` (used to validate the request, per + ``get_serializer_class``) only carries ``name``/``description``/ + ``is_active`` -- the standard ``CreateModelMixin.create`` would echo + that same serializer back, meaning the caller who just created a key + would never see ``id`` or the raw ``key`` value itself. Both are + server-generated and this is the only response that will ever carry + the plaintext key (list/retrieve return it too today, but rotate is + the only other place a caller can *see* a fresh one) -- mirrors + ``GlobalApiDeploymentKeyViewSet.create``. + """ + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + response_serializer = AgentKVKeySerializer(serializer.instance) + return Response(response_serializer.data, status=status.HTTP_201_CREATED) + + @action(detail=True, methods=["post"]) + def rotate(self, request, pk=None): + key_obj = self.get_object() + key_obj.key = uuid.uuid4() + key_obj.save(update_fields=["key", "modified_at"]) + return Response(AgentKVKeySerializer(key_obj).data) diff --git a/backend/backend/base_urls.py b/backend/backend/base_urls.py index 0b763e3c59..30d5d790da 100644 --- a/backend/backend/base_urls.py +++ b/backend/backend/base_urls.py @@ -37,6 +37,8 @@ f"{settings.API_DEPLOYMENT_PATH_PREFIX}/pipeline/", include("pipeline_v2.public_api_urls"), ), + # Agent-KV product API (views own auth; prefix is middleware-whitelisted) + path(f"{settings.AGENT_KV_PATH_PREFIX}/", include("agent_kv.execution_urls")), path("", include("health.urls")), # Internal API for worker communication path("internal/", include("backend.internal_base_urls")), diff --git a/backend/backend/internal_base_urls.py b/backend/backend/internal_base_urls.py index 30d4c8df22..ab184c8fb0 100644 --- a/backend/backend/internal_base_urls.py +++ b/backend/backend/internal_base_urls.py @@ -246,6 +246,12 @@ def test_middleware_debug(request): include("api_v2.internal_urls"), name="api_deployments_internal", ), + # Agent-KV stage-report and finalize APIs for the cloud executor + path( + "v1/agent-kv/", + include("agent_kv.internal_urls"), + name="agent_kv_internal", + ), # Platform configuration and settings APIs path( "v1/platform-settings/", diff --git a/backend/backend/settings/base.py b/backend/backend/settings/base.py index aa03b577ae..f90416969c 100644 --- a/backend/backend/settings/base.py +++ b/backend/backend/settings/base.py @@ -204,6 +204,48 @@ def get_required_setting(setting_key: str, default: str | None = None) -> str | os.environ.get("API_DEPLOYMENT_RATE_LIMIT_LOCK_BLOCKING_TIMEOUT", 5) ) +# Agent-KV API +AGENT_KV_PATH_PREFIX = os.environ.get("AGENT_KV_PATH_PREFIX", "agent-kv") +# Bucket-rooted object-store root for staged inputs/results, mirroring +# WORKFLOW_EXECUTION_DIR_PREFIX / API_EXECUTION_DIR_PREFIX: the FIRST segment is +# the bucket (s3fs/gcsfs treat it that way), so it must already exist. MUST match +# the cloud executor's AGENT_KV_STORAGE_DIR_PREFIX -- the executor keys its OCR +# cache under the same root. +AGENT_KV_STORAGE_DIR_PREFIX = ( + os.environ.get("AGENT_KV_STORAGE_DIR_PREFIX", "unstract/agent_kv").strip().strip("/") + or "unstract/agent_kv" +) # same normalisation as the cloud executor's reader (whitespace, edge slashes) +AGENT_KV_MAX_FILE_SIZE_MB = int(os.environ.get("AGENT_KV_MAX_FILE_SIZE_MB", 50)) +AGENT_KV_MAX_PAGES = int(os.environ.get("AGENT_KV_MAX_PAGES", 100)) +AGENT_KV_MAX_CALCULATIONS_BYTES = int( + os.environ.get("AGENT_KV_MAX_CALCULATIONS_BYTES", 20_000) +) +AGENT_KV_MAX_SCHEMA_BYTES = int(os.environ.get("AGENT_KV_MAX_SCHEMA_BYTES", 262_144)) +AGENT_KV_RESULT_TTL_DAYS = int(os.environ.get("AGENT_KV_RESULT_TTL_DAYS", 7)) +AGENT_KV_MAX_TIMEOUT_SECONDS = int(os.environ.get("AGENT_KV_MAX_TIMEOUT_SECONDS", 300)) +AGENT_KV_CONCURRENT_LIMIT = int(os.environ.get("AGENT_KV_CONCURRENT_LIMIT", 5)) +AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE = int( + os.environ.get("AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE", 60) +) +# When Redis is unreachable, do requests proceed? Default False = fail CLOSED +# (429). Both Agent-KV limiters used to fail open implicitly, inside an `except` +# block, so a Sentinel failover removed the concurrency ceiling and the per-key +# rate ceiling at once while the API kept accepting billable LLM work. Fail-open +# remains available as a deliberate, visible operational choice. +AGENT_KV_LIMITER_FAIL_OPEN = ( + os.environ.get("AGENT_KV_LIMITER_FAIL_OPEN", "false").lower() == "true" +) +AGENT_KV_SWEEP_GRACE_SECONDS = int(os.environ.get("AGENT_KV_SWEEP_GRACE_SECONDS", 3600)) +AGENT_KV_STUCK_JOB_GRACE_SECONDS = int( + os.environ.get("AGENT_KV_STUCK_JOB_GRACE_SECONDS", 21600) +) +AGENT_KV_CALCULATIONS_ENABLED = ( + os.environ.get("AGENT_KV_CALCULATIONS_ENABLED", "false").lower() == "true" +) +AGENT_KV_STRUCTURED_OUTPUT_ENABLED = ( + os.environ.get("AGENT_KV_STRUCTURED_OUTPUT_ENABLED", "false").lower() == "true" +) + DB_NAME = os.environ.get("DB_NAME", "unstract_db") DB_USER = os.environ.get("DB_USER", "unstract_dev") @@ -454,6 +496,7 @@ def filter(self, record): "dashboard_metrics", "platform_api", "global_api_deployment_key", + "agent_kv", ) TENANT_APPS = [] @@ -978,10 +1021,16 @@ def filter(self, record): "/static", ] WHITELISTED_PATHS = [f"/{PATH_PREFIX}{PATH}" for PATH in WHITELISTED_PATHS_LIST] -# White lists workflow-api-deployment path. This also covers the deployment MCP -# server, which hangs off the same URL and authenticates with the deployment's -# own API key rather than a session. -WHITELISTED_PATHS.append(f"/{API_DEPLOYMENT_PATH_PREFIX}") +WHITELISTED_PATHS.extend( + [ + # workflow-api-deployment. Also covers the deployment MCP server, which + # hangs off the same URL and authenticates with the deployment's own API + # key rather than a session. + f"/{API_DEPLOYMENT_PATH_PREFIX}", + # Agent-KV public API: bearer-key auth happens in the views. + f"/{AGENT_KV_PATH_PREFIX}", + ] +) # Whitelisting health check API WHITELISTED_PATHS.append("/health") diff --git a/backend/backend/urls_v2.py b/backend/backend/urls_v2.py index 4b9636a35c..6e66e9561a 100644 --- a/backend/backend/urls_v2.py +++ b/backend/backend/urls_v2.py @@ -32,6 +32,7 @@ path("", include("feature_flag.urls")), path("workflow/", include("workflow_manager.urls")), path("platform/", include("platform_settings_v2.urls")), + path("agent-kv/", include("agent_kv.urls")), path("api/", include("api_v2.urls")), path("usage/", include("usage_v2.urls")), path("notifications/", include("notification_v2.urls")), diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c1ca5b9ed4..82124f2d5f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -42,6 +42,7 @@ dependencies = [ "django-filter>=24.3", "httpx>=0.27.0", # Hence required to add all indirect local dependencies too here. + "unstract-agent-kv-schema", "unstract-connectors", "unstract-core", "unstract-filesystem", @@ -90,6 +91,7 @@ deploy = [ ] [tool.uv.sources] +unstract-agent-kv-schema = { path = "../unstract/agent-kv-schema", editable = true } unstract-connectors = { path = "../unstract/connectors", editable = true } unstract-core = { path = "../unstract/core", editable = true } unstract-filesystem = { path = "../unstract/filesystem", editable = true } diff --git a/backend/sample.env b/backend/sample.env index ca5456167d..c32b815e46 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -186,6 +186,33 @@ API_DEPLOYMENT_RATE_LIMIT_LOCK_TIMEOUT=2 # Redis lock blocking timeout (in seconds) - how long to wait to acquire lock API_DEPLOYMENT_RATE_LIMIT_LOCK_BLOCKING_TIMEOUT=5 +# Agent-KV API (backend/backend/settings/base.py) — see docs/agent-kv-api.md +# --------------------------------------------------------------------------- +AGENT_KV_PATH_PREFIX=agent-kv # Public URL prefix (whitelisted past tenant middleware) +AGENT_KV_MAX_FILE_SIZE_MB=50 # Submit-time file size cap (MB) +AGENT_KV_MAX_PAGES=100 # Pre-OCR page cap for PDFs/images; also the post-OCR cap the engine enforces +AGENT_KV_MAX_CALCULATIONS_BYTES=20000 # Byte cap on the optional `calculations` field +AGENT_KV_MAX_SCHEMA_BYTES=262144 # Byte cap on the raw `keys` JSON schema (256 KiB) +AGENT_KV_RESULT_TTL_DAYS=7 # Result/input retention window, stamped at submit time +AGENT_KV_MAX_TIMEOUT_SECONDS=300 # Upper bound on the submit `timeout` (sync-wait) field +AGENT_KV_CONCURRENT_LIMIT=5 # Per-org concurrent in-flight job cap +AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE=60 # Per-key request rate limit (submit + validate) +AGENT_KV_SWEEP_GRACE_SECONDS=3600 # Age before a never-dispatched PENDING job is swept +AGENT_KV_STUCK_JOB_GRACE_SECONDS=21600 # Age (from dispatch) before a stuck DISPATCHED/RUNNING job is force-failed (6h) +AGENT_KV_CALCULATIONS_ENABLED=false # Gates the submit `calculations` field; engine can't run it yet +AGENT_KV_STRUCTURED_OUTPUT_ENABLED=false # Gates the submit `structured_output` field; engine can't run it yet + +# Bucket-rooted object-store root for staged inputs/results +# ({AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}/…), like WORKFLOW_EXECUTION_DIR_PREFIX. +# The FIRST segment is the BUCKET (s3fs/gcsfs treat it so) and must already exist — +# the MinIO dev bootstrap creates `unstract`. MUST match the cloud executor's +# AGENT_KV_STORAGE_DIR_PREFIX: the executor keys its OCR cache under the same root. +AGENT_KV_STORAGE_DIR_PREFIX="unstract/agent_kv" + +# Object storage credentials for those files — same shape as +# WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS; required, no default. +AGENT_KV_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' + # Default user auth credentials DEFAULT_AUTH_USERNAME= DEFAULT_AUTH_PASSWORD= diff --git a/backend/uv.lock b/backend/uv.lock index 5f7fe242e7..3978095974 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -3687,6 +3687,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/13/b688a905653871b10b4ff0403c2ff562c17a0bd50be0d44324f3c85ca48f/ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a", size = 45988 }, ] +[[package]] +name = "unstract-agent-kv-schema" +version = "0.1.0" +source = { editable = "../unstract/agent-kv-schema" } + [[package]] name = "unstract-backend" version = "0.0.1" @@ -3720,6 +3725,7 @@ dependencies = [ { name = "social-auth-app-django" }, { name = "social-auth-core" }, { name = "stripe" }, + { name = "unstract-agent-kv-schema" }, { name = "unstract-connectors" }, { name = "unstract-core" }, { name = "unstract-filesystem" }, @@ -3787,6 +3793,7 @@ requires-dist = [ { name = "social-auth-app-django", specifier = "==5.3.0" }, { name = "social-auth-core", specifier = "==4.4.2" }, { name = "stripe", specifier = ">=12.5.1" }, + { name = "unstract-agent-kv-schema", editable = "../unstract/agent-kv-schema" }, { name = "unstract-connectors", editable = "../unstract/connectors" }, { name = "unstract-core", editable = "../unstract/core" }, { name = "unstract-filesystem", editable = "../unstract/filesystem" }, diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index e89ca0e19d..50c8d8fad4 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -74,6 +74,7 @@ services: - traefik.http.routers.backend.rule=Host(`frontend.unstract.localhost`) && (PathPrefix(`/api/v1`) || PathPrefix(`/deployment`) || PathPrefix(`/public`)) - traefik.http.services.backend.loadbalancer.server.port=8000 + # Celery Flower celery-flower: <<: *host_gateway image: unstract/backend:${VERSION} @@ -543,7 +544,13 @@ services: - APPLICATION_NAME=unstract-worker-pg-ide-callback - WORKER_BARRIER_BACKEND=pg - WORKER_PG_QUEUE_CONSUMER_WORKER_TYPE=ide_callback - - WORKER_PG_QUEUE_CONSUMER_QUEUE=ide_callback + # agent_kv_callback carries the Agent-KV terminal callbacks (finalize: + # persist the result, delete the staged input, release the concurrency + # slot, fire the webhook). Without it a job runs to success and then + # sits in RUNNING forever -- the executor's continuation is enqueued + # and nothing drains it. The Celery ide_callback worker has carried + # both queues since the callbacks landed; its PG twin did not. + - WORKER_PG_QUEUE_CONSUMER_QUEUE=ide_callback,agent_kv_callback - WORKER_PG_QUEUE_CONSUMER_HEALTH_PORT=8090 - WORKER_PG_QUEUE_CONSUMER_CONCURRENCY=${PG_IDE_CALLBACK_CONCURRENCY:-2} # Log publisher — see the note on the backend service (UN-4046). diff --git a/docker/sample.env b/docker/sample.env index 47905773da..39155d8ba7 100644 --- a/docker/sample.env +++ b/docker/sample.env @@ -99,6 +99,55 @@ SCHEDULER_TASK_TIME_LIMIT=1800 # Scheduler timeout (30 minutes) LOG_CONSUMER_AUTOSCALE=2,1 # Max 2, min 1 workers LOG_CONSUMER_TASK_TIME_LIMIT=600 # Log processing timeout (10 minutes) +# IDE Callback Worker (worker-ide-callback) — Agent-KV terminal callbacks +# --------------------------------------------------------------------------- +# The ide_callback worker also owns agent_kv_complete/agent_kv_error +# (Agent-KV job finalize callbacks, spec §5.3) on their own queue, +# `agent_kv_callback`, alongside its existing `ide_callback` queue. +# unstract/backend's agent_kv.dispatch dispatches those tasks straight to +# that queue name, so worker-ide-callback must consume both queues or +# Agent-KV jobs will dispatch but never finalize. +# +# NOTE: unlike CALLBACK/FILE_PROCESSING/etc. above, this worker's queue list +# is NOT overridable from this file — there is no CELERY_QUEUES_IDE_CALLBACK +# var wired into workers/run-worker-docker.sh's queue resolution. The queue +# list is a hardcoded default in that script (WORKER_QUEUES["ide_callback"]), +# already set to "ide_callback,agent_kv_callback" there. Nothing to set here; +# if you ever need a different set for this worker, edit that script's +# default directly (or run-worker.sh's equivalent for non-docker dev runs) — +# there's no env-var lever for it today. + +# Agent-KV API (backend/backend/settings/base.py) — see docs/agent-kv-api.md +# --------------------------------------------------------------------------- +# Django app settings for the Agent-KV extraction API (unstract/backend's +# `agent_kv` app). Consumed by the Django backend process, not by +# docker-compose itself — mirror these into backend/.env (backend/sample.env) +# for a real deployment; listed here per the Agent-KV env-sample convention. +AGENT_KV_PATH_PREFIX=agent-kv # Public URL prefix (whitelisted past tenant middleware) +AGENT_KV_MAX_FILE_SIZE_MB=50 # Submit-time file size cap (MB) +AGENT_KV_MAX_PAGES=100 # Pre-OCR page cap for PDFs/images +AGENT_KV_MAX_CALCULATIONS_BYTES=20000 # Byte cap on the optional `calculations` field +AGENT_KV_MAX_SCHEMA_BYTES=262144 # Byte cap on the raw `keys` JSON schema (256 KiB) +AGENT_KV_RESULT_TTL_DAYS=7 # Result/input retention window, stamped at submit time +AGENT_KV_MAX_TIMEOUT_SECONDS=300 # Upper bound on the submit `timeout` (sync-wait) field +AGENT_KV_CONCURRENT_LIMIT=5 # Per-org concurrent in-flight job cap +AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE=60 # Per-key request rate limit (submit + validate) +AGENT_KV_SWEEP_GRACE_SECONDS=3600 # Age before a never-dispatched PENDING job is swept +AGENT_KV_STUCK_JOB_GRACE_SECONDS=21600 # Age (from dispatch) before a stuck DISPATCHED/RUNNING job is force-failed (6h) +AGENT_KV_CALCULATIONS_ENABLED=false # Gates the submit `calculations` field; engine can't run it yet +AGENT_KV_STRUCTURED_OUTPUT_ENABLED=false # Gates the submit `structured_output` field; engine can't run it yet + +# Bucket-rooted object-store root for staged inputs/results +# ({AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}/…), like WORKFLOW_EXECUTION_DIR_PREFIX. +# The FIRST segment is the BUCKET (s3fs/gcsfs treat it so) and must already exist — +# the MinIO dev bootstrap creates `unstract`. MUST match the cloud executor's +# AGENT_KV_STORAGE_DIR_PREFIX: the executor keys its OCR cache under the same root. +AGENT_KV_STORAGE_DIR_PREFIX="unstract/agent_kv" + +# Object storage credentials for those files — same shape as +# WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS; required, no default. +AGENT_KV_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' + # Worker Circuit Breaker Settings CIRCUIT_BREAKER_FAILURE_THRESHOLD=5 CIRCUIT_BREAKER_RECOVERY_TIMEOUT=60 diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md new file mode 100644 index 0000000000..a3d17308af --- /dev/null +++ b/docs/agent-kv-api.md @@ -0,0 +1,1011 @@ +# Agent-KV API + +Agent-KV is Unstract's agentic key-value extraction product: upload a document plus a +`keys.json`-style extraction schema, get back a job id, poll a verbose agent-centric +status document, and fetch a rich result (record + per-field QA/challenger audit +trail). + +This document describes the API **as built in this repo** — the OSS scaffold (routes, +validation, dispatch, job lifecycle, key management). The document, `keys.json` schema, +and OCR/LLM extraction pipeline itself are described only insofar as they shape the +contract; the extraction engine ships as a cloud executor plugin (see +[§11, 501 behavior](#11-when-the-engine-is-unavailable-501-behavior) below). + +Design background: `docs/superpowers/specs/2026-08-28-agent-kv-api-design.md` (referred +to as "the spec" throughout this document; section numbers below are spec section +numbers). This document records what shipped, including a handful of places where the +implementation is more specific than — or diverges from — the spec's sketch. Those are +called out explicitly rather than silently reconciled. + +## 1. Authentication + +There are two independent auth schemes on two different URL prefixes: + +| Surface | Prefix | Auth | +|---|---|---| +| Public execution API (submit/status/result/cancel/validate) | `/agent-kv/…` (top-level, not under the tenant subfolder) | `Authorization: Bearer ` — a dedicated per-organization key, validated by the view itself | +| Key management (create/list/rotate/revoke) | `{PATH_PREFIX}/unstract/…/agent-kv/keys/…` (tenant-scoped, alongside Platform API key management) | Session auth, `IsOrganizationAdmin` only | + +The execution prefix (`AGENT_KV_PATH_PREFIX`, default `agent-kv`) sits **outside** the +tenant-subfolder/organization-middleware chain — it is added to `WHITELISTED_PATHS` in +`backend/backend/settings/base.py` — because a caller authenticates with an API key, not +a session/org header. Every view under it therefore owns its own auth via the +`AgentKVKeyValidator.validate_api_key` decorator (`backend/agent_kv/key_validator.py`), +which: + +1. Requires `Authorization: Bearer ` where `` is a UUID. +2. Looks up an active (`is_active=True`) `AgentKVKey` by that UUID. +3. Injects the resolved key object as `agent_kv_key` into the view kwargs, which every + subsequent lookup filters by (`organization_id=agent_kv_key.organization_id`) — this + is what makes a job in another org indistinguishable from an unknown job id (both + 404, see [Status](#4-status--get-agent-kvjob_id)). + +**Drift from the spec:** spec §6.1 describes this as "every endpoint 401s without a +valid key." The implementation returns **403** (`api_v2.exceptions.Forbidden`, reused +as-is rather than adding a new exception class) for both a missing/malformed +`Authorization` header and an unknown/inactive key. This is exercised directly by +`backend/agent_kv/tests/test_auth.py` and `test_job_views.py::test_all_job_views_401_without_key` +(the test name predates the code's actual status; both assert `403`). Document the code: +**403, not 401**, for any auth failure on the public prefix. + +Key management (`backend/agent_kv/views.py`, `AgentKVKeyViewSet`) is a standard DRF +`ModelViewSet` scoped by the global `OrganizationFilterBackend` and gated by +`IsOrganizationAdmin` (`backend/agent_kv/permissions.py`). Fields returned: +`id, name, description, key, is_active, created_at`; write-side fields are +`name, description, is_active` only (`key` and `id` are server-generated). Routes +(`backend/agent_kv/urls.py`, mounted at `agent-kv/` under +`backend/backend/urls_v2.py`): + +``` +GET/POST .../agent-kv/keys/ +GET/PATCH/DELETE .../agent-kv/keys// +POST .../agent-kv/keys//rotate/ # replaces `key` with a new UUID +``` + +Key storage follows the same conventions as Platform API key management (D5) — +plaintext-at-rest UUID keys, not hashed-at-rest; that hardening is explicitly deferred, +not a v1 gap unique to this feature. + +## 2. The extraction schema (`keys` field) + +The `keys` submit field — spec-called "keys.json" — is a JSON object describing what to +extract. It compiles through `unstract.agent_kv_schema.compile_schema` +(`unstract/agent-kv-schema/src/unstract/agent_kv_schema/`), the **single schema compiler +in the system**: the OSS API validates with it at submit time and at `/validate`, and +the cloud engine imports the same package to execute — the schema language cannot drift +between validation and execution by construction (spec §5.1). + +### Node types + +A schema node is one of three shapes, decided structurally: + +- **Interior node** — every value is itself an object → its keys are child nodes, and + nesting continues (dotted path, e.g. `vendor.address.city`). +- **Leaf node** — no value is an object → defined by scalar attributes: + + | Attribute | Required | Meaning | + |---|---|---| + | `description` | **yes** | Prompted to the extractor; also fed to the breadcrumb-prefixed `effective_description`. | + | `format` | no (default `string`) | `string` \| `number` \| `currency` \| `date` \| `enum:v1,v2,…` \| `regex:` \| any other free-text hint. `number`/`currency` parse via a strip-then-`float()` check; `date` accepts a fixed set of common formats; `enum` matches case-insensitively; `regex` is `re.fullmatch`. | + | `required` | no (default `false`) | Completeness is checked separately from format. | + | `aliases` | no (default `[]`) | Alternate names/labels to help the extractor recognize the field. | + | `multivalued` | no (default `false`) | Value is a comma-separated string; each element is format-checked independently. | + + A node mixing object values and scalar attributes is a **compile-time error** + ("mixes object children and scalar attributes"), as is a leaf with unknown attribute + keys or a missing `description`. + +- **Array node** — has an `_array` key → a flat (non-nested) row schema: + + ```json + "line_items": { + "description": "One row per invoice line", + "_key": "sku", + "_array": { + "sku": {"description": "SKU"}, + "total": {"description": "Line total", "format": "currency"} + } + } + ``` + + `_array`'s value is itself a flat object of leaf columns (row-local paths, not + dotted). `description` is optional context; `_key` (optional) names a column used for + row identity in downstream scoring, otherwise rows are matched positionally. A + nested array inside an array's row schema is rejected at compile time ("not supported + in P8a") — arrays are single-level only in v1. + + `_dedup` (optional, default `true`) controls whether rows identical in **every** + extracted column are collapsed to one. Leave it on for documents where + layout-preserving OCR repeats a row once per replicate column — that is what it + exists for. Set it to `false` when your documents can genuinely contain two + identical rows: + + ```json + "line_items": { + "_dedup": false, + "_array": { + "description": {"description": "Line description"}, + "total": {"description": "Line total", "format": "currency"} + } + } + ``` + + With `_dedup: true` an invoice listing the same item twice comes back with one + row, so the row count — and any `_constraints` aggregate over those rows — is + wrong. The default is `true` only because flipping it would break the OCR corpus + the extractor was built against; it is not the safer value for every document. + Must be a JSON boolean: `"false"` as a string is rejected rather than silently + read as true. + +### Constraints (`_constraints`) + +An optional top-level `_constraints` key: a list of string expressions checked against +the extracted record post-hoc. Each expression is parsed and statically validated +against an AST allowlist before it is ever accepted — no arbitrary code execution: +boolean/comparison/arithmetic operators, plus exactly one string-literal argument to one +of `sum`, `count`, `min`, `max`, `avg` (an array path). Example: +`"count('line_items') >= 1"`. + +### Structural caps (spec §6.1) + +Enforced by `compile_schema` regardless of the byte-size cap below (`SchemaCaps` in +`compile.py`): + +| Cap | Default | +|---|---| +| Max leaves | 200 | +| Max arrays | 20 | +| Max columns per array | 40 | +| Max nesting depth | 6 | +| Max regex pattern length | 200 | +| Max aliases per field | 10 | +| Max description length | 500 | +| Max constraints | 30 | + +These caps are hardcoded in the compiler (not env-configurable); what **is** +env-configurable is the raw byte size of the whole `keys` document +(`AGENT_KV_MAX_SCHEMA_BYTES`) and of `calculations` +(`AGENT_KV_MAX_CALCULATIONS_BYTES`) — see [§13 env reference](#13-environment-reference). + +A schema that fails to compile is rejected with a `400` at submit time — **nothing is +billed** — or, at `/validate`, with a `200` carrying `{"valid": false, "error": ""}` +(compile errors are not 400s there; only a missing `keys` body key is). + +## 3. Submit — `POST /agent-kv/` (multipart) + +Copied verbatim from spec §7.1 and cross-checked field-by-field against +`backend/agent_kv/execution_serializers.py::SubmitSerializer` — every row below matches +the shipped serializer exactly; no drift found in the submit contract itself (see the +notes after the table for two spec-vs-code nuances worth flagging). + +The request is **extractor-scoped**: a per-extractor schema and its knobs live inside +`extractors`, and only fields describing the request itself stay top level. + +**Job-level fields:** + +| Field | Required | Notes | +|---|---|---| +| `file` | yes | One document per job (D12). | +| `extractors` | yes | JSON array (file part or inline JSON string), one entry per extractor. Compiled synchronously; invalid ⇒ 400, nothing billed. | +| `page_start`, `page_end` | no | 1-based inclusive range. **Job-level, not per-extractor**: the range drives the shared OCR pass and the page cap, so it cannot differ between extractors reading the same document. | +| `timeout` | no | Omitted or `0` = pure async (immediate 202). `1–300`: the view polls the job row up to the deadline and returns the result inline if the job completes, else 202 with the job id. | +| `tags`, `custom_data` | no | Echoed through. | +| `webhook_url` | no | Terminal-state POST `{job_id, status}` only — no result payload. | + +**Each `extractors` entry:** + +| Field | Required | Notes | +|---|---|---| +| `name` | yes | Which extractor: `kv` or `table`; anything else ⇒ 400. More than one entry ⇒ 400 — the format is in place for multi-extractor jobs, the execution is not built yet. | +| `keys` | yes | This extractor's own schema. For `kv`, its `keys.json` (see [§2](#2-the-extraction-schema-keys-field)); for `table`, `{"target_table": ""}` — the engine's one required extraction parameter. | +| `options` | no | This extractor's own knobs (below). An unrecognised option ⇒ 400, so a knob aimed at the wrong extractor fails loudly instead of being silently dropped. | + +**`kv` options:** + +| Option | Notes | +|---|---| +| `qa` | Default **on**. | +| `challenge` | Default **on** (~doubles LLM spend; meter records what ran). | +| `extraction_mode` | `whole-doc` (default) \| `per-page`. | +| `structured_output` | Disabled by default; enabled per deployment via `AGENT_KV_STRUCTURED_OUTPUT_ENABLED`. | +| `calculations` | Post-processing instructions; opt-in codegen. Disabled by default; enabled per deployment via `AGENT_KV_CALCULATIONS_ENABLED`. | +| `document_class` | Free-text hint (as CLI). | +| `key_notes` | Free-text notes appended to the prompt (as CLI). | + +**`table` options:** + +| Option | Notes | +|---|---| +| `instructions` | Free-text extraction guidance handed to the engine. | +| `json_structure` | Free-text description of the row structure to return. | +| `enable_header_mapping` | Default **off**. **Changes the result shape** — see [§5](#5-result--get-agent-kvjob_idresult). | +| `correct_number_separators` | Default **off**. Re-reads thousands/decimal separators in numeric cells. | +| `number_format` | `US` (default) \| `EU`. Which convention `correct_number_separators` assumes. | + +Not exposed for `table` (they are meaningless on a blind API): `output_path`, +`enable_highlight`, and the IDE callback hints `prompt_key`/`doc_name`. + +> **No backward compatibility with the pre-`extractors` flat shape.** `keys`, `qa`, +> `challenge`, `extraction_mode`, `structured_output`, `calculations`, +> `document_class` and `key_notes` are no longer accepted at top level — a request +> using the old form gets a 400 for the missing `extractors` field. The API had no +> consumers when this changed, so no alias was kept. + +Not exposed (D6): model choice, challenger model, `parallel_pages`, thinking budgets. + +**Two implementation-level notes not visible in the table:** + +- File type is allowlisted (`.pdf .xlsx .xls`) and size-capped + (`AGENT_KV_MAX_FILE_SIZE_MB`); PDFs are page-counted **locally, pre-OCR**, via + `pdfplumber` and rejected over `AGENT_KV_MAX_PAGES` before any paid work runs. Excel + has no pre-OCR page concept — it is capped by file size only at submit time (the + engine enforces a post-OCR virtual-page cap). + - Images (`.png .jpg .jpeg .tiff`) are **not** accepted, and no longer count as + 1 page. The engine treats only `.pdf/.xlsx/.xls` as a document and + `ImageLoader.load_pages` has no call site, so an accepted image returned + `success: true` with every key not-found and a page billed. They are refused at + submit until the engine side is wired. +- `timeout`'s upper bound is not the literal `300` in the spec prose — it's + `AGENT_KV_MAX_TIMEOUT_SECONDS` (default `300`, but deployment-configurable). + +### Response + +`202` (async, the default): + +```json +{ + "job_id": "5b6e9b0a-...", + "status": "dispatched", + "status_url": "/agent-kv/5b6e9b0a-...", + "created_at": "2026-08-28T10:15:00.123456+00:00" +} +``` + +`200` (when `timeout` was set and the job finished before the deadline): the full +[result payload](#5-result--get-agent-kvjob_idresult), same shape as the result +endpoint. + +`501`: [engine unavailable](#11-when-the-engine-is-unavailable-501-behavior). +`429`: rate-limited (per-key request rate, or per-org concurrent-job limit — see +[§13](#13-environment-reference)). +`402`: the organization's subscription does not permit the request — its trial has +expired, or the subscription is inactive. Nothing is dispatched and nothing is +billed. This is the same gate, the same policy and the same response bodies an +API deployment execute gets from the cloud subscription middleware; Agent-KV +applies it in the submit view instead, because its URL carries no org segment for +that middleware to resolve an organization from. Cloud-only: an OSS-only +deployment returns `501` before ever reaching it. +`400`: serializer/schema validation failure — nothing billed. + +### Example + +```bash +curl -X POST https://api.unstract.example/agent-kv/ \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" \ + -F "file=@invoice.pdf" \ + -F 'extractors=[{ + "name": "kv", + "keys": { + "quotation_number": {"description": "The quote number", "required": true}, + "customer": {"name": {"description": "Bill-to name"}}, + "line_items": { + "description": "One row per line", + "_key": "sku", + "_array": { + "sku": {"description": "SKU"}, + "total": {"description": "Line total", "format": "currency"} + } + }, + "_constraints": ["count(\"line_items\") >= 1"] + }, + "options": {"challenge": false} + }]' \ + -F "webhook_url=https://example.com/hooks/agent-kv" +``` + +## 4. Status — `GET /agent-kv/{job_id}` + +Verbose, agent-centric, stage-level (`backend/agent_kv/execution_views.py::_status_document`). +**Stage names are per-extractor.** They are wire format, and `qa`/`challenge`/`codegen` +mean nothing to the table extractor, so a job's recorded stages are filtered through the +list for the extractor that actually ran +(`backend/agent_kv/constants.py::STAGE_NAMES_BY_EXTRACTOR`). + +- `kv` (superset; only stages that actually ran for this job appear, in a fixed + order): `document_processing, extraction, qa, challenge, normalize, constraints, + codegen, code_execution`. +- `table`: `table_extraction` — a single stage. The table engine exposes no node-level + progress hooks, so finer stage names would describe progress the executor cannot + actually report. It re-reports `table_extraction` as `running` while the engine works, + carrying a `steps` counter that increases as the engine passes its own progress points. + +```json +{ + "job_id": "5b6e9b0a-...", + "status": "running", + "extractors": { + "kv": { + "stage": "challenge", + "stages": [ + {"name": "document_processing", "status": "done", "seconds": 6.2}, + {"name": "extraction", "status": "done", "seconds": 11.4}, + {"name": "qa", "status": "done", "seconds": 4.1}, + {"name": "challenge", "status": "running"} + ] + } + }, + "created_at": "2026-08-28T10:15:00.123456+00:00", + "started_at": "2026-08-28T10:15:01.500000+00:00", + "completed_at": null, + "pages_total": 3 +} +``` + +Each stage entry always carries `status` (`running`|`done`) and, if reported, +`seconds`; any additional flat scalar counters the executor reported for that stage +(e.g. `keys_checked`, `flagged`, `fields_repulled`) ride alongside — nested/list +counters, and any counter using the reserved keys `status`/`seconds`, are dropped by the +internal stage-report endpoint rather than persisted (`backend/agent_kv/internal_views.py::_sanitize_counters`). + +`status` is lowercased (`pending`, `dispatched`, `running`, `completed`, `failed`, +`cancelled`); `error` is included (only) when `status == "failed"`. `pages_total` and +`completed_at` are always present (as `null` until known) — an addition beyond the +spec's illustrative example, not a drift, since spec §7.2 was explicitly a partial +sample ("Verbose, agent-centric, stage-level"). + +```bash +curl https://api.unstract.example/agent-kv/5b6e9b0a-.../ \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" +``` + +## 5. Result — `GET /agent-kv/{job_id}/result` + +Re-readable until `expires_at` (D11), plus explicit `DELETE`. + +- **Non-terminal job** (`PENDING`/`DISPATCHED`/`RUNNING`): `409` with + `{"status": ""}`. +- **Expired, or `COMPLETED` with an already-swept/blank `result_ref`**: `404`. +- **`COMPLETED`**: `200` with the engine's full result object, unchanged (record, + normalized_record, per-field audit trail, `qa_passed`, `challenge_passed`, + `consistency_violations`, `cost_summary`, `timing` — spec §4; this repo does not + shape or re-validate that payload, it stores and replays exactly what the executor's + finalize call sent). +- **`FAILED`**: `200` with + `{"success": false, "status": "failed", "error": ""}`. +- **`CANCELLED`**: `200` with `{"success": false, "status": "cancelled"}`. + +**Calculation result shape** (when `calculations` is supplied and +`AGENT_KV_CALCULATIONS_ENABLED=true`): a `COMPLETED` result adds these fields to the +engine's standard result object: + +- `calculations_applied: true` — indicates calculation codegen ran. +- `execution: {success: bool, rows_written: int, error: string|null}` — sandbox worker + execution status. `success` is `true` if the generated code ran without error; + `rows_written` is the count of rows the code emitted; `error` is `null` on success, or + a user-safe message (e.g., `"Runtime error: division by zero"`) on failure. +- `codegen_validation_passed: bool` — whether the AST gate (layer 1 of a 5-layer + defense-in-depth model) accepted the generated code syntax. +- `calculation_rows: [...]` — an array of computed JSONL rows (size-capped). If the code + produced more rows than the cap allows, this array is empty and `calculation_rows_truncated: true` + is present instead. + +If the sandbox worker fails to execute, the job completes with `success: false` and a +user-safe `error` (e.g., `"Calculation execution timed out"`); the `execution` field +carries the inner error details. + +**Drift from the spec:** §7.3 sketches the failed-job body as +`{success: false, error, timing}`. The shipped body +(`backend/agent_kv/execution_views_result.py::result_payload`) is +`{"success": false, "status": "failed", "error": ...}` — it adds `status` and does +**not** include a `timing` key (there is nothing to time for a job that never produced +an engine result). Document the code: no `timing` on failure/cancellation bodies. + +```bash +curl https://api.unstract.example/agent-kv/5b6e9b0a-.../result \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" +``` + +The payload is **keyed by extractor**, with usage attributed per extractor +alongside the authoritative total: + +```json +{ + "success": true, + "status": "completed", + "extractors": { + "kv": { + "success": true, + "record": {"quotation_number": "Q-10234", "customer": {"name": "Acme Corp"}}, + "normalized_record": {"quotation_number": "Q-10234", "customer": {"name": "Acme Corp"}}, + "keys": [ + {"key_path": "quotation_number", "qa_status": "pass", "challenge_status": "pass"} + ], + "qa_passed": true, + "challenge_passed": true, + "consistency_violations": [], + "cost_summary": { + "total_cost": 0.0, + "input_tokens": 3120, + "output_tokens": 480, + "agents": { + "kv_extractor": {"input_tokens": 2400, "output_tokens": 360, "cost": 0.0}, + "kv_qa": {"input_tokens": 720, "output_tokens": 120, "cost": 0.0} + } + }, + "timing": {"document_processing": 6.2, "extraction": 11.4} + } + }, + "usage_summary": { + "total": {"pages": 3, "input_tokens": 3120, "output_tokens": 480, "total_cost": 0.0}, + "by_extractor": { + "kv": {"pages": 3, "input_tokens": 3120, "output_tokens": 480, "total_cost": 0.0} + } + } +} +``` + +`success` and `status` are at the **top level on every terminal payload** — +completed, failed and cancelled — so a client branches on one key. They describe +the job; each extractor keeps its own `success` inside its own block. +`usage_summary.total` is the billing figure; `by_extractor` exists so a +multi-extractor job can be attributed. With one extractor they are the same numbers. +Note the **failed** and **cancelled** payloads above are NOT extractor-keyed: a +failure belongs to the job, not to one extractor. + +**`cost_summary` dollars are always `0.0` and are not a billing figure**: the executor +zeroes the engine's per-token price fields (they were stale hardcoded list prices), so +`total_cost` and every per-agent `cost` come back `0.0` — metering is what bills, from +the SDK's own litellm-priced usage records. Read `cost_summary` for its token/agent +breakdown only. `agents` is omitted entirely when no agent recorded usage (a full +cache hit). + +**The per-field audit entries in `keys[]` are keyed `key_path`, not `path`.** A live +entry carries more than the example above shows — this one is verbatim from a real run: +`{"key_path": "invoice_number", "value": "INV-…", "found": true, "qa_status": +"unchecked", "qa_attempts": 0, "line_start": 5, "line_end": 5, "normalized_value": +"INV-…"}` (`qa_status` is `"unchecked"` with `qa_attempts: 0` when the job ran with +`qa=false`). + +(The exact result shape beyond `success` is the cloud engine's contract — spec §4 — not +re-specified or validated by this repo.) + +**The `table` extractor's result** is the table engine's extraction output, filed +under `extractors.table`: + +```json +{ + "success": true, + "status": "completed", + "extractors": { + "table": { + "tables": [{"unit": "A1", "rent": 1200}], + "page_count": 3, + "table_pages": [1, 2], + "headers": ["unit", "rent"], + "row_count": 1, + "timing": {"document_processing": 6.2, "total": 41.7} + } + }, + "usage_summary": { + "total": {"pages": 3, "input_tokens": 3120, "output_tokens": 480, "total_cost": 0.0}, + "by_extractor": { + "table": {"pages": 3, "input_tokens": 3120, "output_tokens": 480, "total_cost": 0.0} + } + } +} +``` + +`tables` carries one object per extracted row; `headers` is the canonical column list +the engine settled on; `table_pages` are the 1-based pages the rows came from; and +`row_count` is the number of rows extracted. `timing` rides inside the extractor block, +and `parse_failures` appears only when some page failed to parse. There is **no +`cost_summary`** on this extractor — the table engine has no per-agent cost tracker — so +`usage_summary` is the only billing figure, as it is for `kv`. + +> **`enable_header_mapping` changes the type of `tables`.** With it **off** (the +> default), `tables` is a flat list of row objects, exactly as above. With it **on**, +> the engine wraps them and `tables` becomes +> `{"header_mapping": {...}, "rows": [...]}`. That is a breaking shape change between +> two settings of one option: a client that sets the flag must read `tables.rows` +> instead of iterating `tables`. + +## 6. Validate — `POST /agent-kv/validate` + +Compile-only check; **free of charge but not free of auth** — authenticated (valid key) +and rate-limited the same as submit (spec §6.1: "an open compile endpoint would be a +probing/DoS surface"). No job is created; nothing is billed either way. + +```bash +curl -X POST https://api.unstract.example/agent-kv/validate \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" \ + -H "Content-Type: application/json" \ + -d '{"keys": {"invoice_number": {"description": "Invoice #", "required": true}}}' +``` + +Success (`200`): + +```json +{"valid": true, "leaves": 1, "arrays": 0, "constraints": 0} +``` + +Invalid schema — still `200`, not `400` (schema-compile rejection is a *response*, not +a request error): + +```json +{"valid": false, "error": "Leaf at invoice_number is missing a 'description'"} +``` + +Missing `keys` key in the body is the one case that *is* a `400`: +`{"detail": "body must include 'keys'"}`. + +## 7. Cancel — `POST /agent-kv/{job_id}/cancel` + +Defined precisely per spec §7.4: no task-revocation machinery exists anywhere in the +platform, so cancel flips the job row to `CANCELLED` via the same guarded +`mark_terminal` write gate every other terminalization path uses +(`backend/agent_kv/models.py::AgentKVJob.mark_terminal`) — later callbacks/stage +reports become no-ops; a job not yet picked up is dropped at pickup. + +A job **already running is stopped cooperatively**, not revoked. Cancellation is +detected opportunistically: the executor only learns of it on its next stage report, +which the internal endpoint answers as a no-op once the row is terminal. Both extractors +check at their engine's progress points — `kv` at every graph-node boundary, `table` at +the engine's own progress points (rate-limited, so detection can lag by a few seconds) — +and the job then terminates as `cancelled`. Work already done is still billed and +metered; work not yet started is not. A job that finishes before the cancel is noticed +simply has its result discarded on arrival. + +- Won the race (job was non-terminal): `200` `{"status": "cancelled"}`. +- Already terminal: `409` `{"status": ""}` — **note this one is the raw + uppercase enum value** (e.g. `"COMPLETED"`), unlike every lowercased `status` value + elsewhere in this API (status doc, result 409, validate). This is the code as shipped + (`execution_views.py::JobCancelView`, confirmed by + `test_job_views.py::test_cancel_on_completed_is_409_and_result_untouched`), not a + typo in this document. + +```bash +curl -X POST https://api.unstract.example/agent-kv/5b6e9b0a-.../cancel \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" +``` + +## 8. Delete — `DELETE /agent-kv/{job_id}` + +Deletes the result and any residual staged input immediately, and blanks both refs +(`204`, no body). The job row itself is retained (audit trail); a later `result` fetch +404s (blank `result_ref`), and status still works. + +```bash +curl -X DELETE https://api.unstract.example/agent-kv/5b6e9b0a-.../ \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" +``` + +## 9. Webhook delivery + +`webhook_url` (if given at submit) receives exactly one terminal-state POST: +`{"job_id": "...", "status": "completed"|"failed"|"cancelled"}` — a fixed payload +shape, no result content (spec §6.7, §7.1). Delivered from the `ide_callback` worker +(`workers/ide_callback/agent_kv_tasks.py::_maybe_webhook`) only on a *fresh* finalize +(a duplicate/late finalize for an already-terminal job never re-fires it). + +**SSRF controls, as actually implemented** (`workers/shared/utils/webhook_notify.py`, +function `send_webhook`) — this corrects an earlier draft of this document, which +claimed connect-time re-checking and bounded retries; the code does neither, and its +own module docstring says so: + +- **Resolution-time check only.** The host is resolved once via `socket.getaddrinfo` + and every returned address is checked against private/loopback/link-local/ + reserved/multicast/unspecified ranges *before* the request is made + (`_host_is_public`). It is **not** re-checked at connect time — the subsequent + `requests.post` call performs its own, independent DNS resolution, and nothing + pins that second resolution to the address(es) just validated. +- **Residual risk, accepted for v1: DNS-rebinding TOCTOU.** An attacker whose DNS + answer changes between the pre-check's resolution and `requests`' own resolution + a moment later (serving a public IP to the first, a private/internal IP to the + second) slips through this gap. The module's own docstring names this as a known, + accepted risk. **Fast-follow, not shipped:** IP-pinning the validated address into + the actual connection, so the two resolutions can never disagree. +- **Scheme: https-only**, in this feature's actual usage. `send_webhook` exposes a + generic `allow_http` escape hatch (for explicitly configured dev environments + elsewhere), but the Agent-KV caller (`_maybe_webhook`) never passes it — every + Agent-KV webhook is https-only in practice. +- **No redirects** (`allow_redirects=False`). +- **Fixed, minimal payload** (`{job_id, status}` only — see above) and the + **response body is never read** — only the status code is inspected. +- **Short timeout** (10 seconds). +- **No retries.** One POST attempt only; any failure (host refused by the + pre-check, timeout, connection error, non-2xx status) is logged and swallowed — + there is no retry loop anywhere in this delivery path. + +Test/dev stacks only: `AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=1` on the +ide-callback worker waives both guards (http scheme and non-public host) so the +e2e lane can deliver to a receiver on the compose host. Never set it in +production. + +## 10. Retention and TTL + +- **Input deletion is completion-triggered, not TTL-based** (spec D10: "uploaded + document deleted on job completion"). `FinalizeView.post` + (`backend/agent_kv/internal_views.py`) deletes the staged input file and blanks + `input_ref` the instant a finalize call actually wins the terminal-state guard (the + job just became `COMPLETED` or `FAILED`), via `storage.delete_input` — which only + ever touches `input_ref`, never `result_ref`/the result file. A duplicate/late + finalize call (guard already lost, since the job is already terminal) never deletes + anything: either the winning call already did, or the job reached terminal some + other way (see next bullet). +- **Cancelled jobs' inputs intentionally ride TTL instead.** `JobCancelView` marks a + job `CANCELLED` directly via `AgentKVJob.mark_terminal`, not through `FinalizeView` + — so cancellation never triggers the completion-time delete above. A late finalize + call against an already-`CANCELLED` job also no-ops (it loses the terminal guard, + same as any duplicate). A cancelled job's staged input is therefore cleaned up the + ordinary way: by the TTL sweep once `expires_at` passes, or by an explicit + `DELETE /agent-kv/{job_id}`. +- **Result retention**: `AGENT_KV_RESULT_TTL_DAYS` (default **7** days, D10's + engineering default) — results are re-readable until then, then swept by the + internal `ttl-cleanup` endpoint ([§12](#12-deploy-checklist)), which blanks + `result_ref` (and `input_ref` too, covering the cancelled-job case above, or + defensively for any input that somehow outlives completion). The job row itself is + never deleted (audit trail persists past TTL, only the object-store payloads are + dropped). +- **`expires_at` is stamped at submit time**, not at completion — + `SubmitView.post` sets `expires_at = timezone.now() + timedelta(days=AGENT_KV_RESULT_TTL_DAYS)` + before dispatch even runs. **This is a real divergence from spec D10's framing** + ("results retained 7 days" implicitly from completion): for a job that runs close to + its `timeout` ceiling, the effective post-completion retention window is slightly + under the full 7 days. Documented here as shipped behavior, not silently + reconciled — flagged for anyone tightening D10 later. + +## 11. When the engine is unavailable (501 behavior) + +The Django backend cannot see the executor plugin registry (that lives in the workers +process). Dispatching to a queue no worker consumes would hang, not error — so gating +happens **before enqueue, in the backend**, via a capability probe: + +```python +if not get_plugin("agent_kv"): + raise EngineUnavailable() # 501, "agent-kv engine not available on this deployment" +``` + +`get_plugin(...)` (`backend/plugins/__init__.py`) returns an empty dict — falsy — when +no `plugins/agent_kv/` package is installed, which is the OSS-only case by design (the +plugin is a cloud deliverable, spec §5.1/§11: "OSS scaffold merges dark"). Every +`POST /agent-kv/` on an OSS-only deployment therefore returns: + +```json +{"detail": "agent-kv engine not available on this deployment"} +``` + +with HTTP `501`. `/validate`, key management, status/result/cancel/delete on +*already-existing* jobs are unaffected by this gate — only `SubmitView` probes it, +since only submit needs the engine. + +### 11a. The cancel-at-pickup contract (internal API — frozen; build the cloud engine against this) + +Spec §7.4 states that "a job not yet picked up is dropped at pickup," but leaves the +actual mechanism unspecified. This is it, and it is load-bearing enough that it is +recorded here explicitly rather than left to be inferred from the internal-API code: + +`POST /internal/v1/agent-kv/jobs/{job_id}/stage/` — the stage-report endpoint +(`StageReportView`, `backend/agent_kv/internal_views.py`) — no-ops with +`{"ok": true, "noop": true}` whenever the target job is already terminal (its +candidate query excludes `AgentKVJob.TERMINAL`, so a terminal job's row is simply +never found). **That response body *is* the drop signal.** The FIRST stage report an +executor makes for a job it has just picked up doubles as a liveness check against a +job that was cancelled (or otherwise reached a terminal state) in the window between +dispatch and pickup — since there is no task-revocation machinery anywhere in the +platform (spec §7.4) to stop a picked-up job any other way. + +On `noop: true`, the executor **must**: + +1. Stop doing any further work on that job immediately. +2. **Still call `FinalizeView`** (`POST .../jobs/{job_id}/finalize/`) with whatever + outcome it has — success or failure, it does not matter which, since the write is + going to no-op either way. + +Step 2 is not optional. `FinalizeView`'s terminal-state guard (spec §5.4) correctly +no-ops the status write for an already-terminal job — but its concurrency-slot +release (`AgentKVConcurrencyLimiter.release`) lives in a `finally`, unconditional on +whether the guard actually won ([§10](#10-retention-and-ttl) covers the input-deletion +side of this same guard). An executor that treats `noop: true` as "nothing left to do +here" and skips the finalize call entirely leaks that job's concurrency slot for the +rest of the limiter's TTL (6 hours) instead of releasing it immediately. This is the +entire mechanism behind a pre-pickup cancel actually being observed and cleaned up +promptly — there is no other signal. + +## 12. Deploy checklist + +Everything below is required (or worth checking) to run this feature for real, +beyond `docker compose up`: + +1. **Cloud plugin**: install/enable the `agent_kv` backend capability plugin + (probed via `plugins.get_plugin("agent_kv")`) — without it, submit always 501s + ([§11](#11-when-the-engine-is-unavailable-501-behavior)). This repo ships gated + dark by design; nothing to do for an OSS-only deployment except accept the 501. + The marker the probe keys off is the mere presence of + `backend/plugins/agent_kv/__init__.py` — a cloud image ships it (via the + plugin-copy step), an OSS-only build does not. +2. **Executor worker fleet queue**: the executor dispatch derives its Celery queue name + from the executor name as `celery_executor_{executor_name}` — for Agent-KV that's + **`celery_executor_agentic_kv`** (`unstract/sdk1/src/unstract/sdk1/execution/dispatcher.py`). + `workers/run-worker.sh`'s executor role default queue list + (`celery_executor_legacy,celery_executor_agentic,celery_executor_agentic_table`) + does **not** include it — a self-hosted/OSS-only deployment running the cloud + `agentic_kv` executor plugin must add `celery_executor_agentic_kv` to that worker's + consumed queues (`CELERY_QUEUES_EXECUTOR` on the Docker path, or the equivalent + `run-worker.sh` queue map entry) or dispatched jobs will never be picked up. + **The cloud Helm chart already wires this**: both `workerExecutorV2.args` + (`--queues=...,celery_executor_agentic_kv`) and + `workerPgExecutor.env.WORKER_PG_QUEUE_CONSUMER_QUEUE` include it, and both fleets + carry `additionalConfigs: [..., agentKv]` (`charts/unstract-platform/values.yaml`, + cloud repo) so the executor pods get the `AGENT_KV_*` LLM/LLMWhisperer env alongside + the queue wiring. +3. **Callback queue**: the `ide_callback` worker must consume `agent_kv_callback` + alongside its own `ide_callback` queue, or jobs dispatch but never finalize. This is + already the hardcoded default in both `workers/run-worker-docker.sh` and + `workers/run-worker.sh` (`WORKER_QUEUES["ide_callback"] = "ide_callback,agent_kv_callback"`) + — nothing to configure for a standard deployment; see the note already in + `docker/sample.env` under "IDE Callback Worker." The cloud Helm chart matches: + `workerIdeCallbackV2.args` (`--queues=ide_callback,agent_kv_callback`) and + `workerPgIdeCallback.env.WORKER_PG_QUEUE_CONSUMER_QUEUE` + (`"ide_callback,agent_kv_callback"`) both already include it + (`charts/unstract-platform/values.yaml`, cloud repo). +4. **Internal periodic maintenance — proxy tasks now exist; nothing schedules them + yet.** Two internal endpoints do the real work + (`backend/agent_kv/internal_urls.py`, `internal_views.py::SweepView`/ + `TTLCleanupView`), and thin scheduler-side proxy tasks now call them — + `workers/scheduler/agent_kv_tasks.py::agent_kv_sweep`/`agent_kv_ttl_cleanup`, + registered under the wire names `agent_kv.sweep`/`agent_kv.ttl_cleanup` — mirroring + the dashboard-metrics periodics (`workers/scheduler/dashboard_metrics_tasks.py`) + this feature was designed to follow (spec §5.4), except the call goes through the + shared `InternalAPIClient` facade (`agent_kv_sweep`/`agent_kv_ttl_cleanup` methods on + `workers/shared/api/internal_client.py`) rather than a second bespoke HTTP client. + **Nothing calls these tasks on a schedule yet** — an operator must register both as + periodic tasks via the same PG-scheduler mechanism the dashboard-metrics periodics + use: a `PgPeriodicTask` row per task (`name`, `task_name` = the wire name above, + `queue`, `cron_string`, `enabled=True`, `pg_owned=True` once ready to go live — see + `backend/dashboard_metrics/migrations/0004_pg_periodic_tasks.py` for the exact row + shape this mirrors) before relying on TTL cleanup or stuck-job recovery: + - `agent_kv.sweep` → `POST /internal/v1/agent-kv/sweep/` — terminalizes `PENDING` + jobs never dispatched within `AGENT_KV_SWEEP_GRACE_SECONDS` (default 1 hour) + **and** `DISPATCHED`/`RUNNING` jobs stuck past `AGENT_KV_STUCK_JOB_GRACE_SECONDS` + (default 6 hours) — two independent phases per call, response + `{"swept": N, "timed_out": M}`. Suggested cadence: every few minutes. + - `agent_kv.ttl_cleanup` → `POST /internal/v1/agent-kv/ttl-cleanup/` — deletes + staged input/result files past `expires_at`. Suggested cadence: hourly or daily + (retention is measured in days). + Both endpoints are idempotent, batch-capped at 500 rows per phase per call, and + safe to call more often than needed. + + **Cloud mechanism — Kubernetes CronJobs, not the PG-scheduler.** The sweep and + TTL-cleanup logic itself lives in `backend/agent_kv/maintenance.py::run_sweep`/ + `run_ttl_cleanup` — `SweepView`/`TTLCleanupView` above are now thin wrappers + around it. Two Django management commands wrap the same functions — + `python manage.py agent_kv_sweep` and `python manage.py agent_kv_ttl_cleanup` + (`backend/agent_kv/management/commands/`) — each printing the JSON counts + (`{"swept": N, "timed_out": M}` / `{"cleaned": N}`) and exiting 0. In the cloud + deployment, a Kubernetes CronJob runs each command on the cadences above instead + of registering a `PgPeriodicTask` row. The scheduler proxy tasks + (`agent_kv.sweep`/`agent_kv.ttl_cleanup`) and their PG-scheduler registration + remain the mechanism for OSS/self-hosted deployments, which have no CronJob + equivalent to drive from. + + **Chart keys (cloud repo)**: `backend.agentKvCronJobs` in + `charts/unstract-platform/values.yaml` — `enabled: false` there (on-prem never runs + it), flipped to `enabled: true` in `charts/cloud-deployment-values/cloud.values.yaml`. + Its `jobs` list is the two commands above with concrete schedules: + `{name: sweep, command: agent_kv_sweep, schedule: "*/15 * * * *"}` and + `{name: ttl-cleanup, command: agent_kv_ttl_cleanup, schedule: "17 * * * *"}` — offset + from sweep's :00/:15/:30/:45 ticks so the two CronJobs never contend on the same + rows. Neither job needs the `agentKv` shared-config group (LLM/LLMWhisperer + creds — only the executor touches those): sweep only needs `database`/`redis`, + ttl-cleanup additionally needs `storage` for `AGENT_KV_FILE_STORAGE_CREDENTIALS` + (see item 6 below) — both already on the `backend` deployment's config list. +5. **Env vars**: every `AGENT_KV_*` setting plus `AGENT_KV_FILE_STORAGE_CREDENTIALS` — + see [§13](#13-environment-reference) and `docker/sample.env`. The executor-side vars + (read by the cloud `agentic_kv` plugin, not by the backend settings in §13) are a + separate chart group: `global.sharedConfigs.agentKv` + (`charts/unstract-platform/values.yaml`, cloud repo) — + `AGENT_KV_LLM_PROVIDER`, `AGENT_KV_LITE_MODEL`, `AGENT_KV_ADVANCED_MODEL`, + `AGENT_KV_LLM_API_KEY`, `AGENT_KV_LLMWHISPERER_API_KEY`, + `AGENT_KV_LLMWHISPERER_BASE_URL`, `AGENT_KV_MAX_TOKENS`, `AGENT_KV_PARALLEL_PAGES`, + `AGENT_KV_ENGINE_VERSION`. Of those, two are ESO (External Secrets Operator) + secrets rather than plain values — `AGENT_KV_LLM_API_KEY` and + `AGENT_KV_LLMWHISPERER_API_KEY`, mapped in `global.externalSecrets.groups.agentKv` + to the GCP Secret Manager suffixes `agent-kv-llm-api-key` / + `agent-kv-llmwhisperer-api-key`. `AGENT_KV_LLM_PROVIDER` is a v1 hard allowlist of + two values — `anthropic` and `openai` (`SUPPORTED_PROVIDERS` in the executor's LLM + adapter) — anything else raises `ConfigError` at dispatch, before any LLM call. + + **Rollout order is strict, and getting it wrong takes down the whole executor + fleet.** Both executor fleets mount this group with `envFrom: secretRef: + -agent-kv`, so no executor pod of any kind — not just Agent-KV ones — + can start until that Secret exists: enabling the group ahead of its contents + leaves `workerExecutorV2` and `workerPgExecutor` in `CreateContainerConfigError`. + Per environment, in this order: (1) create the two GCP Secret Manager secrets + `-agent-kv-llm-api-key` and + `-agent-kv-llmwhisperer-api-key`; (2) set the three + non-secret keys — `AGENT_KV_LLM_PROVIDER`, `AGENT_KV_LITE_MODEL`, + `AGENT_KV_ADVANCED_MODEL` — in that environment's own values file (under ESO they + are emitted as inline literals in the ExternalSecret's `target.template`, and a nil + value is silently **dropped**, so leaving them unset ships a Secret without them and + every Agent-KV job then fails at dispatch with `ConfigError: Missing required + Agent-KV env vars`); (3) only then roll the fleets out. `cloud.values.yaml` enables + the group fleet-wide, so step 2 is a per-environment prerequisite, not an optional + extra. In inline (non-ESO) mode the same three keys are enforced at render time by a + `fail` guard in `charts/unstract-platform/templates/shared/agent-kv-secret.yaml`, so + a mistake there is a loud deploy-time failure instead of a per-job runtime one. +6. **Object storage**: `AGENT_KV_FILE_STORAGE_CREDENTIALS` must point at a real bucket + in production (defaults to the same shared MinIO instance as workflow execution). + Paths are prefix- and org-rooted + (`{AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}/input{ext}`, `.../result.json`) — + no document bytes ever ride the message broker. Chart key: + `global.sharedConfigs.storage.AGENT_KV_FILE_STORAGE_CREDENTIALS` + (`charts/unstract-platform/values.yaml`, cloud repo) — derived from + `fileStorageCredentials`/`MINIO_CREDS` unless set explicitly, the same fan-out + pattern as its three siblings (`WORKFLOW_EXECUTION_`, `API_`, + `HITL_FILES_FILE_STORAGE_CREDENTIALS`). +7. **`AGENT_KV_STORAGE_DIR_PREFIX` — must be set on the backend *and* the executor + fleet, to the same value** (default `unstract/agent_kv`). The **first segment is the + bucket** — s3fs/gcsfs read it that way — so it must be a bucket that already exists + (the MinIO dev bootstrap creates `unstract`); a bucket-less prefix makes every submit + fail `NoSuchBucket` and return a 500 with nothing billed. The executor reads the + backend-staged `input_ref` and keys its OCR cache under the same root + (`{prefix}/{org_id}/cache/…`), so a mismatch silently splits the cache and breaks + result read-back. Unlike the executor-only group in item 5, this one has to reach + **both** sides, so in the chart (cloud repo) it must be fanned out to the backend + *and* to `workerExecutorV2`/`workerPgExecutor` from a single source of truth — never + set independently per fleet, where the two can drift apart unnoticed. +8. **Feature flags — `calculations` and `structured_output`**: the cloud engine + cannot execute these yet, so submit rejects them with a 400 until the flags are + flipped. Both `AGENT_KV_CALCULATIONS_ENABLED` and + `AGENT_KV_STRUCTURED_OUTPUT_ENABLED` default `false`; set either to `true` only + once the executor side has shipped support for it. +9. **Executor time limit — the real baseline is 3,600s, not 7,200s.** The executor + fleet (`workerExecutorV2`/`workerPgExecutor`) is not a `FILE_PROCESSING` worker — + `WorkerConfig._get_worker_specific_timeout_defaults()` + (`workers/shared/models/worker_models.py`) falls through to the "conservative + defaults for other workers" branch for `WorkerType.EXECUTOR`: **1 hour / 3,600s** + hard limit, 3,300s soft. The chart's bare `TASK_TIME_LIMIT: "3600"` ConfigMap key + is inert — `get_celery_setting`'s 4-tier resolution (cmdline → + `{WORKER_TYPE}_{SETTING}` → `CELERY_{SETTING}` → default) means only + **`EXECUTOR_TASK_TIME_LIMIT`** (worker-specific) or `CELERY_TASK_TIME_LIMIT` + (global) actually override it; see the comment on `workerExecutorV2`'s + `terminationGracePeriodSeconds` in `charts/unstract-platform/values.yaml`, which + already documents this ("Its ceiling is task_time_limit=3600s... note the chart's + bare TASK_TIME_LIMIT is NOT read; only EXECUTOR_TASK_TIME_LIMIT / CELERY_*"). + At the 100-page cap (`AGENT_KV_MAX_PAGES`) with `extraction_mode="per-page"`, + `parallel_pages=4`, and both QA and challenge enabled (the worst-case option + combination), the arithmetic worked out in Task 9's report is: each of + `key_extractor` → `kv_qa` → `kv_challenger` fans 100 pages across 4 workers (25 + sequential rounds), each round up to ~99s if its slowest call needs a *full* retry + backoff (`SDKLLMClient`'s `_DEFAULT_MAX_RETRIES=8`) — 25 × 99s = 2,475s per stage, + 3 stages sequential = **7,425s**, a gap of **3,825s (over 2x)** against the real + 3,600s baseline, not the 225s a 7,200s baseline would suggest. That 7,425s figure + assumes every single call across all 300 (100 pages × 3 stages) hits the full + 8-retry backoff, which is not realistic — a real call is ~10-30s, so a full run at + the page cap is closer to **~2,000-2,500s** in practice; the 7,425s number is a + theoretical ceiling for capacity planning, not an expected duration. + **Recommendation: do not raise the fleet-wide time limit preemptively.** + `EXECUTOR_TASK_TIME_LIMIT` is fleet-wide — every executor task (every plugin, not + just Agent-KV) inherits it, and it's coupled to `workerPgExecutor`'s PG-visibility + lease/`vt` and `terminationGracePeriodSeconds` (raising one without the others + reintroduces the mid-flight-SIGKILL/silent-redelivery risk `UN-3964`'s comment on + `workerExecutorV2` already guards against). Measure real per-page timings from the + Task 13 integration run first, then either raise `EXECUTOR_TASK_TIME_LIMIT` (and + `terminationGracePeriodSeconds`/the PG lease together, not alone) or lower + `AGENT_KV_MAX_PAGES` for deployments that see per-page-heavy, near-cap documents in + practice. + + **What actually happens at each limit.** At the **soft** limit (3,300s) Celery raises + `SoftTimeLimitExceeded` into the task; the executor catches it, sets a run-level stop + signal that makes every in-flight LLM retry loop abort at its next backoff instead of + sleeping through the remaining ladder (up to ~30s per attempt × 8 attempts), flushes + the usage records accrued so far, and finalizes the job `failed` with the user-safe + error `"timed out"`. At the **hard** limit (3,600s) the worker is SIGKILLed: usage + records still held in memory are lost, so that run's LLM spend goes **unmetered** + (page usage, posted per-OCR-call, is unaffected). That gap is the reason the soft + limit now stops retries promptly — it is what keeps a slow run from crossing into the + hard kill. +10. **Testing lanes — Docker-free unit gate, Docker-required integration lane.** The + cloud rig's `unit-agentic-kv` group (`tests/groups.cloud.yaml`, cloud repo) runs the + executor plugin's own test suite in an isolated venv with no `requires_services` — + no Docker needed, and it's what `tox -e unit` (CI's unit-tier matrix leg) runs. The + integration tier is the one that needs Docker: it authenticates to the container + registry and boots a mock Auth0 sidecar (`.github/workflows/ci-test.yaml`, cloud + repo) for the cross-service suites. A local `unit-agentic-kv` run needs neither + Docker nor the platform stack up. + + **A real-key Agent-KV e2e run cannot go through `tests.rig run`.** Whenever a + selected group needs the platform, the rig sets `UNSTRACT_LLM_MOCK_RESPONSE` + to `MOCK_LLM_OK` (`tests/rig/cli.py`) — and an exported *empty* value counts as + unset, so there is no way to opt out from inside the rig; the lane then exercises + the mock, not a provider. To run it against real LLMWhisperer/LLM keys, boot the + stack first (`python -m tests.rig platform up`, or `docker compose` with + `tests/compose/docker-compose.test.yaml`) and then invoke pytest directly with + `UNSTRACT_BACKEND_URL`, `AGENT_KV_E2E=1` and the `AGENT_KV_*` keys exported: + `AGENT_KV_E2E=1 UNSTRACT_BACKEND_URL=http://localhost:8000 pytest tests/e2e/agent_kv`. + + Two scenarios are operator-gated on top of that: the bad-LLM-key scenario + (`AGENT_KV_E2E_BAD_KEY_JOB=1`, see the test module docstring) and the + completion-webhook scenario, which needs + `AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=1` set BOTH on the + ide-callback worker (it waives the webhook SSRF guards — https scheme + + public host — for test/dev stacks only; never set it in production) and in + the pytest process env; the receiver is reached via + `host.docker.internal` (the compose host-gateway mapping). The lane also + covers sync-wait submits (`timeout` → 200 with the inline result) and an + `.xlsx` document (`fixtures/invoice.xlsx`, the no-pre-OCR-page-count path). +11. **Sandbox worker — calculations codegen fleet** (when + `AGENT_KV_CALCULATIONS_ENABLED=true`). The optional `calculations` field + invokes out-of-process codegen and execution in a dedicated sandbox worker fleet + consuming the `sandbox_codegen` Celery queue. **Deployment order is strict:** + deploy the sandbox worker (Deployment healthy, actively consuming tasks from + `sandbox_codegen`) **before** flipping `AGENT_KV_CALCULATIONS_ENABLED=true`. + With the flag on but the sandbox down, calculation jobs fail user-safely at the + RPC timeout (they do not hang past it); however, **never leave the flag on + without a healthy sandbox fleet**, as all jobs with `calculations` will eventually + exceed their timeouts and return failures to callers. + + **Security posture:** The sandbox pod carries no LLM, OCR, or storage secrets and + runs with default-deny egress — it only executes generated calculation code against + the extracted record in-memory. This no-secrets/deny-all-egress posture is the + **Kubernetes/chart deployment guarantee**. The reference `docker-compose.yaml` + included here is a best-effort dev mirror only, not that guarantee: its + `worker-sandbox` service shares the same full worker env-file (`workers/.env`) as + every other worker for operational simplicity, so on that stack the untrusted-code + boundary is the runner's per-subprocess env scrub (spec §6.3 layer 2), not full + pod-level secret isolation — don't run untrusted `calculations` against secrets you + care about on docker-compose. User-supplied `calculations` expressions are + validated through an AST gate (layer 1 of a 5-layer defense-in-depth model) at + submit time before ever reaching the sandbox, and further constrained (parsing, + type-checking, capability allowlist) at execution time. The AST gate is a + **best-effort** layer-1 control — a fail-closed denylist that closes the cheap, + demonstrated bypasses and raises the cost of the rest, not a provably airtight + sandbox by itself. The *enforced* containment is layers 2–5: the runner's + env-scrub + rlimits (layer 2) and the pod's non-root/read-only-rootfs/no-secrets/ + deny-all-egress/per-job isolation (layers 3–5), with `runtimeClass: gvisor` as the + deferred syscall-sandbox mitigation (see the accepted v1 residual below). Do not + treat an AST-gate pass alone as proof generated code is safe. + + **Accepted v1 residual (in-pod file reads):** because generated code + legitimately needs the `open()` builtin for its input and output files, untrusted + calculation code can read a world-readable file inside its own sandbox pod and + return the contents to the submitter. The import allowlist is exactly `{json, math, + statistics, decimal, datetime, re, collections, itertools, functools, sys}` — + `pathlib`, `os`, `io`, `socket` and `urllib` are all outside it, so this is a single + read of a known path, not directory traversal and not an exfiltration path. This is contained — not by the AST gate, but + by the pod's layers 2–5: no secrets in the pod (not even the platform + `ENCRYPTION_KEY`), non-root on a read-only rootfs, default-deny egress (no + exfiltration), per-job isolation, and results returned only to the submitting + key — so the worst case is disclosure of a non-sensitive container file to the + customer who submitted the job. The deferred hardening for this is a + `runtimeClass: gvisor` sandbox. Consequently the sandbox pod's default-deny egress + and no-secrets posture are load-bearing and must not be relaxed. + +## 13. Environment reference + +All `AGENT_KV_*` settings (`backend/backend/settings/base.py`), each `os.environ.get` +with the default shown: + +| Variable | Default | Meaning | +|---|---|---| +| `AGENT_KV_PATH_PREFIX` | `agent-kv` | Top-level public URL prefix (whitelisted past tenant middleware). | +| `AGENT_KV_MAX_FILE_SIZE_MB` | `50` | Submit-time file size cap. | +| `AGENT_KV_MAX_PAGES` | `100` | Pre-OCR page cap for PDFs (§6.1). Excel has no pre-OCR page concept; images are not accepted. | +| `AGENT_KV_MAX_CALCULATIONS_BYTES` | `20000` | Byte cap on the optional `calculations` field. | +| `AGENT_KV_MAX_SCHEMA_BYTES` | `262144` | Byte cap on the raw `keys` JSON document (256 KiB). | +| `AGENT_KV_RESULT_TTL_DAYS` | `7` | Result retention window (and a cancelled job's input, which rides the same TTL — a completed/failed job's input is deleted immediately at finalize instead), stamped at submit time (see [§10](#10-retention-and-ttl)). | +| `AGENT_KV_MAX_TIMEOUT_SECONDS` | `300` | Upper bound on the submit `timeout` (synchronous-wait) field. | +| `AGENT_KV_CONCURRENT_LIMIT` | `5` | Per-organization concurrent in-flight job cap (own Redis namespace, fails open on Redis errors). | +| `AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE` | `60` | Per-key request rate limit (submit + validate), fails open on Redis errors. | +| `AGENT_KV_SWEEP_GRACE_SECONDS` | `3600` | Age (from `created_at`) before a never-dispatched `PENDING` job is eligible for the sweep. | +| `AGENT_KV_STUCK_JOB_GRACE_SECONDS` | `21600` | Age (from `dispatched_at`) before a `DISPATCHED`/`RUNNING` job is eligible for the sweep's stuck-job phase — force-failed as `"Job timed out"` (6 hours). | +| `AGENT_KV_CALCULATIONS_ENABLED` | `false` | Gates the submit `calculations` field; the engine cannot execute it yet, so submit 400s while this is off. | +| `AGENT_KV_STRUCTURED_OUTPUT_ENABLED` | `false` | Gates the submit `structured_output` field; the engine cannot execute it yet, so submit 400s while this is off. | +| `AGENT_KV_STORAGE_DIR_PREFIX` | `unstract/agent_kv` | Bucket-rooted object-store root for staged inputs/results (`{prefix}/{org_id}/{job_id}/…`), mirroring `WORKFLOW_EXECUTION_DIR_PREFIX`/`API_EXECUTION_DIR_PREFIX`. The **first segment is the bucket** and must already exist. The cloud executor reads the same variable and keys its OCR cache under `{prefix}/{org_id}/cache/…`, so both fleets must agree. Trailing `/` is stripped. | +| `AGENT_KV_FILE_STORAGE_CREDENTIALS` | *(none — must be set)* | JSON credentials for the `AGENT_KV` `FileStorageType` (`unstract/filesystem`), same shape as `WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS`: `{"provider": "minio", "credentials": {"endpoint_url": "...", "key": "...", "secret": "..."}}`. | + +`AGENT_KV_FILE_STORAGE_CREDENTIALS` is looked up via +`FILE_STORAGE_CREDENTIALS_TO_ENV_NAME_MAPPING[FileStorageType.AGENT_KV]` +(`unstract/filesystem/src/unstract/filesystem/file_storage_config.py`) and is required +for `stage_input`/`write_result`/`read_result`/`delete_job_files`/`delete_input` +(`backend/agent_kv/storage.py`) to have anywhere to write to. diff --git a/docs/superpowers/plans/2026-10-06-table-extractor-api-carveout.md b/docs/superpowers/plans/2026-10-06-table-extractor-api-carveout.md new file mode 100644 index 0000000000..78cff019db --- /dev/null +++ b/docs/superpowers/plans/2026-10-06-table-extractor-api-carveout.md @@ -0,0 +1,358 @@ +# Table Extractor on the Agent-KV API — Standalone Release Carve-Out + +**Goal:** ship `POST /agent-kv/` with `extractors: [{"name": "table", "keys": {"target_table": "…"}}]` +to customers as **its own OSS PR + cloud PR off current `main`**, excluding the +`agentic_kv` extraction engine, the schema codegen path and the hardened sandbox — +all of which stay on the large PRs ([unstract#2309](https://github.com/Zipstack/unstract/pull/2309), +[unstract-cloud#1816](https://github.com/Zipstack/unstract-cloud/pull/1816)) until those stabilise. + +**Non-goal:** anything that makes the `kv` extractor reachable. This plan +deliberately leaves the KV code in the tree, dormant and tested, so the large PRs +re-enable it by reverting four one-line subtractions rather than by merging content. + +**Ticket:** [UN-4232](https://zipstack.atlassian.net/browse/UN-4232), under epic [UN-4044](https://zipstack.atlassian.net/browse/UN-4044). +**Source branches to copy from:** OSS `Feat/agent-kv-api`, cloud `UN-4044-agent-kv-cloud-executor`. +**Target branches:** OSS `feat/agent-kv-api-table` off `main`; cloud `UN-4232-agent-kv-api-table` off `main`. +**Strategy: copy-forward, not cherry-pick.** The source branches carry 100+ interleaved +commits across 338 files; reconstruct the subset by copying files, then subtracting. + +--- + +## The constraint that defines the carve-out + +`workers/plugins/agentic_table/src/api_binding.py:29-32`: + +```python +from agentic_kv.constants import AgentKVConfig +from agentic_kv.exceptions import PageCapExceeded +from agentic_kv.llm_adapter import build_llm, flush +from agentic_kv.progress import JobCancelled, StageReporter +``` + +`agentic_table/pyproject.toml` declares `unstract-agentic-table-engine` and +`unstract-extraction-seams` — **not** `agentic_kv`. The import resolves only +because both plugins are installed into the same worker image. Drop the +`agentic_kv` plugin and the table API path fails at import. + +The module's own docstring already names this ("**Known debt:** … Spec step 4 +moves all three behind the seams package"). So **Task 1 is spec §5 step 4, +pulled forward** — not throwaway work, just work done in the order the carve-out forces. + +--- + +## Task 1 — Break `agentic_table`'s dependency on `agentic_kv` (cloud) + +Move into `workers/plugins/extraction_seams/`, keeping **every env var name byte-identical** +so no chart, secret or external-secret wiring changes: + +| New seams module | Moved from | Notes | +|---|---|---| +| `src/config.py` | `agentic_kv/src/constants.py` (`AgentKVConfig`, 248 L) | Keep the `AGENT_KV_*` env names. The table path reads `advanced_model`, `lite_model`, `llmwhisperer_api_key`, `llmwhisperer_base_url`, `parallel_pages` | +| `src/llm.py` | `agentic_kv/src/llm_adapter.py` (`build_llm`, `flush`, 502 L) + `engine/llm_client_types.py` (41 L) | `llm_client_types` holds 4 small dataclasses/exceptions; it comes along | +| `src/progress.py` | `agentic_kv/src/progress.py` (`StageReporter`, `JobCancelled`, 187 L) | **Drop `ENGINE_NODE_TO_STAGE`** — it maps KV engine nodes to KV stages and means nothing to the table path | +| `src/exceptions.py` | `agentic_kv/src/exceptions.py` (`PageCapExceeded`, `ConfigError`) | Only the two the table path raises | + +Then: +- `agentic_table/src/api_binding.py` imports from `extraction_seams` — the module-scope + seam pattern is preserved, so `tests/test_executor_api_operation.py` still patches + `api_binding`'s own attributes; re-point the patch targets only. +- `agentic_table/pyproject.toml` is already correct — it never declared `agentic_kv`. +- `extraction_seams` keeps its one-way rule: nothing in it may import `agentic_table`, + `agentic_table_engine` or `agentic_kv`. + +**Cost to the large cloud PR on rebase:** 12 files inside `agentic_kv` import these +four modules; they re-point to `extraction_seams`. Mechanical, and it is step 4's +work regardless of this plan. + +--- + +## Task 2 — Stand up the API app without the KV extractor (OSS) + +**Copy from `Feat/agent-kv-api`:** + +``` +backend/agent_kv/ (the API app; 6.5k L incl. tests) +backend/backend/{base_urls,internal_base_urls,urls_v2,settings/base}.py +backend/{pyproject.toml,sample.env,uv.lock} +unstract/filesystem/ (the AGENT_KV storage type; 4 files) +unstract/agent-kv-schema/ (keep — see §Decision 3) +workers/ide_callback/ (agent_kv_callback queue; 5 files) +workers/{run-worker.sh,run-worker-docker.sh,sample.env,pyproject.toml} +docker/{docker-compose.yaml,sample.env,dockerfiles/worker-unified.Dockerfile} +tests/{compose/docker-compose.test.yaml,groups.yaml}, tox.ini, pyproject.toml, uv.lock +docs/agent-kv-api.md +``` + +**Then subtract — four lines, each independently revertible by the large PR:** + +1. **`backend/agent_kv/constants.py` — drop the `kv` entry from `EXTRACTOR_ROUTES`.** + This is the single most important line in the carve-out. `SUPPORTED_EXTRACTORS = + tuple(EXTRACTOR_ROUTES)` (`execution_serializers.py:33`), so one deletion makes a + `kv` submit return **400 `unknown extractor 'kv'; supported: ['table']`**. + + Without it, a `kv` submit is accepted with a 202 and dispatched to + `celery_executor_agentic_kv`, which has no consumer in this deployment — the job + sits forever, durably and silently. That is the exact failure mode that already + cost the team ~30 hours of firings with zero executions. + + **Leave `V1_EXTRACTOR_NAME`, `STAGE_NAMES`, `KVOptionsSerializer` and + `STAGE_NAMES_BY_EXTRACTOR["kv"]` in place.** Dormant and still under test; + `execution_views.py:45-56` already tolerates a job row naming an extractor this + build cannot describe. + +2. **`workers/run-worker.sh:76`** — remove `celery_executor_agentic_kv` from the + executor role's queue list; same in `docker/docker-compose.yaml`. + +3. **`backend/agent_kv/execution_urls.py`** — unregister the `/validate` route. + `ValidateView` compiles a `kv` keys schema via `compile_schema` + (`execution_views.py:255`) and nothing else; the table extractor's `keys` is + `{"target_table": …}`, validated by `TableKeysSerializer`. Shipping an endpoint + that validates schemas for an extractor the deployment refuses is an incoherent + public contract. Keep the view class and the `unstract/agent-kv-schema` package — + deleting them would force a content merge in files the large PR rewrites. + +4. **Drop `workers/sandbox/`** (3 files), the `worker-sandbox` compose service and + `PG_ROLE_SANDBOX` in `run-worker.sh`. See §Decision 2. + +--- + +## Task 3 — Test coverage: API and end to end (OSS) + +Explicit scope, not a trailing chore. The carve-out's whole claim is "this is the +stable subset", and the tests are what substantiate it. Two tiers, because they +fail for different reasons and run in different places. + +### 3a · API testing — contract level, no model calls + +`backend/agent_kv/tests/`, in CI on every PR, no LLM or OCR credentials. Much of +this already exists (6.5k lines, largely extractor-agnostic: auth, rate limiter, +storage, sweeps, internal views, job views) and `test_table_extractor_routing.py` +(252 L) covers the table route. Audit the submit-serializer and dispatch suites for +`kv` assumptions, then close these gaps: + +| Area | Assertions | +|---|---| +| **Routing** | `SUPPORTED_EXTRACTORS == ("table",)` · a `kv` submit returns **400, not 202** | +| **Submit validation** | missing `target_table` → 400 · a `kv` option on a table entry → 400 · unknown extractor → 400 · unknown keys **or** unknown options → 400 · more than one extractor entry → 400 · oversized `extractors` payload → 400 | +| **Auth and tenancy** | no key → 403 · revoked or wrong-org key → 403 · another org's job → **404, not 403** (no existence disclosure) | +| **Limits** | per-key rate limit → 429 · concurrency cap → 429 · a cancelled job releases its slot | +| **Lifecycle** | status shape for queued / running / completed / failed / cancelled · `extractors.table.stages` non-empty and containing only `table_extraction` · result byte-for-byte stable on re-read · DELETE then GET result → 404 · swept or TTL-expired job → 404 | +| **Dispatch** | `table` routes to `("agentic_table", "table_extract_api")` · every supported extractor has a route, a stage list **and** an options serializer — the three tables stay in step | +| **Internal endpoints** | stage report · finalize · sweep · TTL cleanup, including the cleanup-failure path | +| **Webhook** | delivered once on completion · a delivery failure does not change job state | +| **Subscription gate** | expired trial and inactive subscription both 402, byte-identical to the API-deployment path · a database error surfaces as 500, never as unmetered access | + +The routing row matters most. It is the guard on Task 2's first subtraction, and the +only thing standing between a future rebase and a `kv` submit that returns 202 and +then sits in an unconsumed queue forever. + +The unknown-keys/unknown-options row is not padding either: DRF silently discards +unrecognised keys, so `"Options"` for `"options"` would be dropped whole, `options` +would default to `{}`, and the job would run with roughly double the LLM spend the +caller asked for — with a 202 and no indication anything was ignored. + +### 3b · End-to-end testing — deployed stack, real LLM and OCR + +`tests/e2e/agent_kv/`, run first against compose, then against the dev namespace +(Task 5b). These are the tests that catch the wiring failures nobody has had a +chance to hit yet. + +| Scenario | What it proves | +|---|---| +| **Table happy path** — `rent_roll.pdf` | submit → poll → rows match the known table | +| **Excel path** — `invoice.xlsx` | the engine's own Excel branch extracts, and UN-4219's post-OCR cap applies | +| **Cancel mid-run** | the run stops, billing stops, the concurrency slot is released | +| **Page cap** | an oversized document is rejected before spend | +| **Sync-wait submit** | the result comes back inline | +| **Webhook on completion** | delivered with the right payload | +| **Unreadable PDF** | 400 at submit | +| **Metering** | page usage on the standard `Audit()` path, token and cost sums in `usage_summary` — the same trail as the IDE path | +| **Stages non-empty** | `GET /agent-kv/{job}` returns `stages: [{"name": "table_extraction", …}]` | + +That last row is load-bearing: it is the only check that catches Task 5a's cross-repo +constant drift **from the outside** — from a client's point of view, where the +symptom is a job that completes normally while reporting no progress at all. + +### 3c · Re-pointing the existing scenarios + +| Keep, re-pointed to `table_keys()` | Drop (KV-engine specific) | +|---|---| +| `test_submit_without_key_is_403` | `test_validate_good_and_bad_schema` (route unregistered) | +| `test_cancel_mid_run` | `test_happy_path_extraction` | +| `test_cancelled_job_does_not_leak_its_concurrency_slot` | `test_calculation_happy_path` | +| `test_delete_completed_job_then_result_404` | `test_hostile_calculation_fails_user_safely` | +| `test_page_cap_rejects_oversized_document` | `test_resubmit_same_document_hits_document_cache` (KV's cached `DocumentProcessor`; the table path is uncached until spec step 5) | +| `test_sync_wait_submit_returns_result_inline` | `test_bad_llm_key_ends_failed` (re-point if cheap) | +| `test_webhook_delivered_on_completion` | | +| `test_concurrency_limit_returns_429` | | +| `test_submit_unreadable_pdf_is_400` | | +| `test_excel_submit_extracts` — **keep, re-pointed.** The table engine has its own Excel branch and UN-4219's post-OCR page cap applies there | | + +Already table-native, keep as-is: `test_table_extractor_happy_path`, +`test_table_entry_without_a_target_table_is_400`, `test_a_kv_option_on_a_table_entry_is_400`, +`test_an_unknown_extractor_is_400`. + +Fixtures: keep `rent_roll.pdf` and `invoice.xlsx`; `invoice.pdf` only if a re-pointed +scenario still uses it. + +### 3d · Where each tier runs + +- **3a** — the unit and integration groups in `tests/groups.yaml`, every PR, no credentials. +- **3b** — the e2e group against compose on the PR, and against the dev namespace once + deployed (Task 5b). Gate the model-calling scenarios on the existing `require_llm` + fixture so a credential-less run skips rather than fails. +- **Cloud** — `tests/groups.cloud.yaml` carries `agentic_table`, `agentic_table_engine` + and `extraction_seams`. These go **red by design** until the OSS PR lands, exactly as + the `agentic_kv` groups do today. Say so in the PR description so a reviewer does not + read it as breakage. + +--- + +## Task 4 — The cloud half, minus the engine and the sandbox + +**Include:** + +``` +workers/plugins/agentic_table/ (9 files) +workers/plugins/agentic_table_engine/ (77 files) +workers/plugins/extraction_seams/ (5 files + Task 1's four new modules) +backend/plugins/agent_kv/ (5 files — the subscription gate) +charts/unstract-platform/templates/shared/agent-kv-secret.yaml +charts/unstract-platform/templates/shared/storage-secret.yaml +charts/unstract-platform/templates/backend/agent-kv-cronjobs.yaml +charts/unstract-platform/templates/external-secrets/external-secrets.yaml +charts/unstract-platform/templates/_helpers.tpl +charts/unstract-platform/templates/worker-v2/configmaps-specific.yaml +charts/unstract-platform/{values.yaml,unittests/agent_kv_cronjobs_test.yaml, + unittests/agent_kv_wiring_test.yaml,unittests/pg_worker_fleet_test.yaml, + unittests/render_guards_test.yaml} +charts/cloud-deployment-values/cloud.values.yaml +tests/groups.cloud.yaml, copy_cloud_deps.py (+ its test), .github/workflows/… +``` + +**Exclude:** all 97 `workers/plugins/agentic_kv/` files · `charts/unstract-platform/templates/worker-sandbox/` (5 files) · `unittests/sandbox_wiring_test.yaml` · `unittests/agent_kv_calculations_ga_test.yaml`. + +**Chart edits, not deletions:** +- `values.yaml` keeps the whole `AGENT_KV_*` block — the table path reads the same + env through Task 1's seams config — but drops the `agentic_kv` executor fleet entry + and the sandbox fleet. +- **`pg_worker_fleet_test.yaml` must be edited to assert the fleet actually shipped.** + Left untouched it either fails the build or, worse, passes while guarding a fleet + that no longer exists. The guard is the only thing standing between this deployment + and an unconsumed queue; a guard that guards nothing is worse than no guard. +- `agent_kv_wiring_test.yaml`: the `AKV-Q5` assertion on + `celery_executor_agentic_kv` comes out with the fleet entry. + +No new worker fleet is needed: `celery_executor_agentic_table` is already wired in +production for the IDE table path, which is precisely why the table work chose a +second *operation* over a new executor name (plan ruling R1). + +--- + +## Task 5 — The cross-repo constant, and a real deploy + +**5a. Pin the silent-drift constant.** `STAGE_TABLE_EXTRACTION = "table_extraction"` +(cloud `api_binding.py`) must equal `TABLE_STAGE_NAMES[0]` (OSS +`backend/agent_kv/constants.py:41`). Today **only a pair of comments ties them**. +On drift: `StageReportView` persists whatever the executor sends, `_status_document` +filters it through the OSS list, and every status response returns an empty `stages` +array while jobs complete normally. No test on either side can catch it alone. + +For a first customer-facing release this is the bug that reads as "your API is +broken". Cheapest fix: assert the literal on both sides, each test naming the other +repo's file and value, so a change to one fails the other's suite on the next sync. + +**5b. Deploy and exercise it.** Push to the dev namespace and run the table e2e +against the deployed stack. The PG consumer wiring and the agent-kv CronJobs have +never run outside compose ([UN-4213](https://zipstack.atlassian.net/browse/UN-4213)). +Dropping the sandbox removes the hardest half of that validation — no hardened pod, +no NetworkPolicy, no least-privilege DB role to prove out. + +**Exit criteria for the whole carve-out:** +- a `table` submit against the deployed stack returns rows for `rent_roll.pdf` +- `GET /agent-kv/{job}` reports a non-empty `stages: [{"name": "table_extraction", …}]` +- a `kv` submit returns 400, not 202 +- cancel mid-run stops the run and releases the concurrency slot +- the xlsx path extracts and is capped +- page usage and token/cost land on the usual `Audit()` path +- the full API suite (§3a) is green in CI without credentials +- `helm unittest` green with the reduced fleet, and `pg_worker_fleet_test` + demonstrably **fails** when a fleet entry is removed +- OSS PR merged before the cloud PR — the cloud table groups depend on the OSS routing + +--- + +## Task 6 — Documentation and the public contract + +- `docs/agent-kv-api.md:199` currently reads "Which extractor: `kv` or `table`". + It must say `table` is the only supported value on this deployment, and the + `/validate` section comes out with the route. +- Customer-facing docs in `unstract-docs` (Docusaurus). +- The `202` → poll `GET /{job}` → `GET /{job}/result` flow, the `target_table` + parameter, the `extractors.table` result shape, and the webhook. + +--- + +## Decisions + +**1 · Quota admission — deferred. Decided 2026-10-06.** +The entitlement gate ships: `backend/plugins/agent_kv/service.py` calls the same +`get_subscription` / `verify_subscription` the API-deployment middleware calls, so an +expired trial or inactive subscription gets a byte-identical 402. What is **not** +wired is quota — an org inside a valid subscription but over its page or token +allowance runs and bills anyway ([UN-4216](https://zipstack.atlassian.net/browse/UN-4216), +tagged a GA blocker). This release ships without it; revisit before open availability. + +**2 · The sandbox is excluded — decided 2026-10-06. Ship what exists.** +`agentic_table` generates Python with an LLM and executes it on **every run** +(`runner.py:1158`, `1340`) through its own `code_executor.py`, where an AST gate +blocking four builtins is the entire boundary: no rlimits, no isolated pod, no +NetworkPolicy. That pod holds LLM keys, OCR adapters and storage credentials and has +full egress. **This is exactly what the IDE table path does in production today**, so +the carve-out introduces no new class of risk — but it does widen reach from +"Prompt Studio users in the org" to "any holder of an API key". +Shipping as-is, with [UN-4215](https://zipstack.atlassian.net/browse/UN-4215) as the +immediate fast-follow rather than a backlog item. Pulling the sandbox in now +would also force the module-allowlist reconciliation (the table prompt permits 15 +modules, the gate allows 10; `copy`, `pathlib`, `string`, `textwrap`, `time`, +`unicodedata` are rejected) — which is the thing most likely to destabilise the +table path, i.e. the opposite of what this carve-out is for. + +**3 · Keep `unstract/agent-kv-schema` and `ValidateView`, unregister the route.** +The package is zero-dependency, self-contained and fully tested, and +`execution_serializers.py` imports `compile_schema` at module scope. Deleting it +forces a content merge in the file the large PR rewrites most; keeping it costs +twelve dormant files and leaves the large PR a one-line route restoration. + +**4 · Keep the URL `/agent-kv/`.** It reads oddly for a table-only API, but the first +customers' integrations are the thing a later rename breaks. Name it the agentic +extraction API in the docs and leave the path alone. + +**5 · One extractor per job stays.** `execution_serializers.py:283` rejects more than +one entry. That is exactly right for table and needs no work here. + +--- + +## Risks + +| Risk | Why it bites | Mitigation | +|---|---|---| +| Rebase cost on the large PRs | The carve-out touches `constants.py`, `execution_serializers.py`, `execution_urls.py`, `run-worker.sh`, `values.yaml` — files both large PRs own | Every subtraction is the deletion of a line whose code still exists. The large PRs re-add four lines and re-point 12 imports; no content merge | +| Temporary duplication in `extraction_seams` | Task 1 moves four modules out of `agentic_kv`, which the large cloud PR still carries copies of | Expected and bounded: the large PR's rebase re-points its 12 importers, completing spec §5 step 4 | +| A helm guard edited into uselessness | `pg_worker_fleet_test` is the only thing catching an unconsumed queue | Edit the assertions to the shipped fleet and verify the test **fails** when a fleet entry is removed | +| Uncached OCR on the table API path | Ruling R3: one LLMWhisperer pass per run until spec step 5 / [UN-4096](https://zipstack.atlassian.net/browse/UN-4096) | Cost, not correctness. Size it against expected customer volume before launch | +| Table reports one coarse stage | The engine has no node-level hooks, so `stages` is `["table_extraction"]` | Known and documented; do not invent stage names the executor cannot substantiate | + +--- + +## Sequencing + +Task 1 gates Task 4 (the cloud PR cannot build without it). Tasks 2 and 3 are OSS and +run in parallel with Task 1. Task 4 needs Task 1; Task 5 needs both PRs stood up; +Task 6 runs alongside from the start. OSS merges before cloud — the cloud table test +groups depend on the OSS routing. + +No week numbers: size this on a walkthrough. The honest shape is that Task 1 is the +only genuinely new engineering, Tasks 2–4 are subtraction and re-wiring of code that +already exists and already passes, and Task 5b is the validation nobody has done yet +for any of it. diff --git a/pyproject.toml b/pyproject.toml index d46632ef40..6019f21a17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ test-rig = [ ] [tool.uv.sources] +unstract-agent-kv-schema = { path = "./unstract/agent-kv-schema", editable = true } unstract-filesystem = { path = "./unstract/filesystem", editable = true } unstract-workflow-execution = { path = "./unstract/workflow-execution", editable = true } unstract-tool-sandbox = { path = "./unstract/tool-sandbox", editable = true } diff --git a/tests/compose/docker-compose.test.yaml b/tests/compose/docker-compose.test.yaml index 742d6b2b5e..7dcce8dd00 100644 --- a/tests/compose/docker-compose.test.yaml +++ b/tests/compose/docker-compose.test.yaml @@ -10,6 +10,18 @@ services: # Workers ask the backend for this, so this is the value that normally # takes effect for fan-out. - MAX_PARALLEL_FILE_BATCHES=${MAX_PARALLEL_FILE_BATCHES:-3} + # Lets the agent-kv e2e lane's page-cap scenario set a cap smaller than + # its 2-page fixture (default matches AGENT_KV_MAX_PAGES's own default + # in docker/sample.env; the test skips unless this is overridden <=2). + - AGENT_KV_MAX_PAGES=${AGENT_KV_MAX_PAGES:-100} + # Bucket-rooted object-store root for staged inputs/results; the first + # segment is the bucket and must exist. Must be identical to the value + # given to worker-executor-v2 below -- the executor reads back what the + # backend staged and caches OCR under the same root. + - AGENT_KV_STORAGE_DIR_PREFIX=${AGENT_KV_STORAGE_DIR_PREFIX:-unstract/agent_kv} + # Flips on the Agent-KV calc e2e lane (Task 12); unset/empty (the + # default) keeps it off so other lanes are unaffected. + - AGENT_KV_CALCULATIONS_ENABLED=${AGENT_KV_CALCULATIONS_ENABLED:-} platform-service: image: unstract/platform-service:${UNSTRACT_TEST_VERSION:-latest} @@ -28,6 +40,32 @@ services: worker-pg-executor: environment: - UNSTRACT_LLM_MOCK_RESPONSE=${UNSTRACT_LLM_MOCK_RESPONSE:-} + # Add the agent-kv executor's queue to the default set (docs/agent-kv-api.md + # §12 item 2) without dropping the queues other e2e lanes' tools dispatch + # to on this same worker -- this is a superset of docker-compose.yaml's + # own default, not a replacement for it. + # + # This MUST be WORKER_PG_QUEUE_CONSUMER_QUEUE, not CELERY_QUEUES_EXECUTOR: + # since UN-4046 made the PG transport unconditional this service runs + # `pg-queue-consumer`, which reads WORKER_PG_QUEUE_CONSUMER_QUEUE. + # CELERY_QUEUES_EXECUTOR is read only by workers/executor/worker.py -- the + # now-disabled Celery executor -- so setting it here drained nothing and + # the agent-kv lane hung in DISPATCHED with no error at the producer. + - WORKER_PG_QUEUE_CONSUMER_QUEUE=${WORKER_PG_QUEUE_CONSUMER_QUEUE:-celery_executor_legacy,celery_executor_agentic,celery_executor_table,celery_executor_smart_table,celery_executor_simple_prompt_studio,celery_executor_agentic_table,celery_executor_lookup_test} + # Agent-KV engine config, read by the cloud `agentic_table` executor plugin + # (not by backend settings) -- docs/agent-kv-api.md §12 item 5 / §13. + # `AGENT_KV_LLM_PROVIDER` is a v1 hard allowlist of `anthropic`|`openai`. + # Model names have no safe default -- Task 13b must supply real ones. + - AGENT_KV_LLM_PROVIDER=${AGENT_KV_LLM_PROVIDER:-anthropic} + - AGENT_KV_LITE_MODEL=${AGENT_KV_LITE_MODEL:-} + - AGENT_KV_ADVANCED_MODEL=${AGENT_KV_ADVANCED_MODEL:-} + - AGENT_KV_LLM_API_KEY=${AGENT_KV_LLM_API_KEY:-} + - AGENT_KV_LLMWHISPERER_API_KEY=${AGENT_KV_LLMWHISPERER_API_KEY:-} + - AGENT_KV_LLMWHISPERER_BASE_URL=${AGENT_KV_LLMWHISPERER_BASE_URL:-https://llmwhisperer-api.us-central.unstract.com/api/v2} + # Same root the backend stages into (see `backend` above) -- the executor + # reads `input_ref` from it and keys its OCR cache under + # `{prefix}/{org_id}/cache/`. Both sides MUST agree. + - AGENT_KV_STORAGE_DIR_PREFIX=${AGENT_KV_STORAGE_DIR_PREFIX:-unstract/agent_kv} worker-pg-fileproc: environment: @@ -39,6 +77,15 @@ services: environment: - MAX_PARALLEL_FILE_BATCHES=${MAX_PARALLEL_FILE_BATCHES:-3} + # Re-anchored onto worker-pg-ide-callback: UN-4046 replaced the Celery + # worker-ide-callback service with its PG-transport twin. + # Completion-webhook e2e: waive the worker's SSRF guards (https + public + # host) so a receiver on the compose host is reachable. Empty/unset (the + # default) keeps the guards mandatory; the gated webhook scenario skips. + worker-pg-ide-callback: + environment: + - AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=${AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE:-} + # The `ui` Playwright group drives a browser at the frontend's OWN origin, and # the app derives every API URL from window.location.origin. The production # nginx config serves only static files plus an SPA fallback, so `/api/v1/*` diff --git a/tests/e2e/agent_kv/__init__.py b/tests/e2e/agent_kv/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e/agent_kv/conftest.py b/tests/e2e/agent_kv/conftest.py new file mode 100644 index 0000000000..9b16da9a90 --- /dev/null +++ b/tests/e2e/agent_kv/conftest.py @@ -0,0 +1,283 @@ +"""Fixtures and HTTP helpers shared by the Agent-KV e2e tests. + +This lane exercises the Agent-KV *product* API (``/agent-kv/...``, top-level, +API-key authed -- not the tenant-scoped platform surface most other e2e +lanes hit). It needs a deployment where the cloud ``agentic_kv`` executor +plugin is installed (``plugins.get_plugin("agent_kv")`` truthy) -- an +OSS-only build 501s every submit (see ``docs/agent-kv-api.md`` §11). The +module-level skip in ``test_agent_kv_e2e.py`` keeps this lane from running +by accident under a plain OSS e2e sweep; this conftest assumes that guard +already passed by the time any fixture here actually executes. +""" + +from __future__ import annotations + +import json +import time +import uuid +from dataclasses import dataclass +from pathlib import Path + +import pytest +import requests + +from tests.e2e.conftest import _org_id +from tests.rig.runtime import PlatformEndpoints + +FIXTURES_DIR = Path(__file__).parent / "fixtures" +# A real, OCR-able 2-page invoice, generated for this lane with reportlab (the +# checked-in backend/agent_kv/tests/fixtures/two_page.pdf has no extractable +# text -- pdfplumber reads it as two blank pages -- so it can't stand in for +# an extraction fixture). It's a static binary checked into the repo; the e2e +# venv itself does not depend on reportlab to regenerate it at test time. +INVOICE_PDF = FIXTURES_DIR / "invoice.pdf" +# A small single-sheet invoice workbook (generated with openpyxl, checked in +# as a static binary like invoice.pdf): Excel exercises the no-pre-OCR-page- +# count path -- ``pages_total`` is None at submit and the engine enforces the +# post-OCR virtual-page cap instead. +INVOICE_XLSX = FIXTURES_DIR / "invoice.xlsx" + +# A real, OCR-able 2-page rent roll, generated for this lane with reportlab the +# same way invoice.pdf was, and checked in as a static binary (the e2e venv +# does not depend on reportlab to regenerate it at test time). The invoice +# fixture cannot stand in: it has no tabular region, so a table extraction +# against it would legitimately return no tables and the scenario would assert +# nothing. +RENT_ROLL_PDF = FIXTURES_DIR / "rent_roll.pdf" + +#: The table the rent-roll fixture contains, named the way the engine expects +#: (`target_table` is its one required extraction parameter). +RENT_ROLL_TABLE = "Rent roll" + + +def table_keys(target_table: str = RENT_ROLL_TABLE) -> dict: + """The `table` extractor's `keys` member. + + Every extractor entry carries `keys`; for the table extractor the thing + being asked for is a table, named by `target_table`. + """ + return {"target_table": target_table} + + +def table_result_body(resp: requests.Response) -> dict: + """The `table` extractor's own result out of an extractor-keyed payload.""" + return resp.json()["extractors"]["table"] + + +def table_stages(status_doc: dict) -> list[dict]: + """The `table` extractor's stage list out of an extractor-keyed status doc.""" + return status_doc["extractors"]["table"]["stages"] + + +# The 3 leaves the happy-path schema asks for; also used to build the schema. +INVOICE_FIELDS = ("invoice_number", "vendor_name", "total_amount") + +_DEFAULT_TIMEOUT = 30 + + +@dataclass(frozen=True) +class AgentKVAuth: + """A usable Agent-KV API key plus the backend root it's valid against.""" + + base: str # backend root, e.g. http://localhost:8000 (no trailing slash) + key: str # raw AgentKVKey UUID + org_id: str + + @property + def headers(self) -> dict[str, str]: + return {"Authorization": f"Bearer {self.key}"} + + @property + def exec_url(self) -> str: + return f"{self.base}/agent-kv/" + + def job_url(self, job_id: str, suffix: str = "") -> str: + return f"{self.base}/agent-kv/{job_id}{suffix}" + + +@pytest.fixture(scope="session") +def agent_kv_key( + platform: PlatformEndpoints, authed_session: requests.Session +) -> AgentKVAuth: + """Create a fresh AgentKVKey for this session via the tenant API. + + Session-scoped: one key, reused by every test in the lane (mirrors + ``api_deployment``'s ``api_deployment`` fixture). Key management sits + under the tenant-scoped platform surface + (``{base}/api/v1/unstract/{org_id}/agent-kv/keys/``), unlike the + execution endpoints this key then authenticates against. + """ + base = platform.backend_url.rstrip("/") + org_id = _org_id(authed_session, base) + prefix = f"{base}/api/v1/unstract/{org_id}" + name = f"e2e-agent-kv-{uuid.uuid4().hex[:8]}" + resp = authed_session.post( + f"{prefix}/agent-kv/keys/", + json={"name": name, "description": "agent-kv e2e lane"}, + timeout=_DEFAULT_TIMEOUT, + ) + assert resp.status_code == 201, f"create agent-kv key: {resp.text}" + body = resp.json() + return AgentKVAuth(base=base, key=body["key"], org_id=org_id) + + +#: Submit fields that describe the REQUEST rather than one extractor (§7.1). +#: The page range drives the shared OCR pass and the page cap, so it cannot be +#: per-extractor. +JOB_LEVEL_SUBMIT_FIELDS = frozenset( + {"page_start", "page_end", "timeout", "tags", "custom_data", "webhook_url"} +) + + +def kv_result_body(resp: requests.Response) -> dict: + """The `kv` extractor's own result out of an extractor-keyed payload (§7.3). + + The result endpoint returns `{"extractors": {...}, "usage_summary": {...}}`, + so a test asserting on the engine's own fields has to reach through one + level. Centralised here so the reach is spelled out once. + """ + return resp.json()["extractors"]["kv"] + + +def kv_stages(status_doc: dict) -> list[dict]: + """The `kv` extractor's stage list out of an extractor-keyed status doc. + + Stage reporting moved under `extractors.` (spec §7.2) because stage + names are extractor-specific -- `qa`/`challenge`/`codegen` mean nothing to a + Table Extractor. Centralised here so the reach is spelled out once. + """ + return status_doc["extractors"]["kv"]["stages"] + + +def submit_raw( + auth: AgentKVAuth, + file_bytes: bytes, + filename: str, + keys: dict | None, + *, + extractor: str = "table", + **fields: object, +) -> requests.Response: + """POST a submit request and return the raw response -- no assertions. + + For tests that need to inspect a non-202 outcome (403, 400, 429, ...). + ``keys=None`` omits ``extractors`` entirely (covers submits that intend to + fail before schema validation is even reached). + + Builds the extractor-scoped wire format (spec §7.0): per-extractor schema + and knobs go inside ``extractors``; only fields describing the request stay + top level. Callers keep passing knobs as plain kwargs and this routes them, + so a test still reads as "submit with this one thing changed". + """ + data: dict[str, object] = {} + options: dict[str, object] = {} + for k, v in fields.items(): + (data if str(k) in JOB_LEVEL_SUBMIT_FIELDS else options)[str(k)] = v + if keys is None: + # `extractors` is omitted entirely, so there is nowhere for per-extractor + # options to go. Silently dropping them would make a test look like it + # exercised a knob it never sent -- and it would still pass, because + # these submits are expected to fail on the missing field anyway. + assert not options, ( + f"submit_raw(keys=None) cannot carry extractor options {sorted(options)}; " + f"pass a schema, or move the field to JOB_LEVEL_SUBMIT_FIELDS if it " + f"belongs there" + ) + else: + data["extractors"] = json.dumps( + [{"name": extractor, "keys": keys, "options": options}] + ) + return requests.post( + auth.exec_url, + headers=auth.headers, + files={"file": (filename, file_bytes, "application/octet-stream")}, + data=data, + timeout=60, + ) + + +def submit( + auth: AgentKVAuth, + file_bytes: bytes, + filename: str, + keys: dict, + *, + extractor: str = "table", + **fields: object, +) -> tuple[str, str]: + """POST a submit request expected to succeed; return (job_id, status_url). + + Asserts the 202 handshake (spec §7.1 / docs §3) so every caller only ever + polls a genuinely dispatched job. A 501 here almost always means the + cloud ``agentic_table`` executor plugin isn't installed on this deployment + -- this whole lane requires it (docs §11). + """ + resp = submit_raw(auth, file_bytes, filename, keys, extractor=extractor, **fields) + assert resp.status_code == 202, ( + f"submit: HTTP {resp.status_code} (expected 202; a 501 means the " + f"agentic_table executor plugin isn't installed on this deployment): {resp.text}" + ) + body = resp.json() + job_id = body["job_id"] + status_url = body["status_url"] + assert job_id, body + return job_id, status_url + + +TERMINAL_JOB_STATUSES = {"completed", "failed", "cancelled"} + + +def poll(auth: AgentKVAuth, job_id: str, timeout_s: float = 600) -> dict: + """Poll the status document until the job reaches a terminal state. + + Returns the last status document. Fails loudly (rather than looping + forever) once ``timeout_s`` elapses. + """ + deadline = time.monotonic() + timeout_s + last: dict = {} + while time.monotonic() < deadline: + resp = requests.get( + auth.job_url(job_id), headers=auth.headers, timeout=_DEFAULT_TIMEOUT + ) + assert resp.status_code == 200, f"status poll: {resp.status_code}: {resp.text}" + last = resp.json() + if last.get("status") in TERMINAL_JOB_STATUSES: + return last + time.sleep(2) + pytest.fail(f"job {job_id} not terminal within {timeout_s}s; last status: {last}") + + +def result(auth: AgentKVAuth, job_id: str) -> requests.Response: + """GET the result endpoint and return the raw response (any status).""" + return requests.get( + auth.job_url(job_id, "/result"), headers=auth.headers, timeout=_DEFAULT_TIMEOUT + ) + + +def cancel(auth: AgentKVAuth, job_id: str) -> requests.Response: + return requests.post( + auth.job_url(job_id, "/cancel"), headers=auth.headers, timeout=_DEFAULT_TIMEOUT + ) + + +def delete(auth: AgentKVAuth, job_id: str) -> requests.Response: + return requests.delete( + auth.job_url(job_id), headers=auth.headers, timeout=_DEFAULT_TIMEOUT + ) + + +def invoice_schema() -> dict: + """A 3-leaf schema matching ``fixtures/invoice.pdf``'s obvious fields.""" + return { + "invoice_number": {"description": "The invoice number", "required": True}, + "vendor_name": {"description": "The vendor or supplier name issuing the invoice"}, + "total_amount": { + "description": "The total amount due on the invoice", + "format": "currency", + }, + } + + +def invalid_schema() -> dict: + """A schema that fails to compile: a leaf missing the required 'description'.""" + return {"total": {"format": "currency"}} diff --git a/tests/e2e/agent_kv/fixtures/invoice.pdf b/tests/e2e/agent_kv/fixtures/invoice.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a79fdf55c4f6785ce6a0b4376f0c278cfb3786f9 GIT binary patch literal 2435 zcmdT`$9n2U6yED8#>N;hWmFJCfB{pDO*7S)Vn)&k1gJ~NbryMnWS4!OH6vUu*STMA zzU-ofG;?(7f67d_Uuz_p6i0>s`2Dv(!2nyPm8Id>H2Osi>=sGbKnEnS-$%=VU{*68M zU)oEb*sBs(2uLmvgZKgLU`0a}^9z`hJqr=;jqhzu z2Skx!8ZWx(yD(!0@UU3m0xp{l6uBM&TBn-GWgA5wmKKqr z9P7eZ^%3LhEJ5qHb-z6;_YpIGf_60JPaFj%A2w^<)^|zxpmpT_nWROp&YNnYNt+?N#cAjW0hvlh|UFCccLem~)mrm`bGv-`efCsgHi)LtdHjS4yrhJ!3 z3W0e1KZ&fd^X*0(xV=R^zN2z-cO#=*J&m( zXbE@ur}N!plIe-FP_z)6HoARtJlj9l9+{AJk=f*}Fl|SQu3?Q0!QE^h7Z1s1VKh=M z_d`ppMV&lfTJKt8NAy%^&zIq1+pfezxp_`;`fBEBvCY%sV&UgolZjOgr&(>hO>@q# zMKZVFGVwOSj|1Vqrk3NsNv&fRN|PIL7kRq#qp3zbVPv}5f?MRzw+dbAZH2TAmHOOD z3PNIrINK^hQ9&~UHg|pgirov-64Q0>-RHBOR)1XTK3itvkkfMqEiQHT^;PZ8t6qdh zso5Z^im6%QP;Wk=ycFMBJ-uA;OCDR+VQ5ynl1h%R_80g^K;YMzQL{rQcu~~ch$soG5s)omW^0#nyI+)i&hpVq_tF)xlT8?vSfE} zuNRH+m_fW^>r1;kMS8bVu9lcY!$UVI?j{}^-P_yVP10PT!R14B zc;!Bg`DXRH6n|a_{n>M@PEXZbBXa?{SvRyQMH&9$vIDQalc_HbRw)`V3`KuDAj9(< z4^;3$%a8*mc!2RiV`y?rf1iBF)(Jo!u$$guvF#?TxYzMp8{ z@rB_}WjzOJ2kacL9<3Un<^@H-Sy>U-EK+GsiK{_Hx?>rzNPa6l98UO$g0+P(xtknAMX*|RH8pd1{ zn9IS#UeDXZ(?{qpPfx)>H`jZ}dn7%=r1*>75Bytl5?CPD?#d?=b|MA+Vw!o~qEEl= z;3~QY1@$wQ2*$H>H)pHv39h<{iT05xO>&$n3=R}06t9NVh|EVO)iaPoSXpp-OWdX+ zVnj%#hL&6EHYWtt9Q}}4$^oxs^tpSNO~qyEc~~pwn20IXgSiwEf!fl~`S30i4F+ip z3O^5coH^ii6SULm{gHwsQ`Es0X6b`i008B`Q?U2&2K~$dng-MD6($ASfWEx(o2l2l zYbJC<_Xe8?5nRB#v)VjEEndH)g-5`GAd0c?`10(dF?Ddf)rv_>@59XOgVd?o>O?0((=75oruy;Ktts!7}cXvv)-FS=dME|T2C6r!lV7E=y6@% zr>_u#^n0Fxh;Ltf6y_3>hCeJj?Gid&gF}i8{oLi{XpA~LbH<{Ekd6bO++~~J!jdT}gG`$f%;!(b;YaX{`*aLs-;;3l+s1G%!5fSx1 zEEBI?6J0t^LnulnD^p-P;vb5J?-V5s%=n7CF)Xg^1xhs!jwO0B;V$ZT6!sJxx^_eE z5H*%Mk2*}FXKK>QUL_mcAO9YLVfBaFv{CG+{qIxuttZVOnY;R3GqH@%C z%bAgC%8_Nh!YPSv7*f%a8O zsU}S_QJ^AW0XW-5j|m$>B$8DOK5j%*Q7!_*TDl1P7n0+?S9;{in-rUB&O{$ms4*<( z`+W96x&9!4s-e(EopYtnO=+;6(=Uj>lHh+g8Tjn&UUj}4GmvUHlLY?`_&Fyx)l2Ee z_cVYPe9)PT{Bfn-Od-xwt+c1k4IJgZ+`}7U3^27?M!#66F}dIiypwzhVx}}kW^6~s z1~s;5=Z;;LTF#W*>K;lyWd*zIw;8;fXd|1wUPjxRQz1}+x?_!$XJ%Cr849-j+rw#o zBa>;@DATPaG5;M&Or`8*0ZVD+?v!|$;+_{;_iDm6q6QzHw>}1c5Od{E`K~@er?pDX zU9Pw(Do#+EYew`Y+SsM&Rjjx|1&b&#-d-Dv!dh@3v1AsaQ?Di}1nUwtBiU1V9Q9T@ zTrdm?{nmCUUp&?#$r0uV8&9k`|4}0{GbJu~)852;xWCV^HiLIsy6ElpxsyWS z-p=Vq&gj})GXW0TRPD%yJDja&i37X0;6xh^7FxVpbSq4dE9p%!Qn*&}jOZ-4v z`LA+7g$vB`FPhR_qIc5|)bWda*=PO(M4XERreQ>QtHx@P-)m_(v@kI99%c(TIbV*8%cGmiZxR`r|(?(+~=ih z&`RasOpYzu)*<~e7$o9Yw!a`iO2lswXGXPr&uZtxdR~4_FuVWh^8lFb{<-ISb@DaY zb(=#gm72%s`IS@m3|S4G{GfM~SiY**9M72WY3W2)^}eR3;sddUGvD9c#g1|)V~WT= zC3&+?+Qv&WQmr5zuFC%etZO~SMMa%ZM~cTr=e_dnTZT(O($pz`q+7J1sKW=k0hM@| zyqLGjv?13)%?vk5%uzP_3&*iN3gnT2NyxKu&<@gtWREWYmaKx zrRgNgiRgl~=d8>p9P7_Oeig`LSXe?r1ZAaNS9|UK6Rv#O#*n?p8+m7wvk>(BB5CME zr8(4rk5W;aAe1#7-#*FWP~7GzwOw^teKlf6gfu9^^nkSZq0~aGYA%D=hr}xBWMYpy zAC%St!|dlyMIGV_@crnul^=zaq7aZGZIu$Iewcms;;v99AzU(@n- zx(MS^!G+LnX83IfY-?F_sk0RsVZA`Hj%Z1c5m#5n3LBbX#lrPKV*PH{NM!tqcwAIVjD$ROyBXm)gq>hI z;==5DL=HHGr<_SkRW+V-yBx+JoUQJK^J=mmD0( zH({< zx{s&519>8w3mDVWIIf>~kG`Gl{ITq4IRWij3EAfCV@V+Er~B)2A=k+#GtZ1CVTS*B z7v(Va?l>hTzCn0TFZ5aCE2WZzRgFk@gYS3x-h5pw42%{E!wQk2jYbu4-eWZ=WB5$f zgvMq4)VTSBCaxyez`{}bAbi@lM0+Ye)NNhUsYh@2ja21v#JSftFDUSX+m6qEK|hWF zja{9jPOGeFc$Qq#`uN?%S3!@5F01i8z%fCSN;80lZaU?T_wlX#H zV+@tq2Q~AKVUE&|g4ua<1Rgf98|ZBKHjF))xi!TjE9TUq>*$>BW?YS$8kI}N>dxXV z>=bR&<>OX$Q?(q*Of9FkiS8fKum`Sx@GLQ?xTK*QQhyFfY0Df%fSqUuqt(9wNqB{B z(K?g58Z-N(SiCvX<&4<~ac#8GEikEX*%r46)$yQTS{j@{+@AZ?#Hf<@Y_*yLO!GrU zIDd0PpK(2jy^_+^Lp)Jyq!G2-7GOExC{xS2V-;zi2T#5niL6(_F@4zd)iU<}`uU#NvQWp+Ed?_%+W)ECT7Xq$;H6c{A8mQoLIc27#^p`stJck?1 zcgH=?yJU+N>SxGLtgyOYEeS9}LfRs>4qd`mlIabD%#Q9RCs)>>O=Jdwyb26#bT+`O zK?cb+^HVUL&ZWT=6Wh+1<|S+j2UQ9s+#Yj1dwEOI+Mn=Ei3!ydE4O6A=#FV51l~IA#kwkRKdbXxl!W@;YGrQ+zGzTM2J5qNBwK zkOcFz@dmm22nqhY7o=;uV~da~)lz|MbY8--MXILjRN_clqoq{gL`I}eXXKkF7i)WIVGS^h#@igRie&DZG zgcwel_J9XQqE>thL5UMjP)V!GPu%o4b`+~dgGY^=0MogYAPJc!>w6>CVd>WXa6Lp( z3BxmaW!A@Zc6s^2wvqTJVkW(_5nB8*je+1>COKNlD(4(QavAn+{!ku!ZwW?%klmy; z)*w2!t}S*_;;m(Yha(@@fwsvCyLBSFP1$mh{7#) z+r5e(r}b{~<4I3c2eP|Yq9{l<*$Zt-NY9#2AL_m!cJbAdC*g%h!f&mW)ssR_j)u35 zw*wG!@#y734HLzYvWA*&z@G$*ZLPFdV~G8MA(`P9!N0`xZ<_xWR#Te%wI{-)F||}{ z0Q!oFV(c=(DjK@Ds1wS_yY-blJB%=Z26MZx88WN;yMv>UctVJ*{*)}$ffM%tsBfDE z0Fh;)QJq|(=1x8;UA_@UqPuygiO`v80mgd$b$^PEzQDChwwba`35Dv%Po?(Ag+r@Y zD^whKZ>?FZTK2f2Z^O*fZ>BtAnrkvH^K7bwor;&f!O!)ju=jYn3V@Kz9;HNG13=TEhH{a5 zN29hR6a!cM%JSk7xz?WQ%{K3T7~-y_r*dI@Lmb;7-;&{Ra71Pj*p(0XMx)QUqyc-= z32ShW!Lgmx&y9NH1v*7rD~Sk#z>mlmP)EI^5#LMw>+I98yKQQl#_EI{G3Cc5-0<}R z*Uiere&y7DOX<~Uhnv@p~4y3ccF9e?!?ZbHu+o z`BmW6UiceGa_yg$|EDuv1z)Z6-(XivU&So=e^mX|I9E&QcN~3;Rrg;COHUgQP0UXRFXalv4B^ON9RT{kK1Xf=-t0cHkPw$V4`Dkp?ga&@A+?H{VSmdj|dB;0XaOc-Io*IjPM)py$}9_WnHQARR4>rlwcX!kLY85 zP+$5=Uk#grL2^LO^B<61r0GzwzJM9tQy@b#fX^{t0muQgJv6hgd8LU;}*k z3NQ|{z618*0PWfPfAJu5<$>p1*V1raj2+aoOuX<&&O6kvIa7l!G_36v3jCFf>e<~{ zHi$ygGGC6?*@*$NW+Uk8mMMDJM#VfsF&QdH^K_QW(OiP!A`}&Q=es@JMlU~Gw&s8+ z=D^l{PY8~>c2*HK;cGp5;qT~c8`>`*1?E?EIFH3H(Z%JlM=CDSv+10Ws&90pVSOXr zHL%HN)Abe?iVaUET-c-p4d*%)ymb7U3it#rX|k^D-^xF ze-|L>hKpUmnOhy!n$0XHB3$EM!5SFDw+)umLFU%o0X9c@f*cnEeI#V)w7NN_M#m$= zX<-&QsO+|7t(0>)5wjo11%(;K%VcBngw$ka8si74@p`vR($T6(i*>ac)7|@ZvQXk0 zf^XG#s{H04CDMLsxrK#Bcs3f3eb68+J}_Oz$5MYS-AKI)G~c^BRls#8Ip zG)m2!S}XgHeb!%2W=@p_9op97a7%ofY6#tr3++Ff%` z{k!M=k!nPPtGZa+Q{{Gx^xImjAq>*9dYu*O{=>PpX@}>PxyT&~>YbcM4-Ye;M=tHq zy+~JEb46vtYu~KE@m3{p3X>;ykXYW)J4kM`Nw)%J($gQ zc~se2<6ld-d%wlwt&d;Y!ha2LHggl;jv62n=Sb)CcD|}>>4@|&D3equ6oUN3w@y6M z4{~T4->2^5fpRtk&m@4xPETYzTl+0(u&l2GvSMUS(W-U71RUaytG>vooUkO(8qX|irY;+ zB|j$?+<+ST;w>TU-znyNpA##qmLOGT$I|#Q9qAi^V9l6@cd5{*xSu0t%Rk7qjM(Zh z0WNbN?)YJCGfw5~MYg%?X{e}%%_mpaeBE=Ld4z6sK5drdlaeS(5;Llci2>EnIq5tN z--{>t5URDJteD?FJ6WO6=jP_d4z`D_cH|gLu9gF&SbyY6!9$w5E7kTc@=g$3aHpH!P$v8J51!u^>WMSEuc_uTH5H z&~!YCZZ1&-*)%s6@CJ}V;OT9-z#C|IJ=A|eUN|mo5!&u0Kn%s@h)}5Bmx#XrG+e;g literal 0 HcmV?d00001 diff --git a/tests/e2e/agent_kv/test_agent_kv_e2e.py b/tests/e2e/agent_kv/test_agent_kv_e2e.py new file mode 100644 index 0000000000..b04578cea6 --- /dev/null +++ b/tests/e2e/agent_kv/test_agent_kv_e2e.py @@ -0,0 +1,709 @@ +"""E2E: the Agent-KV product API against a real running platform. + +This lane needs the cloud ``agentic_kv`` executor plugin (docs/agent-kv-api.md +§11) -- an OSS-only deployment 501s every submit, so it is NOT part of a plain +OSS e2e sweep. The whole module skips unless both: + +- ``UNSTRACT_BACKEND_URL`` is set (the usual e2e platform-URL gate every + other lane in this tree relies on), AND +- ``AGENT_KV_E2E=1`` is set explicitly -- so this lane never runs by + accident just because a platform happens to be up; it must be opted into + for a run that actually ships the plugin. + +A second, narrower gate (the ``require_llm`` fixture below) applies only to +the two scenarios whose assertions depend on a job actually reaching +COMPLETED (real extraction happened): either ``AGENT_KV_LLM_API_KEY`` or +``UNSTRACT_LLM_MOCK_RESPONSE`` must be set. Every other scenario here is +gated purely by request-time checks (auth, schema compile, the concurrency +limiter, the pre-OCR page cap) that resolve before any LLM call, so they +don't need either. + +Values extracted under ``UNSTRACT_LLM_MOCK_RESPONSE`` are whatever the mock +config returns, not real answers -- assertions here deliberately check +*structure* (the requested keys are present) rather than field values, for +both the mock and a real-key run, to keep this lane's outcome independent of +model behavior. + +Test order matters: the concurrency scenario is deliberately defined LAST +(see its own docstring) and ``tests/groups.yaml``'s ``e2e-agent-kv`` entry +pins ``parallel: false`` for the same reason -- see that scenario for why. + +**A third, operator-driven gate: ``AGENT_KV_E2E_BAD_KEY_JOB=1``.** One +scenario here (``test_bad_llm_key_ends_failed``) needs a *dispatched* job to +end FAILED, which no request-time check can produce -- the only deterministic +way to get there is to run the platform with a deliberately invalid +``AGENT_KV_LLM_API_KEY`` so the executor's LLM calls all fail. That is a +whole-stack configuration, not something a test can arrange, so the scenario +skips unless an operator sets ``AGENT_KV_E2E_BAD_KEY_JOB=1`` to declare "this +stack is running with a bad Agent-KV LLM key on purpose". Note that every +other scenario in this module that needs a job to COMPLETE will (correctly) +fail on such a stack -- run this one on its own, e.g.:: + + AGENT_KV_E2E=1 AGENT_KV_E2E_BAD_KEY_JOB=1 pytest tests/e2e/agent_kv -k bad_llm_key + +What it asserts is the *shape* of the failure, not its text: the job reaches +``failed`` and the ``error`` the customer sees is user-safe -- no filesystem +paths, no ``Permission denied``, no provider response body (spec Sec 8's +``" failed: "`` contract). +""" + +from __future__ import annotations + +import concurrent.futures +import json +import os +import time + +import pytest +import requests + +from tests.e2e.agent_kv.conftest import ( + INVOICE_PDF, + INVOICE_XLSX, + RENT_ROLL_PDF, + AgentKVAuth, + cancel, + delete, + poll, + result, + submit, + submit_raw, + table_keys, + table_result_body, + table_stages, +) + +if not (os.environ.get("UNSTRACT_BACKEND_URL") and os.environ.get("AGENT_KV_E2E") == "1"): + pytest.skip( + "agent-kv e2e lane needs UNSTRACT_BACKEND_URL and AGENT_KV_E2E=1 " + "(this lane requires the cloud agentic_kv executor plugin -- see " + "docs/agent-kv-api.md §11)", + allow_module_level=True, + ) + +pytestmark = [pytest.mark.e2e] + +_INVOICE_BYTES = INVOICE_PDF.read_bytes() +_RENT_ROLL_BYTES = RENT_ROLL_PDF.read_bytes() + +# Bounded wait for the org's concurrency slots to free up again. Used by the +# cancel scenario (a cancelled job must not leak its slot) and by the +# concurrency scenario's own cleanup. +_SLOT_DRAIN_TIMEOUT_S = 120 +_SLOT_DRAIN_POLL_INTERVAL_S = 3 + + +def _validation_error_attrs(body: object) -> set[str]: + """The field names a 400 body blames, across BOTH error envelopes. + + This lane runs against whichever deployment is up, and the two differ: + + - OSS DRF default -- ``{"file": ["Unreadable PDF"]}``: the field names are + the top-level keys. + - Cloud (``drf-standardized-errors``) -- + ``{"type": "validation_error", "errors": [{"code": "invalid", + "detail": "Unreadable PDF", "attr": "file"}]}``: the field names are the + ``attr`` of each entry under ``errors``. + + Returns an empty set for anything that is neither shape, so a caller's + ``assert "file" in _validation_error_attrs(body), body`` fails loudly with + the real body rather than raising a ``KeyError``/``TypeError``. + """ + if not isinstance(body, dict): + return set() + errors = body.get("errors") + if isinstance(errors, list): + return { + entry["attr"] + for entry in errors + if isinstance(entry, dict) and isinstance(entry.get("attr"), str) + } + return {key for key in body if isinstance(key, str)} + + +def _cheap_submit(auth: AgentKVAuth) -> requests.Response: + """One raw submit with QA and challenge off -- the cheapest job this API + will accept, used purely to occupy/probe a concurrency slot. + """ + return submit_raw( + auth, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + ) + + +def _drain_until_slot_free( + auth: AgentKVAuth, timeout_s: float = _SLOT_DRAIN_TIMEOUT_S +) -> bool: + """Probe with cheap submits until one is ACCEPTED (proof a slot is free) + or the bounded wait runs out. Any accepted probe is cancelled again so it + doesn't hold the slot it just proved was available. + + Returns True if a probe was accepted, False if the window expired or the + probes stopped being answerable. Never raises -- callers decide whether a + False is an assertion failure (the cancel scenario) or merely a + best-effort cleanup giving up (the concurrency scenario). + """ + deadline = time.monotonic() + timeout_s + while time.monotonic() < deadline: + try: + probe = _cheap_submit(auth) + except Exception: # noqa: BLE001 - probing only + return False + if probe.status_code == 202: + try: + cancel(auth, probe.json()["job_id"]) + except Exception: # noqa: BLE001 - cleanup only + pass + return True + time.sleep(_SLOT_DRAIN_POLL_INTERVAL_S) + return False + + +@pytest.fixture +def require_llm() -> None: + """Skip a scenario that needs a job to actually reach COMPLETED. + + Every other scenario in this module resolves before any LLM call + (auth/schema/limiter/page-cap checks); only the two that poll a job all + the way to a genuine COMPLETED result need this. + """ + has_key = os.environ.get("AGENT_KV_LLM_API_KEY") + has_mock = os.environ.get("UNSTRACT_LLM_MOCK_RESPONSE") + if not (has_key or has_mock): + pytest.skip( + "needs AGENT_KV_LLM_API_KEY or UNSTRACT_LLM_MOCK_RESPONSE so the " + "job can actually reach COMPLETED" + ) + + +# --------------------------------------------------------------------------- +# 1. Auth +# --------------------------------------------------------------------------- + + +def test_submit_without_key_is_403(agent_kv_key: AgentKVAuth) -> None: + resp = requests.post( + agent_kv_key.exec_url, + files={"file": ("invoice.pdf", _INVOICE_BYTES, "application/pdf")}, + data={"keys": '{"total": {"description": "Grand total"}}'}, + timeout=30, + ) + assert resp.status_code == 403, resp.text + + +# --------------------------------------------------------------------------- +# 2. Validate +# --------------------------------------------------------------------------- + + +def test_table_extractor_happy_path(agent_kv_key: AgentKVAuth, require_llm: None) -> None: + """A `table` entry runs the table engine and files its result under `table`. + + Before the job row recorded its extractor, every response keyed by the + hardcoded `kv` name -- a table job's output would have been filed under the + wrong extractor, which no unit test on either side would have caught. + """ + job_id, status_url = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + extractor="table", + ) + assert status_url.endswith(job_id), status_url + + status_doc = poll(agent_kv_key, job_id, timeout_s=600) + assert status_doc["status"] == "completed", status_doc + # Keyed by the extractor that ran, and by that one only. + assert set(status_doc["extractors"]) == {"table"}, status_doc + + # R7: the stage is recorded AND visible. StageReportView persists whatever + # name the executor sends, so a KV-only stage-name filter would leave this + # list empty while the job still completed. + stages = {s["name"]: s for s in table_stages(status_doc)} + assert "table_extraction" in stages, table_stages(status_doc) + assert stages["table_extraction"]["status"] == "done", stages + + resp = result(agent_kv_key, job_id) + assert resp.status_code == 200, resp.text + payload = resp.json() + assert payload["success"] is True, payload + assert payload["status"] == "completed", payload + + body = table_result_body(resp) + # Structure, not values: this lane runs under a mock LLM as well as a real + # key, so field values are not assertable (see the module docstring). + assert isinstance(body["tables"], list), body + assert "page_count" in body, body + assert "row_count" in body, body + + assert set(payload["usage_summary"]["by_extractor"]) == {"table"}, payload + + # Re-readable until TTL (D11), same contract as the kv path. + again = result(agent_kv_key, job_id) + assert again.status_code == 200, again.text + assert again.json() == payload, "result must be stable on re-read" + + +def test_table_entry_without_a_target_table_is_400(agent_kv_key: AgentKVAuth) -> None: + """Rejected at submit, before any paid work -- `target_table` is the + engine's one required extraction parameter. + """ + resp = submit_raw( + agent_kv_key, _RENT_ROLL_BYTES, "rent_roll.pdf", {}, extractor="table" + ) + assert resp.status_code == 400, resp.text + assert "target_table" in resp.text, resp.text + + +def test_a_kv_option_on_a_table_entry_is_400(agent_kv_key: AgentKVAuth) -> None: + """Per-extractor options exist so an option aimed at the wrong extractor + cannot be silently dropped and change what the caller is billed for. + """ + resp = submit_raw( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + extractor="table", + qa=True, + ) + assert resp.status_code == 400, resp.text + assert "qa" in resp.text, resp.text + + +def test_an_unknown_extractor_is_400(agent_kv_key: AgentKVAuth) -> None: + resp = submit_raw( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + extractor="nonexistent", + ) + assert resp.status_code == 400, resp.text + + +# --------------------------------------------------------------------------- +# 4. Failure body (submit-time rejection) +# --------------------------------------------------------------------------- + + +def test_submit_unreadable_pdf_is_400(agent_kv_key: AgentKVAuth) -> None: + """A ``.pdf``-named file that isn't a real PDF is rejected before dispatch. + + This is the submit-serializer's own page-count check + (``pdfplumber.open()`` raising -> ``{"file": "Unreadable PDF"}``), which + is deterministic and needs no LLM/engine work to reach. The executor's + own FAILED-terminal path (a job that *dispatches* successfully but the + engine later fails) has no deterministic trigger reachable without a + real key/schema combination that's guaranteed to fail -- that path is + covered by the unit suites instead (e.g. + ``backend/agent_kv/tests/test_job_views.py``, + ``backend/agent_kv/tests/test_dispatch.py``). + """ + garbage = b"this is not a pdf file, just garbage bytes" * 20 + resp = submit_raw(agent_kv_key, garbage, "garbage.pdf", table_keys()) + assert resp.status_code == 400, ( + f"expected 400 (a 501 here means the agent-kv engine plugin isn't " + f"installed on this deployment): {resp.text}" + ) + body = resp.json() + assert "file" in _validation_error_attrs(body), body + + +# --------------------------------------------------------------------------- +# 5. Cancel +# --------------------------------------------------------------------------- + + +def test_cancel_mid_run(agent_kv_key: AgentKVAuth) -> None: + job_id, _ = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + ) + cancel_resp = cancel(agent_kv_key, job_id) + # Either cancel won the race (200) or the job was already terminal by the + # time it landed (409) -- both are correct per docs §7; either way the + # status endpoint must agree with what cancel just reported. + assert cancel_resp.status_code in (200, 409), cancel_resp.text + cancel_body = cancel_resp.json() + + status_resp = requests.get( + agent_kv_key.job_url(job_id), headers=agent_kv_key.headers, timeout=30 + ) + assert status_resp.status_code == 200, status_resp.text + final_status = status_resp.json()["status"] # lowercased, per docs §4 + + if cancel_resp.status_code == 200: + assert cancel_body == {"status": "cancelled"}, cancel_body + assert final_status == "cancelled", status_resp.json() + else: + # docs §7: the 409 body's status is the RAW uppercase enum value, + # unlike every other status field in this API. + assert cancel_body["status"] == final_status.upper(), (cancel_body, final_status) + + +def test_cancelled_job_does_not_leak_its_concurrency_slot( + agent_kv_key: AgentKVAuth, +) -> None: + """A cancelled job must give its concurrency slot back. + + ``JobCancelView`` marks the job ``CANCELLED`` directly and never touches + ``AgentKVConcurrencyLimiter``; the slot is released by the job's own + *finalize* call, in a ``finally`` (docs/agent-kv-api.md §11a). So the + release is asynchronous -- it lands whenever the executor that picked the + job up notices the cancellation and calls back -- which is exactly the + kind of path where a leak hides: nothing in the cancel request itself + would fail if the release were dropped, and the only other backstop is + the limiter's 6-hour slot TTL. + + This is a bounded probe, not a saturation test: it proves the limiter is + still handing out slots after a cancel (i.e. a cancel cannot wedge it), + and it reuses the same drain helper the concurrency scenario ends with. + It deliberately does NOT saturate the org first -- doing so here would + make every scenario defined after it flaky, which is why the one + saturating scenario in this module is defined last. The saturated form of + this check is that last scenario's own drain. + """ + job_id, _ = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + ) + cancel_resp = cancel(agent_kv_key, job_id) + assert cancel_resp.status_code in (200, 409), cancel_resp.text + + assert _drain_until_slot_free(agent_kv_key), ( + f"no submit was accepted within {_SLOT_DRAIN_TIMEOUT_S}s of cancelling " + f"job {job_id}; the limiter is refusing new work, which means a slot " + f"was leaked rather than released by finalize" + ) + + +# --------------------------------------------------------------------------- +# 6. Delete +# --------------------------------------------------------------------------- + + +def test_delete_completed_job_then_result_404( + agent_kv_key: AgentKVAuth, require_llm: None +) -> None: + job_id, _ = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + ) + status_doc = poll(agent_kv_key, job_id, timeout_s=600) + assert status_doc["status"] == "completed", status_doc + + del_resp = delete(agent_kv_key, job_id) + assert del_resp.status_code == 204, del_resp.text + + after = result(agent_kv_key, job_id) + assert after.status_code == 404, after.text + + +# --------------------------------------------------------------------------- +# 7. Page cap +# --------------------------------------------------------------------------- + + +def test_page_cap_rejects_oversized_document(agent_kv_key: AgentKVAuth) -> None: + fixture_pages = 2 # fixtures/invoice.pdf + cap_raw = os.environ.get("AGENT_KV_MAX_PAGES") + if cap_raw is None or int(cap_raw) > fixture_pages: + pytest.skip( + f"AGENT_KV_MAX_PAGES={cap_raw!r} exceeds the fixture's " + f"{fixture_pages} pages; set AGENT_KV_MAX_PAGES<={fixture_pages} " + "on the backend service to exercise the cap" + ) + resp = submit_raw(agent_kv_key, _RENT_ROLL_BYTES, "rent_roll.pdf", table_keys()) + assert resp.status_code == 400, resp.text + body = resp.json() + assert "file" in _validation_error_attrs(body), body + + +# --------------------------------------------------------------------------- +# 8. Executor-side failure (operator-configured bad LLM key) +# --------------------------------------------------------------------------- + + +def _assert_user_safe_error(error: str) -> None: + """No internals in a customer-visible error string (spec §8).""" + lowered = error.lower() + for leak in ("permission denied", "traceback", "errno", "sk-", "api_key"): + assert leak not in lowered, f"internal detail {leak!r} leaked into: {error!r}" + # No filesystem paths: the engine's own text used to carry the executor's + # tempfile/page-image directories verbatim. + for leak in ("/app/", "/tmp/", "/var/", "storage/processing", "\\"): + assert leak not in error, f"a filesystem path leaked into: {error!r}" + + +def test_bad_llm_key_ends_failed(agent_kv_key: AgentKVAuth) -> None: + """A job that DISPATCHES and then fails in the executor ends ``failed`` + with a user-safe error. + + Every other failure scenario in this module is a request-time rejection + (400/403/429) that never reaches the engine. This one covers the other + half of the contract -- the terminal FAILED path -- and it needs the whole + stack to be running with a deliberately invalid ``AGENT_KV_LLM_API_KEY``, + which no test can arrange for itself. Hence the explicit operator gate + (see the module docstring for how to run it). + + The assertion is about SHAPE, not text: whatever went wrong inside the + engine, the customer-visible ``error`` must not carry a filesystem path, a + ``Permission denied``, or a provider response body -- spec §8 mandates the + user-safe ``" failed: "`` form (or one of the fixed strings + ``cancelled`` / ``timed out``), which is what the executor now builds from + the node listener's exception rather than echoing the engine's own text. + """ + if os.environ.get("AGENT_KV_E2E_BAD_KEY_JOB") != "1": + pytest.skip( + "needs a platform deliberately configured with an INVALID " + "AGENT_KV_LLM_API_KEY; set AGENT_KV_E2E_BAD_KEY_JOB=1 to declare " + "that this stack is running that way (see the module docstring)" + ) + + job_id, _ = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + ) + + status_doc = poll(agent_kv_key, job_id, timeout_s=600) + assert status_doc["status"] == "failed", status_doc + assert "error" in status_doc, status_doc + error = status_doc["error"] + assert isinstance(error, str), status_doc + assert error, status_doc + + _assert_user_safe_error(error) + + # The result endpoint reports the same failure, in the docs §5 shape. + resp = result(agent_kv_key, job_id) + assert resp.status_code == 200, resp.text + body = resp.json() + assert body["success"] is False, body + assert body["status"] == "failed", body + _assert_user_safe_error(body["error"]) + + +# --------------------------------------------------------------------------- +# 9. Document cache (spec §6.5 document tier) +# --------------------------------------------------------------------------- + + +def test_sync_wait_submit_returns_result_inline( + agent_kv_key: AgentKVAuth, require_llm: None +) -> None: + """A submit with ``timeout`` seconds sync-waits: if the job reaches a + terminal state inside the window, the response is 200 with the result + payload itself (docs §3) -- no polling leg. Uses the same invoice as the + happy path, so a warm document cache keeps the wait well inside the + window; even a cold cache (~15 s pipeline) fits comfortably. + """ + resp = submit_raw( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + timeout=120, + ) + assert resp.status_code == 200, ( + f"sync-wait submit: HTTP {resp.status_code} (expected 200 with the " + f"inline result): {resp.text}" + ) + body = resp.json() + # `success`/`status` are top level on every terminal payload; the engine's + # own record is namespaced per extractor (spec §7.3). + assert body.get("success") is True, body + body = table_result_body(resp) + assert isinstance(body["tables"], list), body + assert "row_count" in body, body + + +# --------------------------------------------------------------------------- +# 9c. Excel input +# --------------------------------------------------------------------------- + + +def test_excel_submit_extracts(agent_kv_key: AgentKVAuth, require_llm: None) -> None: + """An .xlsx submit runs the whole pipeline. Excel has no pre-OCR page + count (``pages_total`` stays None at submit) and takes the post-OCR + virtual-page cap path instead. Structure-only assertions, like the + happy path. + """ + job_id, _ = submit( + agent_kv_key, INVOICE_XLSX.read_bytes(), "invoice.xlsx", table_keys("Line items") + ) + doc = poll(agent_kv_key, job_id) + assert doc["status"] == "completed", doc + body = table_result_body(result(agent_kv_key, job_id)) + assert body.get("success") is True, body + assert isinstance(body["tables"], list), body + assert "page_count" in body, body + + +# --------------------------------------------------------------------------- +# 9d. Webhook delivery (operator-gated) +# --------------------------------------------------------------------------- + + +def test_webhook_delivered_on_completion( + agent_kv_key: AgentKVAuth, require_llm: None +) -> None: + """Completion-webhook delivery, end to end: submit with a ``webhook_url`` + pointing at a receiver on the compose host and wait for the POST. + + Operator-gated: the worker's SSRF guards (https + public host, spec + §6.7) refuse a host-local receiver unless the stack runs with + ``AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=1`` on the ide-callback + worker (test/dev only), reached via ``host.docker.internal`` (compose + host-gateway mapping). Skips unless the operator set the same env for + the test run to declare that setup. + """ + if os.environ.get("AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE", "").lower() not in ( + "1", + "true", + "yes", + ): + pytest.skip( + "webhook e2e needs AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=1 " + "on both the ide-callback worker and this test run" + ) + + import http.server + import threading + + hits: list[dict] = [] + + class _Receiver(http.server.BaseHTTPRequestHandler): + def do_POST(self): # noqa: N802 - stdlib API name + length = int(self.headers.get("Content-Length", "0")) + hits.append(json.loads(self.rfile.read(length) or b"{}")) + self.send_response(200) + self.end_headers() + + def log_message(self, *args): # keep pytest output pristine + pass + + server = http.server.HTTPServer(("0.0.0.0", 0), _Receiver) + port = server.server_address[1] + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + job_id, _ = submit( + agent_kv_key, + _RENT_ROLL_BYTES, + "rent_roll.pdf", + table_keys(), + webhook_url=f"http://host.docker.internal:{port}/agent-kv-hook", + ) + doc = poll(agent_kv_key, job_id) + assert doc["status"] == "completed", doc + deadline = time.time() + 30 + while time.time() < deadline and not hits: + time.sleep(0.5) + assert hits, "no webhook POST arrived within 30s of completion" + assert hits[0] == {"job_id": job_id, "status": "completed"}, hits + finally: + server.shutdown() + thread.join(timeout=5) + + +# --------------------------------------------------------------------------- +# 9e. Calculations (sandbox worker, operator-gated) +# --------------------------------------------------------------------------- + +_CALC_ENV_GATE = "AGENT_KV_E2E_CALCULATIONS" + + +def _skip_unless_calculations_declared() -> None: + """Calculations need a stack running with ``AGENT_KV_CALCULATIONS_ENABLED + =true`` on the backend AND the sandbox codegen worker fleet actually up + and consuming ``sandbox_codegen`` (docs/agent-kv-api.md §11) -- neither + of which this test process can see or arrange for itself. So, like the + bad-LLM-key scenario above, it needs an explicit operator declaration + that the stack under test is configured that way; not every stack that + runs the rest of this lane has the sandbox worker running. + """ + if os.environ.get(_CALC_ENV_GATE) != "1": + pytest.skip( + "calculation e2e scenarios need a stack running with " + "AGENT_KV_CALCULATIONS_ENABLED=true and the sandbox codegen " + f"worker fleet up (docs/agent-kv-api.md §11); set {_CALC_ENV_GATE}=1 " + "to declare that this stack is configured that way" + ) + + +def test_concurrency_limit_returns_429(agent_kv_key: AgentKVAuth) -> None: + """Saturate the org's concurrency limiter; every other submit-based + scenario in this module runs before this one, and ``tests/groups.yaml`` + pins this whole group ``parallel: false`` -- both for the same reason: + + ``JobCancelView`` marks a job ``CANCELLED`` directly via + ``AgentKVJob.mark_terminal`` and never touches + ``AgentKVConcurrencyLimiter`` -- only a job's own *finalize* call + releases its slot (in a ``finally``, unconditional on whether the + terminal-state guard actually won -- docs/agent-kv-api.md §11a), and + that only happens once the executor that picked the job up actually + runs it to completion/failure and calls back. So immediately after this + test oversaturates the limiter, the org's concurrency budget stays + genuinely exhausted for however long those accepted jobs take to + finalize -- anywhere from seconds to the executor's task time limit. + Any sibling submit-based test running concurrently with, or soon after, + this one would risk being 429'd by *this* test's saturation rather than + by anything it did itself. Running this scenario last (and the group + serially) removes that risk entirely for this module; the cleanup below + is a courtesy for whatever runs against this org after it, not a + correctness requirement for this test itself -- the limiter's own 6h + slot TTL (``AgentKVConcurrencyLimiter._SLOT_TTL_SECONDS``) is the + ultimate backstop either way. + """ + limit = int(os.environ.get("AGENT_KV_CONCURRENT_LIMIT", "5")) + n = limit + 1 + + def _submit(_i: int): + return _cheap_submit(agent_kv_key) + + accepted_job_ids: list[str] = [] + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=n) as pool: + responses = list(pool.map(_submit, range(n))) + + statuses = [r.status_code for r in responses] + assert all(s in (202, 429) for s in statuses), ( + f"unexpected status among {n} concurrent submits " + f"(only 202/429 are valid outcomes here): {statuses}" + ) + assert 429 in statuses, ( + f"submitting {n} concurrent jobs against a limit of {limit} " + f"produced no 429: {statuses}" + ) + for r in responses: + if r.status_code == 202: + accepted_job_ids.append(r.json()["job_id"]) + assert len(accepted_job_ids) <= limit, accepted_job_ids + finally: + # Cancelling stops each job's eventual result from ever being + # readable, but -- per the docstring above -- does NOT itself free + # its concurrency slot; only that job's own finalize call does. + for job_id in accepted_job_ids: + try: + cancel(agent_kv_key, job_id) + except Exception: # noqa: BLE001 - cleanup only, never fail the test on it + pass + + # Best-effort drain: keep probing with a cheap submission until one + # is actually accepted again (proof a slot freed up) or the bounded + # wait runs out. The RESULT is deliberately ignored here -- see the + # 6h-TTL backstop note in the docstring above; this is a courtesy for + # whatever runs next, not a correctness requirement for this test. + _drain_until_slot_free(agent_kv_key) diff --git a/tests/groups.yaml b/tests/groups.yaml index cc2e5beb73..652c4885c9 100644 --- a/tests/groups.yaml +++ b/tests/groups.yaml @@ -89,7 +89,8 @@ groups: unit-workers: tier: unit workdir: workers - paths: [tests, shared/tests] + paths: [tests, shared/tests, shared/infrastructure/config/tests, + ide_callback/tests] # Negative filter: worker unit tests aren't tagged `unit` (~1100 of them), # so select by exclusion. `conftest.py` marks real-Postgres tests # `integration` (routed to the `integration-workers` group); `slow` tests are @@ -128,6 +129,25 @@ groups: install_editable: true coverage_source: src + # Both of these had tests but NO group, so nothing collected them. For + # agent-kv-schema that mattered specifically: test_compile.py is the + # regression net for the schema depth/ReDoS caps, so the caps were asserted + # only on a developer's machine. A PR reporting "165 passed" for this package + # was reporting a local figure, not a CI one. + unit-agent-kv-schema: + tier: unit + workdir: unstract/agent-kv-schema + paths: [tests] + install_editable: true + coverage_source: src + + unit-filesystem: + tier: unit + workdir: unstract/filesystem + paths: [tests] + install_editable: true + coverage_source: src + # runner/tests was registered in NO group, so it was collected by nothing. Its # only suite guards that LOG_TRANSPORT and LOG_STREAM_QUEUE_NAME reach the tool # sidecar's hand-picked env allowlist — a list that drops anything not named in @@ -254,6 +274,29 @@ groups: depends_on: [e2e-smoke] optional: true + # Agent-KV product API (/agent-kv/...). Needs the cloud `agentic_kv` + # executor plugin installed (docs/agent-kv-api.md §11) -- an OSS-only image + # 501s every submit, so this is optional: an OSS-only CI run must not go + # red just because the plugin isn't there. The module itself additionally + # self-gates on AGENT_KV_E2E=1 so it never runs by accident even when the + # platform happens to be up (see tests/e2e/agent_kv/test_agent_kv_e2e.py). + # Longer budget than the other platform groups: the happy-path and delete + # scenarios poll a real extraction job through to COMPLETED. + e2e-agent-kv: + tier: e2e + paths: [tests/e2e/agent_kv] + requires_platform: true + depends_on: [e2e-smoke] + optional: true + timeout_seconds: 1200 + # Serial, not xdist-parallel: the concurrency scenario deliberately + # saturates the org's AgentKVConcurrencyLimiter slots, which + # JobCancelView never releases (only a finalize callback does, by + # design -- see the scenario's own docstring). Running sibling + # submit-based tests on other xdist workers at the same moment would + # let that saturation 429 them too. + parallel: false + ui: # Playwright browser e2e over the real UI. Scaffolding only for now — the # spec directory holds a single smoke test; extend it here. diff --git a/tox.ini b/tox.ini index b1d66781a9..cd4bc9c8fd 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,11 @@ passenv = GITHUB_* DOCKER_HOST UNSTRACT_* + # The Agent-KV e2e lane's stack config (engine keys/models, storage prefix, + # the caps its scenarios override) is read by + # tests/compose/docker-compose.test.yaml from the caller's environment; tox + # would otherwise strip it and the lane would silently run misconfigured. + AGENT_KV_* # ComposeRuntime's `docker compose up` resolves ${VERSION} image tags in # docker/docker-compose.yaml from the caller's environment. VERSION diff --git a/unstract/agent-kv-schema/pyproject.toml b/unstract/agent-kv-schema/pyproject.toml new file mode 100644 index 0000000000..9db37d39b7 --- /dev/null +++ b/unstract/agent-kv-schema/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "unstract-agent-kv-schema" +version = "0.1.0" +description = "Agent-KV keys.json schema compiler — single source of truth for API validation and the extraction engine" +requires-python = ">=3.12" +dependencies = [] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/unstract"] diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/__init__.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/__init__.py new file mode 100644 index 0000000000..07087e3f47 --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/__init__.py @@ -0,0 +1,15 @@ +from .compile import CompiledSchema, SchemaCaps, SchemaError, compile_schema +from .constraints import evaluate_constraints +from .dataclasses import ArraySpec, KeySpec +from .validators import validate_format + +__all__ = [ + "ArraySpec", + "CompiledSchema", + "KeySpec", + "SchemaCaps", + "SchemaError", + "compile_schema", + "evaluate_constraints", + "validate_format", +] diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py new file mode 100644 index 0000000000..6b28c3b07f --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py @@ -0,0 +1,265 @@ +"""Cap-enforcing, syntax-validating wrapper over the ported compiler. + +This is the entry point the API uses for submit-time validation. Anything +``compile_schema`` accepts, the engine must execute; anything it rejects never +reaches OCR or an LLM. + +It is NOT, despite what this docstring said before, "the single entry point +both the API and the cloud engine use". The engine calls the raw +``kv_schema.compile``/``compile_arrays`` directly (see the import note in the +cloud plugin's ``engine/kv_extractor.py``), so the caps below are enforced on +the submit path only. That is sound while the backend is the sole producer of a +compiled schema -- which it is today -- but it means the caps are a gate, not +an invariant the engine itself re-checks. Anything that ever hands the engine a +schema from another source has to apply them, or this module has to become what +it claimed to be. +""" + +import ast +import re +from dataclasses import dataclass, field + +from . import kv_schema +from .dataclasses import ArraySpec, KeySpec + + +class SchemaError(ValueError): + """User-facing schema rejection; message is safe to return in a 400.""" + + +@dataclass(frozen=True) +class SchemaCaps: + max_leaves: int = 200 + max_arrays: int = 20 + max_columns_per_array: int = 40 + max_depth: int = 6 + max_regex_len: int = 200 + max_aliases: int = 10 + max_description_len: int = 500 + max_constraints: int = 30 + + +@dataclass(frozen=True) +class CompiledSchema: + key_specs: list[KeySpec] = field(default_factory=list) + array_specs: list[ArraySpec] = field(default_factory=list) + constraints: list[str] = field(default_factory=list) + + +_ALLOWED_CALLS = {"sum", "count", "min", "max", "avg"} +_ALLOWED_NODES = ( + ast.Expression, + ast.BoolOp, + ast.And, + ast.Or, + ast.UnaryOp, + ast.Not, + ast.USub, + ast.Compare, + ast.Eq, + ast.NotEq, + ast.Lt, + ast.LtE, + ast.Gt, + ast.GtE, + ast.BinOp, + ast.Add, + ast.Sub, + ast.Mult, + ast.Div, + ast.Call, + ast.Name, + ast.Attribute, + ast.Constant, + ast.Load, +) + + +def _check_constraint_syntax(expr: str) -> None: + """Static allowlist mirroring constraints._evaluate_one's grammar.""" + try: + tree = ast.parse(expr, mode="eval") + except SyntaxError as e: + raise SchemaError(f"constraint does not parse: {expr!r} ({e.msg})") from e + for node in ast.walk(tree): + if not isinstance(node, _ALLOWED_NODES): + raise SchemaError( + f"constraint uses disallowed syntax ({type(node).__name__}): {expr!r}" + ) + if isinstance(node, ast.Call): + if not isinstance(node.func, ast.Name) or node.func.id not in _ALLOWED_CALLS: + raise SchemaError(f"constraint calls a disallowed function: {expr!r}") + if ( + node.keywords + or len(node.args) != 1 + or not isinstance(node.args[0], ast.Constant) + or not isinstance(node.args[0].value, str) + ): + raise SchemaError( + f"constraint aggregate needs one string literal arg: {expr!r}" + ) + + +def _max_depth(node: object, max_depth: int, depth: int = 0) -> int: + # HARD ceiling FIRST -- before the `_array` short-circuit and before any + # further recursion. Two bugs this closes: (a) a pathologically deep plain + # schema (256 KiB of JSON nests ~40k deep, far past Python's ~1000 + # recursion limit) used to raise an uncaught RecursionError (500) here; + # now it fails fast as a clean SchemaError (400). (b) The `_array` + # short-circuit below cannot be reached to bypass the cap once `depth` + # has already blown past it. + if depth > max_depth: + raise SchemaError(f"schema exceeds max_depth={max_depth}") + if not isinstance(node, dict): + return depth + if "_array" in node: + return depth + 1 # array columns are row-local, not nesting + child = [v for v in node.values() if isinstance(v, dict)] + if not child: + return depth + 1 + return max(_max_depth(v, max_depth, depth + 1) for v in child) + + +# Quantified groups that themselves contain a quantifier, e.g. `(a+)+`, `(a*)*`, +# `(.+)*`, `(?:x+)+`. This is the shape behind catastrophic backtracking. +_NESTED_QUANTIFIER = re.compile( + r"\((?:\?[:=!]|\?<[=!]|\?P<[^>]+>)?" # group open, incl. non-capturing/named + r"[^()]*[+*}]\??" # ...containing a quantifier + r"[^()]*\)" # ...group close + r"\s*[+*]|\)\s*\{\d+,\d*\}" # ...itself quantified +) + + +# A quantified group whose alternatives OVERLAP, e.g. `(a|aa)+`. Found in +# review after the nested-quantifier check shipped: `^(a|aa)+$` passes that +# check and still backtracks catastrophically, because at each position the +# engine can consume one `a` or two and must try both on failure. +# +# Only LITERAL branches are compared, and only by the prefix relation: if one +# branch is a prefix of another (`a` of `aa`), the group is ambiguous and +# refused. `(foo|bar)+` is left alone -- distinct first characters mean no +# position admits two parses, so it is linear. Branches containing +# metacharacters are not analysed (`.`/classes/nested groups need real regex +# analysis, which is what UN-4225 is for); this closes the demonstrated family +# without pretending to be a decision procedure. +_QUANTIFIED_GROUP = re.compile(r"\((\?:)?([^()]*)\)\s*(?:[+*]|\{\d+,\d*\})") +_LITERAL_BRANCH = re.compile(r"^[\w\-/ ]*$") + + +def _overlapping_alternation(pattern: str) -> str | None: + """Return a human-readable overlap if a quantified group is ambiguous.""" + for match in _QUANTIFIED_GROUP.finditer(pattern): + body = match.group(2) + if "|" not in body: + continue + branches = body.split("|") + if not all(_LITERAL_BRANCH.match(b) for b in branches): + continue + for i, a in enumerate(branches): + for j, b in enumerate(branches): + if i != j and a and b.startswith(a): + return f"{a!r} is a prefix of {b!r}" + return None + + +def _reject_unsafe_regex(path: str, pattern: str) -> None: + """Refuse an author-supplied pattern at SUBMIT rather than at match time. + + Two separate problems, both found in review: + + 1. The pattern was never compiled here, so a syntactically invalid one was + accepted and only discovered per-value in the engine's QA pass -- where + ``_check_one`` swallows ``re.error`` and returns True, silently passing + validation the author thought they had configured. + 2. ``validate_format`` runs the pattern against extracted values with no + time budget, so a catastrophically-backtracking pattern is a DoS. The + length cap is NOT a mitigation: ``^(a+)+$`` is 7 characters and takes + ~1.9s on 26 ``a``s, ~4x per character added (measured), so a 40-char + value runs for hours. With ``AGENT_KV_CONCURRENT_LIMIT=5`` one org can + pin five shared worker slots from a single submit. + + The nested-quantifier check is a CONSERVATIVE HEURISTIC, not a proof. It + rejects the shape responsible for the realistic cases (a quantified group + whose body is itself quantified) and will reject some safe patterns that + happen to look like it -- an explicit trade, since the author gets an + immediate, actionable error instead of a job that hangs. It does not catch + every pathological pattern; the complete fix is a linear-time engine (RE2), + which cannot be added here because this package deliberately has zero + dependencies and is installed by both repos. Tracked as UN-4225. + """ + try: + re.compile(pattern) + except re.error as e: + raise SchemaError(f"'{path}' has an invalid regex: {e}") from None + if _NESTED_QUANTIFIER.search(pattern): + raise SchemaError( + f"'{path}' regex has a nested quantifier (e.g. '(a+)+'), which can " + "backtrack catastrophically and stall extraction. Rewrite it " + "without a quantifier inside a quantified group." + ) + overlap = _overlapping_alternation(pattern) + if overlap: + raise SchemaError( + f"'{path}' regex quantifies a group whose alternatives overlap " + f"({overlap}), e.g. '(a|aa)+', which can backtrack " + "catastrophically and stall extraction. Make the alternatives " + "mutually exclusive, or use a character class." + ) + + +def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema: + caps = caps or SchemaCaps() + if not isinstance(spec, dict): + raise SchemaError("Top-level key schema must be a JSON object") + cleaned = {k: v for k, v in spec.items() if k != "_constraints"} + if _max_depth(cleaned, caps.max_depth) > caps.max_depth: + raise SchemaError(f"schema exceeds max_depth={caps.max_depth}") + # The compile.py `_max_depth` pre-check does not count array-column + # nesting (arrays are row-local there, by design) and cannot see a decoy + # top-level `_array` field's real nesting -- so the actual recursive walk + # (`kv_schema._walk`) carries its own max_depth ceiling too, both to close + # that bypass and to guarantee a clean SchemaError instead of an uncaught + # RecursionError on a deeply-nested input. + try: + key_specs = kv_schema.compile(spec, max_depth=caps.max_depth) + array_specs = kv_schema.compile_arrays(spec, max_depth=caps.max_depth) + except ValueError as e: + raise SchemaError(str(e)) from e + + if len(key_specs) > caps.max_leaves: + raise SchemaError(f"schema exceeds max_leaves={caps.max_leaves}") + if len(array_specs) > caps.max_arrays: + raise SchemaError(f"schema exceeds max_arrays={caps.max_arrays}") + for aspec in array_specs: + if len(aspec.item_specs) > caps.max_columns_per_array: + raise SchemaError( + f"array '{aspec.path}' exceeds " + f"max_columns_per_array={caps.max_columns_per_array}" + ) + for kspec in key_specs + [s for a in array_specs for s in a.item_specs]: + _reject_unsafe_regex(kspec.path, kspec.regex_pattern) + if len(kspec.regex_pattern) > caps.max_regex_len: + raise SchemaError( + f"'{kspec.path}' regex exceeds max_regex_len={caps.max_regex_len}" + ) + if len(kspec.aliases) > caps.max_aliases: + raise SchemaError(f"'{kspec.path}' exceeds max_aliases={caps.max_aliases}") + if len(kspec.effective_description) > caps.max_description_len: + raise SchemaError( + f"'{kspec.path}' description exceeds " + f"max_description_len={caps.max_description_len}" + ) + + constraints = spec.get("_constraints", []) + if not isinstance(constraints, list) or not all( + isinstance(c, str) for c in constraints + ): + raise SchemaError("_constraints must be a list of strings") + if len(constraints) > caps.max_constraints: + raise SchemaError(f"schema exceeds max_constraints={caps.max_constraints}") + for expr in constraints: + _check_constraint_syntax(expr) + + return CompiledSchema( + key_specs=key_specs, array_specs=array_specs, constraints=list(constraints) + ) diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py new file mode 100644 index 0000000000..85efa58b2e --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py @@ -0,0 +1,258 @@ +"""Cross-field consistency (spec §18.3): a fail-closed evaluator for schema-author-declared +constraints over the NORMALIZED key values. No eval/exec — a static AST allowlist (Compare/BoolOp/ +BinOp/UnaryOp/Name/Attribute-path/Constant only). Operands resolve to normalized values; a missing/ +empty/un-coercible operand SKIPS the constraint (advisory), never crashes. Returns violated exprs. +""" + +import ast +import math +import operator + +from .validators import coerce_number + +# Equality on these values is TOLERANT, not exact. Operands are normalized +# currency/number values that arrived as floats, so binary floating point makes +# exact `==` wrong for the thing constraints exist to express. The feature's own +# headline example reports a false violation on a CORRECT invoice: +# three line items of 8230.4 sum to 24691.199999999997, which `operator.eq` +# says is not 24691.2 (reproduced). +# +# The tolerance has to sit BELOW a cent at every realistic magnitude while +# staying ABOVE the float noise floor. Those two bounds are what set the +# numbers, and the first attempt at this got it wrong: `rel_tol=1e-9` is 0.1 at +# a total of $100,000,000, so it silently absorbed a one-cent reconciliation +# error on a large invoice -- the exact failure this check exists to catch +# (found in review; the original test only covered a ~$25,000 total). +# +# Noise floor: summing N values of magnitude M accumulates roughly +# N * 2.2e-16 * M of float error. M=$1e8 over 1,000 rows is ~2.2e-5, i.e. about +# a five-hundredth of a cent. +# +# rel_tol=1e-12 -> 1e-4 at $1e8 (a hundredth of a cent): absorbs that noise, +# and a one-cent error is 100x larger, so it is still +# reported. Relative rather than absolute-only so the bound +# tracks magnitude -- an absolute epsilon suited to invoice +# totals is meaningless against unit prices in thousandths. +# abs_tol=1e-6 -> keeps comparisons against exact zero working, where +# relative tolerance is useless (every non-zero value is +# infinitely far from 0 in relative terms), and is itself +# well under a cent. +# +# Known ceiling: past ~$1e12 with ~10,000 rows the noise floor (~2.2) exceeds a +# cent and no float tolerance can separate the two. That is a float problem, +# not a tolerance problem, and the fix is Decimal end to end -- +# `normalizers.coerce_number`, UN-4226. This makes the COMPARISON correct for +# the float values that exist today, and stays correct afterwards. +_REL_TOL = 1e-12 +_ABS_TOL = 1e-6 + + +def _num_eq(a, b) -> bool: + if isinstance(a, (int, float)) and isinstance(b, (int, float)): + return math.isclose(a, b, rel_tol=_REL_TOL, abs_tol=_ABS_TOL) + return operator.eq(a, b) + + +def _num_ne(a, b) -> bool: + return not _num_eq(a, b) + + +_CMP = { + ast.Eq: _num_eq, + ast.NotEq: _num_ne, + # Ordering comparisons are left exact on purpose: a tolerant `<` would make + # `a < b` and `a == b` both true at the boundary, and the failure mode these + # express (a total that is too large/small) is not a rounding artefact. + ast.Lt: operator.lt, + ast.LtE: operator.le, + ast.Gt: operator.gt, + ast.GtE: operator.ge, +} +_BIN = { + ast.Add: operator.add, + ast.Sub: operator.sub, + ast.Mult: operator.mul, + ast.Div: operator.truediv, +} + +# P8c: the ONLY callable names permitted in a constraint, and only over a single +# string-literal `'array_path'` / `'array_path.column'` argument. This is NOT a general +# function-call capability — it is a closed allowlist of pure numeric reductions evaluated +# in Python (never via eval/exec) so a scalar key can be reconciled against an array column +# (e.g. grand_total == sum('line_items.line_total')). +_AGG = {"sum", "count", "min", "max", "avg"} + + +class _Skip(Exception): + """Operand missing/empty/incomparable — skip this constraint (advisory).""" + + +def _path(node) -> str: + """Reconstruct a dotted path from a Name / Attribute-chain (pure attribute access only).""" + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return f"{_path(node.value)}.{node.attr}" + raise _Skip() + + +def _coerce(raw: str): + """Normalized value -> finite float when numeric, else the string (ISO dates compare + lexicographically). Uses the canonical coercer so thousands-separated values ('9,000') + parse numerically (a bare float() would leave them as strings and silently drop a real + violation via the like-type guard); 'nan'/'inf' tokens stay strings (not comparable numbers). + """ + n = coerce_number( + raw + ) # strips commas/$/%; returns None for non-numeric AND non-finite + return n if n is not None else raw + + +def _aggregate(node: ast.Call, arrays: dict[str, list[dict[str, str]]]): + """Evaluate one of the five allowlisted aggregates over an array column. + + FAIL-CLOSED by construction: the only thing this accepts is `NAME('literal')` where + NAME is in `_AGG`, the func is a bare ast.Name (no attribute access — blocks + `os.system`/`x.__class__`), there is exactly one positional arg, no keywords, and that + arg is a string Constant. Any deviation raises `_Skip` (-> constraint skipped, never run). + + Argument is `'array_path'` (only valid for count = row count) or `'array_path.column'`. + Numeric cells (sum/min/max/avg) are pulled with the SAME `coerce_number` used elsewhere + (strips commas/$/%, drops non-finite/empty -> None); such cells are skipped (not zeroed). + `count('a.col')` counts rows whose column value is NON-EMPTY (text columns like + sku/description count too); `count('a')` is the row count. A sum/min/max/avg over zero + usable cells raises `_Skip` (advisory) rather than guessing 0. + """ + if not isinstance(node.func, ast.Name) or node.func.id not in _AGG: + raise _Skip() # not a whitelisted aggregate name + if node.keywords or len(node.args) != 1: + raise _Skip() # exactly one positional arg, no keywords + arg = node.args[0] + if not (isinstance(arg, ast.Constant) and isinstance(arg.value, str)): + raise _Skip() # arg must be a literal string path + fn, ref = node.func.id, arg.value + # Split on the LAST dot: an ArraySpec.path may itself be dotted (e.g. 'invoice.lines'), + # while the column is always a row-LOCAL bare name. A bare ref (no dot) is a whole-array + # row count -> array_path=ref, column=''. + if "." in ref: + array_path, _, column = ref.rpartition(".") + else: + array_path, column = ref, "" + rows = arrays.get(array_path) + if rows is None: # no such array available + raise _Skip() + + if fn == "count": + if not column: # count('array') -> number of rows + return float(len(rows)) + # count('array.col') -> rows with a non-empty value for that column (text columns + # like sku/description/name are valid to count; numeric coercion would zero them out) + return float(sum(1 for r in rows if (r.get(column) or "").strip() != "")) + + if not column: # sum/min/max/avg need a column + raise _Skip() + nums = [n for r in rows if (n := coerce_number(r.get(column))) is not None] + if not nums: # nothing usable -> advisory skip + raise _Skip() + # `math.fsum`, not the builtin `sum`: exactly-rounded, so accumulation error + # does not grow with row count. The builtin accumulates left to right, and + # on a long array that drift is the one thing that could push a CORRECT + # total outside the comparison tolerance and report a false violation. + # + # Review raised exactly that risk for a 100,000-row array. It did not + # reproduce -- 60 randomised value/row combinations up to 100,000 rows and + # ~$100k per line produced zero false positives, and the specific figure + # quoted (`sum([8230.40] * 100_000) == 823039999.9985306`) is not what + # CPython computes; it is exactly 823040000.0, because for identical + # addends the partial sums stay representable. But "does not reproduce + # across 60 samples" is a weaker guarantee than "cannot happen", the fix + # costs one function call, and this is the third review pass over these few + # lines -- so the error source is removed rather than argued about. + if fn == "sum": + return float(math.fsum(nums)) + if fn == "min": + return float(min(nums)) + if fn == "max": + return float(max(nums)) + return float(math.fsum(nums) / len(nums)) # avg + + +def _operand(node, values: dict[str, str], arrays: dict[str, list[dict[str, str]]]): + if isinstance(node, ast.Call): + return _aggregate(node, arrays) # ONLY the _AGG allowlist; else _Skip + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.BinOp) and type(node.op) in _BIN: + a, b = _operand(node.left, values, arrays), _operand(node.right, values, arrays) + if not (isinstance(a, (int, float)) and isinstance(b, (int, float))): + raise _Skip() + return _BIN[type(node.op)](a, b) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + v = _operand(node.operand, values, arrays) + if not isinstance(v, (int, float)): + raise _Skip() + return -v + # else: a key reference + path = _path(node) + raw = values.get(path) + if raw is None or raw == "": + raise _Skip() + return _coerce(raw) + + +def _truth(node, values: dict[str, str], arrays: dict[str, list[dict[str, str]]]) -> bool: + if isinstance(node, ast.BoolOp): + sub = [_truth(v, values, arrays) for v in node.values] + return all(sub) if isinstance(node.op, ast.And) else any(sub) + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + return not _truth(node.operand, values, arrays) + if isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators): + left = _operand(node.left, values, arrays) + for op, comp in zip(node.ops, node.comparators, strict=False): + if type(op) not in _CMP: + raise _Skip() + right = _operand(comp, values, arrays) + # only compare like-typed operands (number<->number, str<->str); else skip + if isinstance(left, (int, float)) != isinstance(right, (int, float)): + raise _Skip() + if not _CMP[type(op)](left, right): + return False + left = right + return True + raise _Skip() + + +def _evaluate_one( + expr: str, values: dict[str, str], arrays: dict[str, list[dict[str, str]]] +): + """Return True/False, or None to skip (missing operand / unsupported / unsafe).""" + try: + tree = ast.parse(expr, mode="eval") + except SyntaxError: + return None + try: + return _truth(tree.body, values, arrays) + except _Skip: + return None + except Exception: + return None # fail-closed: any surprise -> skip, never crash the pipeline + + +def evaluate_constraints( + constraints: list[str], + values: dict[str, str], + arrays: dict[str, list[dict[str, str]]] | None = None, +) -> list[str]: + """Return the list of constraint expressions that evaluated to False (violations). + Skipped (missing operand / unsupported / unsafe) constraints are NOT violations. + + `arrays` (optional) maps an ArraySpec path to its rendered rows (list of {column: value}) + so the five allowlisted aggregates (sum/count/min/max/avg) can reconcile a scalar key + against an array column. Defaults to {} -> aggregates become no-op skips (back-compat). + """ + arrays = arrays or {} + violations = [] + for expr in constraints or []: + if _evaluate_one(expr, values, arrays) is False: + violations.append(expr) + return violations diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py new file mode 100644 index 0000000000..10b72ef1b2 --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py @@ -0,0 +1,53 @@ +"""Dataclasses for the key-value extractor. + +ExecutionResult is intentionally NOT defined here — code execution reuses the +imported src/core code_executor's result type (see spec §4, §10). +""" + +from dataclasses import dataclass, field + + +@dataclass +class KeySpec: + """One compiled leaf key from the user's nested key schema.""" + + path: str # dotted path, e.g. "vendor.address.city" + effective_description: str # breadcrumb + leaf description + format: str = "string" # kind: string|number|date|currency|enum|regex| + enum_values: list[str] = field(default_factory=list) # set when format == "enum" + regex_pattern: str = "" # set when format == "regex" + required: bool = False + aliases: list[str] = field(default_factory=list) + multivalued: bool = False # value is a comma-separated string + + +@dataclass +class ArraySpec: + """One compiled FLAT-array node (P8a). `path` = dotted array location (e.g. 'line_items', + 'invoice.lines'). `item_specs` = the declared columns as row-LOCAL scalar KeySpecs (their + `path` is the bare column name). `key_column` (optional) is a column used for row identity in + scoring; '' = positional. Nested arrays inside an item are P8b and rejected at compile. + """ + + path: str + description: str = "" + item_specs: list[KeySpec] = field(default_factory=list) + key_column: str = "" + required: bool = False + #: Collapse rows identical in EVERY extracted cell? Declared per array as + #: `"_dedup": false`. + #: + #: Defaults True, which is the pre-existing behaviour and is wanted for the + #: corpus the extractor was built against: layout-preserving OCR repeats a + #: label once per replicate column, producing rows identical in every cell. + #: + #: But it is NOT lossless, and the docstring that claimed it was has been + #: corrected. A document that genuinely contains two identical line items + #: comes back with one, so the row count and any calculation summing or + #: counting the rows are wrong -- and the codegen path consumes exactly + #: these rows. Being unable to tell two real records from one OCR artefact + #: is a reason not to guess, so a schema author who knows their documents + #: can say so. Default left at True rather than flipped: changing it would + #: trade a known-wrong case for an untested one on the corpus this was + #: built for. + dedup_rows: bool = True diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py new file mode 100644 index 0000000000..93cedeb508 --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py @@ -0,0 +1,224 @@ +"""Compile a user-authored nested key schema into a flat, ordered KeySpec list, +and reassemble flat extracted values back into the nested shape. + +Leaf/interior rule (spec §7): a node is INTERIOR iff ALL of its values are objects +(its keys are child nodes); otherwise it is a LEAF defined by its scalar attributes. +A node mixing object and scalar values is a compile-time error. Reserved attribute +words cannot be used as child-node key names. +""" + +from typing import Any + +from .dataclasses import ArraySpec, KeySpec + +RESERVED = {"description", "format", "required", "aliases", "multivalued"} +RESERVED_NODE = { + "_array", + "description", + "_key", + "_dedup", +} # node-level (distinct from leaf-attr RESERVED) + +# Absolute structural-depth ceiling for callers that don't pass an explicit +# max_depth (the public compile/compile_arrays entry points). It only has to +# sit well below Python's ~1000 recursion limit so a hostile keys.json fails +# fast as a clean ValueError (-> SchemaError) instead of a RecursionError; +# compile_schema always passes the real, much smaller cap from SchemaCaps. +_DEFAULT_MAX_DEPTH = 100 + + +def _parse_dedup(raw: Any, path: str) -> bool: + """Validate the per-array `_dedup` flag. + + Strict about the type rather than truthy: `"_dedup": "false"` is a string + and would be TRUE under a truthiness check, silently leaving dedup on for + an author who explicitly asked for it off -- and silently dropping their + duplicate rows. A wrong type is a schema error, not a default. + """ + if isinstance(raw, bool): + return raw + raise ValueError(f"'{path}' _dedup must be true or false (got {type(raw).__name__})") + + +def _parse_format(raw: str): + """Return (kind, enum_values, regex_pattern) for a declared format string.""" + if raw.startswith("enum:"): + values = [v.strip() for v in raw[len("enum:") :].split(",") if v.strip()] + return "enum", values, "" + if raw.startswith("regex:"): + return "regex", [], raw[len("regex:") :] + return raw, [], "" + + +def _walk( + node: dict[str, Any], + path_parts: list[str], + out: list[KeySpec], + arrays: list[ArraySpec], + *, + max_depth: int = _DEFAULT_MAX_DEPTH, + depth: int = 0, +) -> None: + # HARD structural-depth ceiling at the TOP of the recursive walk. This is + # the real backstop the compile.py `_max_depth` pre-check can't provide: + # that check treats array columns as row-local (not nesting) and is blind + # to a decoy top-level `_array` field's real nesting, so a schema can pass + # it yet still recurse arbitrarily deep here. `depth` increments on EVERY + # recursion (interior children AND array columns), so exceeding the cap + # raises a clean ValueError -- surfaced as SchemaError by compile_schema -- + # instead of an uncaught RecursionError. + if depth > max_depth: + raise ValueError(f"schema exceeds max_depth={max_depth}") + if not isinstance(node, dict) or not node: + raise ValueError( + f"Schema node at {'.'.join(path_parts) or ''} must be a non-empty object" + ) + + if "_array" in node: + extra = set(node.keys()) - RESERVED_NODE + if extra: + raise ValueError( + f"Array node at {'.'.join(path_parts) or ''} has unexpected keys {sorted(extra)}; " + f"allowed: {sorted(RESERVED_NODE)}" + ) + item_schema = node["_array"] + if not isinstance(item_schema, dict) or not item_schema: + raise ValueError( + f"Array item schema at {'.'.join(path_parts)} must be a non-empty object" + ) + item_specs: list[KeySpec] = [] + for col_name, col_node in item_schema.items(): + if isinstance(col_node, dict) and "_array" in col_node: + raise ValueError( + f"Nested array at {'.'.join(path_parts)}.{col_name} is P8b (not supported in P8a)" + ) + _walk( + col_node, + [col_name], + item_specs, + [], # row-LOCAL paths; nested arrays sink to [] (rejected above) + max_depth=max_depth, + depth=depth + 1, + ) + arrays.append( + ArraySpec( + path=".".join(path_parts), + description=str(node.get("description", "")), + item_specs=item_specs, + key_column=str(node.get("_key", "")), + dedup_rows=_parse_dedup(node.get("_dedup", True), ".".join(path_parts)), + ) + ) + return + + object_values = [v for v in node.values() if isinstance(v, dict)] + scalar_keys = [k for k, v in node.items() if not isinstance(v, dict)] + + is_interior = len(object_values) == len(node) # every value is an object + is_leaf = len(object_values) == 0 # no value is an object + + if not is_interior and not is_leaf: + raise ValueError( + f"Schema node at {'.'.join(path_parts) or ''} mixes object children " + f"and scalar attributes; an interior node's values must all be objects" + ) + + if is_interior: + for child_name, child_node in node.items(): + _walk( + child_node, + path_parts + [child_name], + out, + arrays, + max_depth=max_depth, + depth=depth + 1, + ) + return + + # Leaf node: scalar attributes only. + unknown = [k for k in scalar_keys if k not in RESERVED] + if unknown: + raise ValueError( + f"Leaf at {'.'.join(path_parts)} has unknown attribute(s) {unknown}; " + f"allowed: {sorted(RESERVED)}" + ) + if "description" not in node: + raise ValueError(f"Leaf at {'.'.join(path_parts)} is missing a 'description'") + + kind, enum_values, regex_pattern = _parse_format(str(node.get("format", "string"))) + breadcrumb = " > ".join(path_parts) + out.append( + KeySpec( + path=".".join(path_parts), + effective_description=f"{breadcrumb}: {node['description']}", + format=kind, + enum_values=enum_values, + regex_pattern=regex_pattern, + required=bool(node.get("required", False)), + aliases=list(node.get("aliases", [])), + multivalued=bool(node.get("multivalued", False)), + ) + ) + + +def _compile_both(spec_json: dict[str, Any], max_depth: int = _DEFAULT_MAX_DEPTH): + if not isinstance(spec_json, dict): + raise ValueError("Top-level key schema must be a JSON object") + out: list[KeySpec] = [] + arrays: list[ArraySpec] = [] + for top_name, top_node in spec_json.items(): + if top_name == "_constraints": + continue + _walk(top_node, [top_name], out, arrays, max_depth=max_depth) + return out, arrays + + +def compile( + spec_json: dict[str, Any], max_depth: int = _DEFAULT_MAX_DEPTH +) -> list[KeySpec]: + """Compile to an ordered flat list of SCALAR leaf KeySpecs (array nodes excluded).""" + return _compile_both(spec_json, max_depth)[0] + + +def compile_arrays( + spec_json: dict[str, Any], max_depth: int = _DEFAULT_MAX_DEPTH +) -> list[ArraySpec]: + """Compile the top-level/interior-nested array nodes to an ordered list of ArraySpecs.""" + return _compile_both(spec_json, max_depth)[1] + + +def reassemble( + values: dict[str, Any], + specs: list[KeySpec], + arrays: list[ArraySpec] = None, + array_values: dict[str, Any] = None, +) -> dict[str, Any]: + """Rebuild the nested dict from flat {dotted_path: value}. + + Iterates `specs` (so output key order follows the schema) and places each + present value at its dotted path. Multi-valued leaves are comma-separated + strings and pass through verbatim. Paths absent from `values` are skipped. + + Arrays (P8a): for each ArraySpec in `arrays`, place its pre-rendered + list-of-dicts (from `array_values[path]`) at its dotted path. Scalar-only + callers (arrays/array_values=None) behave identically to before. + """ + out: dict[str, Any] = {} + for spec in specs: + if spec.path not in values: + continue + parts = spec.path.split(".") + node = out + for part in parts[:-1]: + node = node.setdefault(part, {}) + node[parts[-1]] = values[spec.path] + for aspec in arrays or []: + rows = (array_values or {}).get(aspec.path) + if rows is None: + continue + parts = aspec.path.split(".") + node = out + for part in parts[:-1]: + node = node.setdefault(part, {}) + node[parts[-1]] = rows # list-of-dicts + return out diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/validators.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/validators.py new file mode 100644 index 0000000000..e05fabdd3b --- /dev/null +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/validators.py @@ -0,0 +1,115 @@ +"""Deterministic format/type validation for extracted values (QA, spec §9 check 2). + +An empty value passes here (presence is the separate completeness check). For a +multivalued leaf each comma-separated element is validated against the format. +""" + +import math +import re +from datetime import datetime + +from .dataclasses import KeySpec + +_NUMERIC_STRIP = re.compile(r"[,\s$%]") +_DATE_FORMATS = ( + "%m/%d/%Y", + "%m/%d/%y", + "%Y-%m-%d", + "%d/%m/%Y", + "%m-%d-%Y", + "%B %d, %Y", + "%b %d, %Y", + "%m/%Y", + "%Y", +) + + +def _is_number(value: str) -> bool: + cleaned = _NUMERIC_STRIP.sub("", value) + if cleaned in ("", "-", "."): + return False + try: + float(cleaned) + return True + except ValueError: + return False + + +def _is_date(value: str) -> bool: + v = value.strip() + for fmt in _DATE_FORMATS: + try: + datetime.strptime(v, fmt) + return True + except ValueError: + continue + return False + + +def _check_one(value: str, spec: KeySpec) -> bool: + fmt = spec.format + if fmt in ("number", "currency"): + return _is_number(value) + if fmt == "date": + return _is_date(value) + if fmt == "enum": + return value.strip().casefold() in {e.casefold() for e in spec.enum_values} + if fmt == "regex": + try: + return re.fullmatch(spec.regex_pattern, value.strip()) is not None + except re.error: + return True # a broken author-supplied pattern should not fail extraction + # "string" or any free-text hint: nothing to validate. + return True + + +def validate_format(value: str, spec: KeySpec) -> bool: + """True if `value` conforms to `spec.format`. Empty value -> True (completeness is + checked separately). Multivalued -> every element must conform. + """ + if value is None or value.strip() == "": + return True + if spec.multivalued: + elements: list[str] = [e.strip() for e in value.split(",") if e.strip()] + return all(_check_one(e, spec) for e in elements) if elements else True + return _check_one(value, spec) + + +def coerce_number(value: str) -> float | None: + """Parse a numeric/currency string to a float, or None. Strips commas/spaces/$/%.""" + if value is None: + return None + cleaned = _NUMERIC_STRIP.sub("", value) + if cleaned in ("", "-", "."): + return None + try: + n = float(cleaned) + except ValueError: + return None + # float() accepts 'inf'/'nan'/'1e400' as literals — reject non-finite so callers + # (output normalization, constraint comparison) never see inf/nan. + return n if math.isfinite(n) else None + + +def coerce_date(value: str) -> str | None: + """Parse a date string to ISO 'YYYY-MM-DD', or None.""" + if value is None: + return None + v = value.strip() + for fmt in _DATE_FORMATS: + try: + return datetime.strptime(v, fmt).strftime("%Y-%m-%d") + except ValueError: + continue + return None + + +def coerce_enum(value: str, spec: KeySpec) -> "str | None": + """Return the canonical `spec.enum_values` element matching `value` case-insensitively, or None.""" + if value is None: + return None + cf = value.strip().casefold() + for e in spec.enum_values: + if e.casefold() == cf: + return e + return None diff --git a/unstract/agent-kv-schema/tests/__init__.py b/unstract/agent-kv-schema/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/unstract/agent-kv-schema/tests/test_compile.py b/unstract/agent-kv-schema/tests/test_compile.py new file mode 100644 index 0000000000..ff3d4c7c10 --- /dev/null +++ b/unstract/agent-kv-schema/tests/test_compile.py @@ -0,0 +1,250 @@ +"""compile_schema: structural validation + caps on top of the ported compiler.""" + +import pytest + +from unstract.agent_kv_schema import ( + CompiledSchema, + SchemaCaps, + SchemaError, + compile_schema, +) + +VALID = { + "quotation_number": {"description": "The quote number", "required": True}, + "customer": {"name": {"description": "Bill-to name"}}, + "line_items": { + "description": "One row per line", + "_key": "sku", + "_array": { + "sku": {"description": "SKU"}, + "total": {"description": "Line total", "format": "currency"}, + }, + }, + "_constraints": ["count('line_items') >= 1"], +} + + +def test_valid_schema_compiles(): + out = compile_schema(VALID) + assert isinstance(out, CompiledSchema) + assert [s.path for s in out.key_specs] == ["quotation_number", "customer.name"] + assert out.array_specs[0].path == "line_items" + assert out.constraints == ["count('line_items') >= 1"] + + +def test_missing_description_is_schema_error(): + with pytest.raises(SchemaError, match="missing a 'description'"): + compile_schema({"a": {"format": "string"}}) + + +def test_mixed_node_is_schema_error(): + with pytest.raises(SchemaError, match="mixes object children"): + compile_schema({"a": {"description": "x", "b": {"description": "y"}}}) + + +def test_leaf_cap_enforced(): + spec = {f"k{i}": {"description": "d"} for i in range(5)} + caps = SchemaCaps(max_leaves=4) + with pytest.raises(SchemaError, match="max_leaves"): + compile_schema(spec, caps=caps) + + +def test_depth_cap_enforced(): + spec = {"a": {"b": {"c": {"d": {"description": "deep"}}}}} + caps = SchemaCaps(max_depth=3) + with pytest.raises(SchemaError, match="max_depth"): + compile_schema(spec, caps=caps) + + +def test_regex_length_cap(): + spec = {"a": {"description": "d", "format": "regex:" + "x" * 300}} + with pytest.raises(SchemaError, match="max_regex_len"): + compile_schema(spec) + + +def test_bad_constraint_syntax_rejected(): + spec = { + "a": {"description": "d"}, + "_constraints": ["__import__('os').system('true')"], + } + with pytest.raises(SchemaError, match="constraint"): + compile_schema(spec) + + +def test_constraint_call_allowlist(): + spec = {"a": {"description": "d"}, "_constraints": ["foo('a.b') > 1"]} + with pytest.raises(SchemaError, match="constraint"): + compile_schema(spec) + + +def test_constraints_cap(): + spec = {"a": {"description": "d"}, "_constraints": ["a > 0"] * 31} + with pytest.raises(SchemaError, match="max_constraints"): + compile_schema(spec) + + +def test_non_dict_top_level_rejected(): + with pytest.raises(SchemaError): + compile_schema(["not", "a", "dict"]) + + +# --------------------------------------------------------------------------- +# Depth-cap bypass + unbounded-recursion DoS (pre-Greptile critical #2). +# --------------------------------------------------------------------------- + + +def _nest(levels: int) -> dict: + """Build a plain interior schema `levels` deep ending in a leaf.""" + node = {"description": "deep"} + for i in range(levels): + node = {f"l{i}": node} + return node + + +def test_deeply_nested_raises_schema_error_not_recursion_error(): + # Well past max_depth but nowhere near Python's recursion limit: must be a + # clean SchemaError (400), never accepted. + deep, caps = _nest(30), SchemaCaps(max_depth=6) + with pytest.raises(SchemaError, match="max_depth"): + compile_schema(deep, caps=caps) + + +def test_decoy_array_key_cannot_bypass_depth_cap(): + # A top-level field literally named "_array" short-circuited the old + # depth walk (`if "_array" in node: return depth + 1`), so its real + # nesting was never counted. The compiler treats "_array" as a plain + # field name here (an array NODE is one CONTAINING an `_array` key), so + # the nesting under it is real -- and must still be capped. + decoy = {"_array": _nest(30)} + caps = SchemaCaps(max_depth=6) + with pytest.raises(SchemaError, match="max_depth"): + compile_schema(decoy, caps=caps) + + +def test_valid_array_within_limits_still_compiles(): + # Genuine array-column spec (node CONTAINING `_array`) with shallow + # columns must still compile -- the ceiling bounds total structural + # depth, it does not break real arrays. + spec = { + "rows": { + "description": "line items", + "_array": { + "sku": {"description": "SKU"}, + "qty": {"description": "Quantity", "format": "number"}, + }, + } + } + out = compile_schema(spec, caps=SchemaCaps(max_depth=6)) + assert isinstance(out, CompiledSchema) + assert out.array_specs[0].path == "rows" + assert [s.path for s in out.array_specs[0].item_specs] == ["sku", "qty"] + + +def test_pathologically_deep_raises_fast_no_recursion_error(): + # ~5000 levels: 256 KiB of JSON nests far past Python's ~1000 recursion + # limit, so the OLD code raised an uncaught RecursionError (500). Must be + # a clean, fast SchemaError instead. + deep, caps = _nest(5000), SchemaCaps(max_depth=6) + with pytest.raises(SchemaError, match="max_depth"): + compile_schema(deep, caps=caps) + + +def test_pathologically_deep_under_decoy_array_raises_fast(): + # Same DoS but hidden under a decoy `_array` key so the compile.py depth + # pre-check short-circuits -- the guard inside the real recursive walk + # (`kv_schema._walk`) must still catch it as SchemaError, not + # RecursionError. + decoy, caps = {"_array": _nest(5000)}, SchemaCaps(max_depth=6) + with pytest.raises(SchemaError, match="max_depth"): + compile_schema(decoy, caps=caps) + + +# --------------------------------------------------------------------------- +# Author-supplied regex is refused at SUBMIT, not discovered at match time. +# +# Two defects, both found in review: +# 1. The pattern was never compiled here, so an invalid one was accepted and +# only hit per-value in the engine's QA pass -- where `_check_one` swallows +# `re.error` and returns True, silently passing validation the author +# thought they had configured. +# 2. `validate_format` runs the pattern with no time budget, so catastrophic +# backtracking is a DoS. The length cap is NOT a mitigation: `^(a+)+$` is 7 +# characters and takes ~1.9s against 26 `a`s, ~4x per character added +# (measured on this code), so a 40-char value runs for hours -- and +# AGENT_KV_CONCURRENT_LIMIT=5 lets one org pin five shared worker slots +# from a single submit. +# +# The detector is a conservative heuristic, not a proof: it rejects the shape +# behind the realistic cases (a quantifier inside a quantified group). The +# complete fix is a linear-time engine (RE2), which cannot live here because +# this package deliberately has zero dependencies -- UN-4225. +# --------------------------------------------------------------------------- + + +def _schema(pattern: str) -> dict: + return {"amount": {"description": "x", "format": f"regex:{pattern}"}} + + +@pytest.mark.parametrize( + "pattern", + [ + "^(a+)+$", # the classic + "^(a*)*$", + "(?:x+)+", # non-capturing group is not an escape hatch + "^(a+){2,}$", # counted repetition of a quantified group + ], +) +def test_nested_quantifier_is_refused_at_compile(pattern): + spec = _schema(pattern) + with pytest.raises(SchemaError, match="nested quantifier"): + compile_schema(spec) + + +def test_invalid_regex_is_refused_at_compile_not_swallowed_at_match_time(): + spec = _schema("[") + with pytest.raises(SchemaError, match="invalid regex"): + compile_schema(spec) + + +@pytest.mark.parametrize( + "pattern", + [ + r"^\d{3}-\d{4}$", + r"^[A-Z]{2}\d+$", + r"^INV-\d+$", + r"^\$?[\d,]+\.\d{2}$", + ], +) +def test_ordinary_patterns_still_compile(pattern): + """The other half of the heuristic's contract. A detector that rejected + real-world patterns would just push authors off the regex format entirely. + """ + compiled = compile_schema(_schema(pattern)) + assert compiled.key_specs[0].regex_pattern == pattern + + +# The overlapping-alternation family, found in review AFTER the +# nested-quantifier check shipped: `^(a|aa)+$` passes that check and still +# backtracks catastrophically, because at each position the engine can consume +# one `a` or two and must try both on failure. +@pytest.mark.parametrize("pattern", ["^(a|aa)+$", "^(a|ab)+$", "(?:x|xx)+"]) +def test_overlapping_alternation_is_refused(pattern): + spec = _schema(pattern) + with pytest.raises(SchemaError, match="alternatives overlap"): + compile_schema(spec) + + +@pytest.mark.parametrize( + "pattern", + [ + "^(foo|bar)+$", # distinct first chars -> no position admits two parses + r"^(\d|[A-Z])+$", # metacharacter branches are not analysed + "^INV-(A|B)+$", + ], +) +def test_non_overlapping_alternation_still_compiles(pattern): + """The detector compares only LITERAL branches, by the prefix relation. + Refusing every quantified alternation would reject a lot of safe, ordinary + patterns -- `(?:a|b)+` is linear. + """ + compile_schema(_schema(pattern)) diff --git a/unstract/agent-kv-schema/tests/test_constraints.py b/unstract/agent-kv-schema/tests/test_constraints.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/unstract/agent-kv-schema/tests/test_constraints_tolerance.py b/unstract/agent-kv-schema/tests/test_constraints_tolerance.py new file mode 100644 index 0000000000..6bfdbf4d77 --- /dev/null +++ b/unstract/agent-kv-schema/tests/test_constraints_tolerance.py @@ -0,0 +1,131 @@ +"""Equality in a constraint is tolerant, not exact. + +Operands are normalized currency/number values that arrive as floats, so +binary floating point made exact `operator.eq` wrong for the one thing +constraints exist to express. The feature's own headline example reported a +false violation on a CORRECT invoice -- which is worse than no check, because +it trains reviewers to ignore the output. +""" + +import pytest + +from unstract.agent_kv_schema import evaluate_constraints + +# Three identical line items. 8230.4 * 3 is 24691.199999999997 in binary +# floating point, so `sum(...) == 24691.2` was False and the constraint was +# reported violated. +_ROWS = {"line_items": [{"line_total": "8230.40"} for _ in range(3)]} + + +def test_the_headline_example_is_not_a_false_violation(): + violations = evaluate_constraints( + ["grand_total == sum('line_items.line_total')"], + {"grand_total": "24691.20"}, + _ROWS, + ) + assert violations == [] + + +def test_a_real_mismatch_is_still_reported(): + """The tolerance must not swallow genuine errors -- an invoice off by a + cent is exactly what this check is for. + """ + violations = evaluate_constraints( + ["grand_total == sum('line_items.line_total')"], + {"grand_total": "24691.21"}, + _ROWS, + ) + assert violations == ["grand_total == sum('line_items.line_total')"] + + +def test_not_equal_is_the_negation_of_the_tolerant_equality(): + """`!=` must agree with `==`, or a schema author gets both reported.""" + expr = "grand_total != sum('line_items.line_total')" + assert evaluate_constraints([expr], {"grand_total": "24691.20"}, _ROWS) == [expr] + assert evaluate_constraints([expr], {"grand_total": "24691.21"}, _ROWS) == [] + + +def test_comparison_against_exact_zero_still_works(): + """Relative tolerance alone is useless at zero -- every non-zero value is + infinitely far from 0 in relative terms -- which is why abs_tol is also set. + """ + assert evaluate_constraints(["balance == 0"], {"balance": "0.00"}, {}) == [] + assert evaluate_constraints(["balance == 0"], {"balance": "1.00"}, {}) == [ + "balance == 0" + ] + + +def test_ordering_comparisons_stay_exact(): + """Left deliberately exact: a tolerant `<` would make `a < b` and `a == b` + both true at the boundary, and "a total that is too large" is not a + rounding artefact. + """ + assert evaluate_constraints(["total > 100"], {"total": "100.00"}, {}) == [ + "total > 100" + ] + assert evaluate_constraints(["total >= 100"], {"total": "100.00"}, {}) == [] + + +# The tolerance must sit BELOW a cent at every realistic magnitude, not just at +# invoice scale. `rel_tol=1e-9` (the first attempt) is 0.1 at a $100,000,000 +# total, so it silently absorbed a one-cent reconciliation error -- the exact +# failure the check exists to catch. The original cent-mismatch test only +# covered a ~$25,000 total and so could not see it. + + +def test_a_one_cent_error_is_caught_at_one_hundred_million(): + rows = {"line_items": [{"line_total": "50000000.00"} for _ in range(2)]} + expr = "grand_total == sum('line_items.line_total')" + assert evaluate_constraints([expr], {"grand_total": "100000000.01"}, rows) == [expr] + + +def test_a_correct_one_hundred_million_total_is_not_a_false_violation(): + """The other side: tightening must not start reporting correct documents.""" + rows = {"line_items": [{"line_total": "50000000.00"} for _ in range(2)]} + assert ( + evaluate_constraints( + ["grand_total == sum('line_items.line_total')"], + {"grand_total": "100000000.00"}, + rows, + ) + == [] + ) + + +def test_float_noise_is_still_absorbed_over_many_rows(): + """The lower bound on the tolerance. 1,000 rows of 8230.40 do not sum to + exactly 8230400.0 in binary floating point, and that must not be a + violation. + """ + rows = {"line_items": [{"line_total": "8230.40"} for _ in range(1000)]} + assert ( + evaluate_constraints( + ["grand_total == sum('line_items.line_total')"], + {"grand_total": "8230400.00"}, + rows, + ) + == [] + ) + + +# Long arrays. The aggregate uses `math.fsum`, so accumulation error does not +# grow with row count and a correct total stays correct at any realistic array +# length -- while a real one-cent error is still reported at the same length. +@pytest.mark.parametrize("rows", [1_000, 10_000, 100_000]) +def test_a_correct_total_is_not_a_violation_however_many_rows(rows): + items = {"line_items": [{"line_total": "8230.40"}] * rows} + total = f"{8230.40 * rows:.2f}" + assert ( + evaluate_constraints( + ["grand_total == sum('line_items.line_total')"], + {"grand_total": total}, + items, + ) + == [] + ) + + +def test_a_one_cent_error_is_still_caught_at_one_hundred_thousand_rows(): + items = {"line_items": [{"line_total": "8230.40"}] * 100_000} + expr = "grand_total == sum('line_items.line_total')" + assert evaluate_constraints([expr], {"grand_total": "823040000.01"}, items) == [expr] diff --git a/unstract/agent-kv-schema/uv.lock b/unstract/agent-kv-schema/uv.lock new file mode 100644 index 0000000000..cef30e8cf2 --- /dev/null +++ b/unstract/agent-kv-schema/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "unstract-agent-kv-schema" +version = "0.1.0" +source = { editable = "." } diff --git a/unstract/filesystem/src/unstract/filesystem/file_storage_config.py b/unstract/filesystem/src/unstract/filesystem/file_storage_config.py index 870ed30a02..06ba625154 100644 --- a/unstract/filesystem/src/unstract/filesystem/file_storage_config.py +++ b/unstract/filesystem/src/unstract/filesystem/file_storage_config.py @@ -29,10 +29,12 @@ def get_provider(var_name: str, default: str = "minio") -> FileStorageProvider: FileStorageType.WORKFLOW_EXECUTION: StorageType.SHARED_TEMPORARY, FileStorageType.API_EXECUTION: StorageType.SHARED_TEMPORARY, FileStorageType.HITL_FILES: StorageType.SHARED_TEMPORARY, # Use shared temporary for HITL files + FileStorageType.AGENT_KV: StorageType.SHARED_TEMPORARY, } FILE_STORAGE_CREDENTIALS_TO_ENV_NAME_MAPPING = { FileStorageType.WORKFLOW_EXECUTION: "WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS", FileStorageType.API_EXECUTION: "API_FILE_STORAGE_CREDENTIALS", FileStorageType.HITL_FILES: "HITL_FILES_FILE_STORAGE_CREDENTIALS", + FileStorageType.AGENT_KV: "AGENT_KV_FILE_STORAGE_CREDENTIALS", } diff --git a/unstract/filesystem/src/unstract/filesystem/file_storage_types.py b/unstract/filesystem/src/unstract/filesystem/file_storage_types.py index 2b6956fe69..b27241071c 100644 --- a/unstract/filesystem/src/unstract/filesystem/file_storage_types.py +++ b/unstract/filesystem/src/unstract/filesystem/file_storage_types.py @@ -5,3 +5,4 @@ class FileStorageType(Enum): WORKFLOW_EXECUTION = "WORKFLOW_EXECUTION" API_EXECUTION = "API_EXECUTION" HITL_FILES = "HITL_FILES" + AGENT_KV = "AGENT_KV" diff --git a/unstract/filesystem/tests/__init__.py b/unstract/filesystem/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/unstract/filesystem/tests/test_agent_kv_storage_type.py b/unstract/filesystem/tests/test_agent_kv_storage_type.py new file mode 100644 index 0000000000..63f44ae4da --- /dev/null +++ b/unstract/filesystem/tests/test_agent_kv_storage_type.py @@ -0,0 +1,15 @@ +from unstract.filesystem.file_storage_config import ( + FILE_STORAGE_CREDENTIALS_TO_ENV_NAME_MAPPING, + STORAGE_MAPPING, +) +from unstract.filesystem.file_storage_types import FileStorageType + + +def test_agent_kv_type_exists_and_is_mapped(): + t = FileStorageType.AGENT_KV + assert t.value == "AGENT_KV" + assert t in STORAGE_MAPPING + assert ( + FILE_STORAGE_CREDENTIALS_TO_ENV_NAME_MAPPING[t] + == "AGENT_KV_FILE_STORAGE_CREDENTIALS" + ) diff --git a/uv.lock b/uv.lock index d7dd25246b..b69549e83e 100644 --- a/uv.lock +++ b/uv.lock @@ -3708,6 +3708,11 @@ test-rig = [ ] workers = [{ name = "unstract-workers", editable = "workers" }] +[[package]] +name = "unstract-agent-kv-schema" +version = "0.1.0" +source = { editable = "unstract/agent-kv-schema" } + [[package]] name = "unstract-connectors" version = "0.0.3" @@ -3955,6 +3960,7 @@ dependencies = [ { name = "python-socketio" }, { name = "redis" }, { name = "requests" }, + { name = "unstract-agent-kv-schema" }, { name = "unstract-connectors" }, { name = "unstract-core" }, { name = "unstract-filesystem" }, @@ -3980,6 +3986,7 @@ requires-dist = [ { name = "python-socketio", specifier = ">=5.9.0" }, { name = "redis", specifier = ">=4.5.0,<6.0.0" }, { name = "requests", specifier = ">=2.33.0,<3.0.0" }, + { name = "unstract-agent-kv-schema", editable = "unstract/agent-kv-schema" }, { name = "unstract-connectors", editable = "unstract/connectors" }, { name = "unstract-core", editable = "unstract/core" }, { name = "unstract-filesystem", editable = "unstract/filesystem" }, diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py new file mode 100644 index 0000000000..b74cb1ed31 --- /dev/null +++ b/workers/ide_callback/agent_kv_tasks.py @@ -0,0 +1,181 @@ +"""Agent-KV terminal callbacks (spec §5.3). Thin: parse, finalize, webhook. + +These are the Celery ``link``/``link_error`` callbacks that +``agent_kv.dispatch.dispatch_job`` (backend, Task 8) attaches to the +executor dispatch: ``agent_kv_complete`` on success, ``agent_kv_error`` on +an unhandled executor exception. Both run on the dedicated +``agent_kv_callback`` queue (see +``shared/infrastructure/config/registry.py``) served by the IDE_CALLBACK +worker, and both terminalize the job via +``InternalAPIClient.agent_kv_finalize`` (Task 11's internal endpoint) +before firing the completion webhook (Task 13). +""" + +import logging +import os +from typing import Any + +from queue_backend import worker_task +from shared.utils.webhook_notify import send_webhook + +logger = logging.getLogger(__name__) + +_UNKNOWN = "Executor failed without an error message" + + +def _get_api_client(): + """Lazily build an InternalAPIClient. + + Lazy import + plain instantiation (no shared config/session): mirrors + ``queue_backend/pg_queue/consumer.py`` and ``.../reaper.py``'s + ``_get_api_client`` helpers, which use this exact pattern to keep the + module import-cycle-free at load time. Unlike ``ide_callback.tasks``'s + ``_get_api_client`` (which returns a ``PromptStudioAPIClient`` scoped to + a handful of prompt-studio endpoints), this callback talks to a general + internal endpoint, so it goes through the general-purpose + ``InternalAPIClient`` facade that owns ``agent_kv_finalize``. + """ + from shared.api import InternalAPIClient + + return InternalAPIClient() + + +def _resolve_error(failed_task_id: str, explicit: str | None = None) -> str: + """Resolve the real error text for the ``agent_kv_error`` link_error callback. + + Mirrors ``ide_callback.tasks._get_task_error``'s precedence exactly: + prefer ``explicit`` when the caller already has it. The PG-queue + transport's self-chained error path (``queue_backend/pg_queue/consumer.py``'s + ``_chain_continuation``) hands the real exception through + ``callback_kwargs["error"]`` -- on that path the executor ran eagerly and + never wrote a Celery result backend entry under ``failed_task_id``, so + the ``AsyncResult`` lookup below would come back empty. The Celery + ``link_error`` path (``agent_kv.dispatch.dispatch_job``) passes no + explicit error and relies on the result backend, then ``_UNKNOWN``. + + Falsy-aware (``explicit or ...``), matching ``_get_task_error``'s own + ``if explicit is not None`` intent in practice: an empty-string + ``explicit`` (e.g. an executor that raised with no message) must not be + persisted verbatim as a blank error -- it falls through to the result + backend lookup, then ``_UNKNOWN``, same as an absent explicit error. + + Kept as a sibling implementation rather than importing + ``ide_callback.tasks._get_task_error`` directly: ``ide_callback/tasks.py`` + already imports *this* module at its own bottom (so both of + ``workers/worker.py``'s task-loading mechanisms register + ``agent_kv_complete``/``agent_kv_error`` -- see that file's comment). + Importing back from ``tasks.py`` here would still resolve (Python + tolerates the resulting redundant re-import under the two different + ``sys.modules`` names that mechanism ends up using), but it adds a + two-way coupling between the modules for ~10 lines of small, stable + logic that isn't worth the added complexity to reason about. + """ + if explicit: + return explicit + try: + from celery import current_app as app + from celery.result import AsyncResult + + res = AsyncResult(failed_task_id, app=app) + if res.result: + return str(res.result) + except Exception: + pass + return _UNKNOWN + + +@worker_task(name="agent_kv_complete") +def agent_kv_complete( + result_dict: dict[str, Any], + callback_kwargs: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Celery link callback after the agent-kv executor task returns. + + ``result_dict["success"]`` reflects the *executor's* own outcome: it can + still be ``False`` here even though this is the success ``link`` (the + executor caught its error and returned a structured failure instead of + raising, which would have routed to ``agent_kv_error`` instead). Either + way the job is finalized via the internal API, then the webhook fires + if the response says to. + """ + cb = callback_kwargs or {} + job_id = cb.get("job_id", "") + org_id = cb.get("org_id", "") + api = _get_api_client() + + try: + if not result_dict.get("success", False): + error = result_dict.get("error") or _UNKNOWN + logger.error( + "agent_kv executor reported failure: job_id=%s error=%s", job_id, error + ) + out = api.agent_kv_finalize(job_id, org_id, success=False, error=error) + else: + data = result_dict.get("data") or {} + out = api.agent_kv_finalize( + job_id, + org_id, + success=True, + result=data.get("output") or {}, + usage_summary=data.get("usage_summary"), + ) + + _maybe_webhook(out, job_id) + return {"job_id": job_id, "finalized": out.get("finalized", False)} + + except Exception: + logger.exception( + "agent_kv_complete callback failed: job_id=%s org_id=%s", job_id, org_id + ) + raise + + +@worker_task(name="agent_kv_error") +def agent_kv_error( + failed_task_id: str, + callback_kwargs: dict[str, Any] | None = None, +) -> dict[str, Any] | None: + """Celery link_error callback when the agent-kv executor task raises.""" + cb = callback_kwargs or {} + job_id = cb.get("job_id", "") + org_id = cb.get("org_id", "") + api = _get_api_client() + + try: + error = _resolve_error(failed_task_id, explicit=cb.get("error")) + logger.error("agent_kv executor task failed: job_id=%s error=%s", job_id, error) + out = api.agent_kv_finalize(job_id, org_id, success=False, error=error) + + _maybe_webhook(out, job_id) + return {"job_id": job_id, "finalized": out.get("finalized", False)} + + except Exception: + logger.exception( + "agent_kv_error callback failed: job_id=%s org_id=%s", job_id, org_id + ) + return None + + +def _maybe_webhook(finalize_response: dict[str, Any], job_id: str) -> None: + """Fire the completion webhook, but only for a fresh finalize. + + ``finalized`` is ``False`` for a duplicate/late finalize call (the job + was already terminal) as well as for an unknown job -- either way the + webhook already fired (or never should), so firing again here would + double-notify the caller. + """ + if not finalize_response.get("finalized"): + return + url = finalize_response.get("webhook_url") or "" + if not url: + return + # Test/dev stacks only (e2e lane): waive the SSRF guards so a receiver on + # the compose host is reachable. Unset/false in production. + allow_insecure = os.environ.get( + "AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE", "" + ).lower() in ("1", "true", "yes") + send_webhook( + url, + {"job_id": job_id, "status": finalize_response.get("status", "")}, + allow_insecure=allow_insecure, + ) diff --git a/workers/ide_callback/tasks.py b/workers/ide_callback/tasks.py index 1a298dffe3..968879bac6 100644 --- a/workers/ide_callback/tasks.py +++ b/workers/ide_callback/tasks.py @@ -693,3 +693,19 @@ def extraction_error( source, file_id, ) + + +# ------------------------------------------------------------------ +# Agent-KV terminal callbacks (spec §5.3) +# +# ``workers/worker.py``'s ``load_worker_tasks()`` registers this worker +# type's tasks by loading THIS file directly (by path, under the bare +# module name ``"tasks"``) rather than importing the ``ide_callback`` +# package -- so ``agent_kv_complete``/``agent_kv_error`` only bind to the +# Celery app if something imports them as a side effect of importing this +# module. Import at the bottom (not the top) to avoid a circular import: +# ``agent_kv_tasks`` doesn't currently import back from here, but keeping +# task-module imports last matches this file's own load-bearing position +# in the worker boot sequence. +# ------------------------------------------------------------------ +from ide_callback import agent_kv_tasks # noqa: E402, F401 diff --git a/workers/ide_callback/tests/__init__.py b/workers/ide_callback/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workers/ide_callback/tests/test_agent_kv_callbacks.py b/workers/ide_callback/tests/test_agent_kv_callbacks.py new file mode 100644 index 0000000000..4b1e2f1ef1 --- /dev/null +++ b/workers/ide_callback/tests/test_agent_kv_callbacks.py @@ -0,0 +1,401 @@ +"""Unit tests for Agent-KV terminal callback tasks (spec §5.3). + +Mirrors ``tests/test_ide_callback.py``'s style: task functions are called +directly (bypassing Celery task machinery) since we're testing callback +logic, not Celery routing. ``_get_api_client`` and ``send_webhook`` are +mocked at the ``ide_callback.agent_kv_tasks`` import site. +""" + +from unittest.mock import MagicMock, patch + +import pytest + +_PATCH_GET_CLIENT = "ide_callback.agent_kv_tasks._get_api_client" +_PATCH_SEND_WEBHOOK = "ide_callback.agent_kv_tasks.send_webhook" +_PATCH_ASYNC_RESULT = "celery.result.AsyncResult" + +_UNKNOWN = "Executor failed without an error message" + + +@pytest.fixture +def mock_api(): + """A mocked InternalAPIClient whose agent_kv_finalize succeeds with a webhook.""" + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": True, + "webhook_url": "https://example.com/hook", + "status": "completed", + } + return api + + +@pytest.fixture +def cb_kwargs(): + """Standard callback_kwargs, matching agent_kv.dispatch.dispatch_job's shape.""" + return {"job_id": "job-1", "org_id": "org-1"} + + +# --------------------------------------------------------------------------- +# agent_kv_complete +# --------------------------------------------------------------------------- + + +class TestAgentKvComplete: + def _call(self, result_dict, callback_kwargs=None): + from ide_callback.agent_kv_tasks import agent_kv_complete + + return agent_kv_complete(result_dict, callback_kwargs) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_success_path_finalizes_true_with_engine_result( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + result_dict = { + "success": True, + "data": { + "output": {"field_1": "value_1"}, + "usage_summary": {"total_tokens": 42}, + }, + "error": None, + } + + result = self._call(result_dict, cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", + "org-1", + success=True, + result={"field_1": "value_1"}, + usage_summary={"total_tokens": 42}, + ) + assert result == {"job_id": "job-1", "finalized": True} + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_missing_output_and_usage_summary_default_to_empty( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + result_dict = {"success": True, "data": {}, "error": None} + + self._call(result_dict, cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=True, result={}, usage_summary=None + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_executor_reported_failure_finalizes_false_with_error( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + result_dict = {"success": False, "data": None, "error": "boom"} + + self._call(result_dict, cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error="boom" + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_executor_failure_with_no_error_message_uses_fallback( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + result_dict = {"success": False, "data": None, "error": None} + + self._call(result_dict, cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error=_UNKNOWN + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_none_callback_kwargs_uses_empty_defaults( + self, mock_get_client, mock_send_webhook, mock_api + ): + mock_get_client.return_value = mock_api + result_dict = {"success": True, "data": {"output": {}}, "error": None} + + result = self._call(result_dict, None) + + mock_api.agent_kv_finalize.assert_called_once_with( + "", "", success=True, result={}, usage_summary=None + ) + assert result["job_id"] == "" + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_returns_finalized_false_when_finalize_says_so( + self, mock_get_client, mock_send_webhook, cb_kwargs + ): + """A duplicate/late callback: the backend reports finalized=False.""" + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": False, + "webhook_url": "", + "status": "completed", + } + mock_get_client.return_value = api + result_dict = {"success": True, "data": {"output": {}}, "error": None} + + result = self._call(result_dict, cb_kwargs) + + assert result == {"job_id": "job-1", "finalized": False} + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_finalize_raising_is_logged_and_reraised( + self, mock_get_client, mock_send_webhook, cb_kwargs, caplog + ): + """agent_kv_complete mirrors ide_index_complete: log, then re-raise.""" + import logging + + api = MagicMock() + api.agent_kv_finalize.side_effect = RuntimeError("backend unreachable") + mock_get_client.return_value = api + result_dict = {"success": True, "data": {"output": {}}, "error": None} + + with caplog.at_level(logging.ERROR, logger="ide_callback.agent_kv_tasks"): + with pytest.raises(RuntimeError, match="backend unreachable"): + self._call(result_dict, cb_kwargs) + + assert "agent_kv_complete callback failed" in caplog.text + mock_send_webhook.assert_not_called() + + +# --------------------------------------------------------------------------- +# agent_kv_error +# --------------------------------------------------------------------------- + + +class TestAgentKvError: + def _call(self, failed_task_id, callback_kwargs=None): + from ide_callback.agent_kv_tasks import agent_kv_error + + return agent_kv_error(failed_task_id, callback_kwargs) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_error_link_finalizes_false_with_result_backend_error( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + mock_async_result = MagicMock() + mock_async_result.result = RuntimeError("executor crashed") + + with patch(_PATCH_ASYNC_RESULT, return_value=mock_async_result): + result = self._call("failed-task-1", cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error="executor crashed" + ) + assert result == {"job_id": "job-1", "finalized": True} + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_error_link_falls_back_when_no_result_available( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + mock_async_result = MagicMock() + mock_async_result.result = None + + with patch(_PATCH_ASYNC_RESULT, return_value=mock_async_result): + self._call("failed-task-2", cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error=_UNKNOWN + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_error_link_falls_back_when_lookup_raises( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs + ): + mock_get_client.return_value = mock_api + + with patch(_PATCH_ASYNC_RESULT, side_effect=RuntimeError("backend down")): + self._call("failed-task-3", cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error=_UNKNOWN + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_none_callback_kwargs_uses_empty_defaults( + self, mock_get_client, mock_send_webhook, mock_api + ): + mock_get_client.return_value = mock_api + + with patch(_PATCH_ASYNC_RESULT, return_value=MagicMock(result=None)): + result = self._call("failed-task-4", None) + + mock_api.agent_kv_finalize.assert_called_once_with( + "", "", success=False, error=_UNKNOWN + ) + assert result["job_id"] == "" + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_pg_transport_explicit_error_preferred_over_async_result( + self, mock_get_client, mock_send_webhook, mock_api + ): + """PG-queue self-chained path: callback_kwargs carries the real error. + + ``queue_backend/pg_queue/consumer.py``'s ``_chain_continuation`` injects + the executor's real error into ``callback_kwargs["error"]`` because the + PG path runs the executor eagerly and never writes a Celery result + backend entry under ``failed_task_id`` -- so this must be preferred + over (and must skip) the ``AsyncResult`` lookup entirely. + """ + mock_get_client.return_value = mock_api + pg_cb_kwargs = {"job_id": "job-1", "org_id": "org-1", "error": "real cause"} + + with patch(_PATCH_ASYNC_RESULT) as mock_async_result_cls: + result = self._call("failed-task-pg-1", pg_cb_kwargs) + + mock_async_result_cls.assert_not_called() + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error="real cause" + ) + assert result == {"job_id": "job-1", "finalized": True} + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_pg_transport_empty_explicit_error_falls_back_to_async_result( + self, mock_get_client, mock_send_webhook, mock_api + ): + """An empty-string explicit error must not be persisted verbatim -- + it's falsy, so it falls through to the result-backend lookup (then + ``_UNKNOWN``), same as an absent explicit error. + """ + mock_get_client.return_value = mock_api + pg_cb_kwargs = {"job_id": "job-1", "org_id": "org-1", "error": ""} + mock_async_result = MagicMock() + mock_async_result.result = RuntimeError("real backend cause") + + with patch(_PATCH_ASYNC_RESULT, return_value=mock_async_result): + self._call("failed-task-empty-error", pg_cb_kwargs) + + mock_api.agent_kv_finalize.assert_called_once_with( + "job-1", "org-1", success=False, error="real backend cause" + ) + + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_finalize_raising_is_swallowed_and_logged( + self, mock_get_client, mock_send_webhook, cb_kwargs, caplog + ): + """agent_kv_error mirrors ide_index_error: swallow, log, don't raise.""" + import logging + + api = MagicMock() + api.agent_kv_finalize.side_effect = RuntimeError("backend unreachable") + mock_get_client.return_value = api + + with ( + patch(_PATCH_ASYNC_RESULT, return_value=MagicMock(result=None)), + caplog.at_level(logging.ERROR, logger="ide_callback.agent_kv_tasks"), + ): + result = self._call("failed-task-6", cb_kwargs) + + assert result is None + assert "agent_kv_error callback failed" in caplog.text + mock_send_webhook.assert_not_called() + + +# --------------------------------------------------------------------------- +# Webhook firing rules (shared by both callbacks) +# --------------------------------------------------------------------------- + + +class TestWebhookFiring: + """Webhook fires iff finalize returns finalized=True AND a non-empty webhook_url.""" + + def _call_complete(self, callback_kwargs, api): + from ide_callback.agent_kv_tasks import agent_kv_complete + + with patch(_PATCH_GET_CLIENT, return_value=api): + result_dict = {"success": True, "data": {"output": {"x": 1}}, "error": None} + return agent_kv_complete(result_dict, callback_kwargs) + + @patch(_PATCH_SEND_WEBHOOK) + def test_webhook_fires_when_finalized_true_and_url_present( + self, mock_send_webhook, cb_kwargs + ): + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": True, + "webhook_url": "https://example.com/hook", + "status": "completed", + } + + self._call_complete(cb_kwargs, api) + + mock_send_webhook.assert_called_once_with( + "https://example.com/hook", + {"job_id": "job-1", "status": "completed"}, + allow_insecure=False, + ) + + @patch(_PATCH_SEND_WEBHOOK) + def test_webhook_not_fired_on_duplicate_finalize_false( + self, mock_send_webhook, cb_kwargs + ): + """Duplicate/late finalize: finalized=False must not re-fire the webhook, + even though the job (from an earlier finalize) still carries a webhook_url. + """ + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": False, + "webhook_url": "https://example.com/hook", + "status": "completed", + } + + self._call_complete(cb_kwargs, api) + + mock_send_webhook.assert_not_called() + + @patch(_PATCH_SEND_WEBHOOK) + def test_webhook_not_fired_when_url_empty(self, mock_send_webhook, cb_kwargs): + """finalized=True but the job has no webhook configured: nothing to call.""" + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": True, + "webhook_url": "", + "status": "completed", + } + + self._call_complete(cb_kwargs, api) + + mock_send_webhook.assert_not_called() + + @patch(_PATCH_SEND_WEBHOOK) + def test_webhook_fires_from_error_link_too(self, mock_send_webhook, cb_kwargs): + from ide_callback.agent_kv_tasks import agent_kv_error + + api = MagicMock() + api.agent_kv_finalize.return_value = { + "finalized": True, + "webhook_url": "https://example.com/hook", + "status": "failed", + } + + with ( + patch(_PATCH_GET_CLIENT, return_value=api), + patch(_PATCH_ASYNC_RESULT, return_value=MagicMock(result=None)), + ): + agent_kv_error("failed-task-5", cb_kwargs) + + mock_send_webhook.assert_called_once_with( + "https://example.com/hook", + {"job_id": "job-1", "status": "failed"}, + allow_insecure=False, + ) diff --git a/workers/ide_callback/worker.py b/workers/ide_callback/worker.py index cb1bb4200c..36a2c25ca8 100644 --- a/workers/ide_callback/worker.py +++ b/workers/ide_callback/worker.py @@ -56,3 +56,16 @@ def check_ide_callback_health(): WorkerRegistry.register_health_check( WorkerType.IDE_CALLBACK, "ide_callback_health", check_ide_callback_health ) + +# Import task modules so Celery registers them on this app -- mirrors how +# other single-app workers (e.g. executor/worker.py) import their tasks +# module explicitly. NOTE: this module is not the actual Celery entrypoint +# in production today -- both docker-compose and run-worker(.sh|-docker.sh) +# always launch the unified `workers/worker.py` (`celery -A worker worker`) +# for this worker type, which loads `ide_callback/tasks.py` itself via +# `load_worker_tasks()` (see that file's bottom-of-file import of +# `agent_kv_tasks` for the mechanism that actually applies in production). +# These imports are kept here anyway so this module stays correct/self- +# registering if it's ever wired up as a real `-A` target. +import ide_callback.agent_kv_tasks # noqa: E402, F401 +import ide_callback.tasks # noqa: E402, F401 diff --git a/workers/pyproject.toml b/workers/pyproject.toml index da672e0661..6842772245 100644 --- a/workers/pyproject.toml +++ b/workers/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "unstract-tool-sandbox", "unstract-workflow-execution", "unstract-filesystem", + "unstract-agent-kv-schema", # Caching "redis>=4.5.0,<6.0.0", # Redis client for worker cache access # PG Queue — direct Postgres access for the bespoke SKIP LOCKED queue @@ -223,6 +224,7 @@ exclude_lines = [ [tool.uv.sources] unstract-filesystem = { path = "../unstract/filesystem", editable = true } +unstract-agent-kv-schema = { path = "../unstract/agent-kv-schema", editable = true } unstract-workflow-execution = { path = "../unstract/workflow-execution", editable = true } unstract-tool-sandbox = { path = "../unstract/tool-sandbox", editable = true } unstract-tool-registry = { path = "../unstract/tool-registry", editable = true } diff --git a/workers/run-worker-docker.sh b/workers/run-worker-docker.sh index 05d2b9398f..eeafc65dfc 100755 --- a/workers/run-worker-docker.sh +++ b/workers/run-worker-docker.sh @@ -62,7 +62,10 @@ declare -A WORKER_QUEUES=( ["log_consumer"]="celery_log_task_queue" ["scheduler"]="scheduler" ["${EXECUTOR_WORKER_TYPE}"]="celery_executor_legacy" - ["ide_callback"]="ide_callback" + # agent_kv_callback carries the Agent-KV terminal callbacks + # (agent_kv_complete/agent_kv_error, spec §5.3) dispatched by + # backend/agent_kv/dispatch.py; ide_callback owns both queues. + ["ide_callback"]="ide_callback,agent_kv_callback" ) # Worker health ports diff --git a/workers/run-worker.sh b/workers/run-worker.sh index 5bd41dfbeb..e538831c8d 100755 --- a/workers/run-worker.sh +++ b/workers/run-worker.sh @@ -87,7 +87,7 @@ declare -rA PG_CONSUMER_ROLES=( # succeeds, so nothing errors at the producer. ["$PG_ROLE_NOTIFICATION"]="notification;notifications,notifications_webhook,notifications_email,notifications_sms,notifications_priority" # Prompt Studio IDE callbacks (ide_index_*/ide_prompt_*/extraction_*). - ["$PG_ROLE_IDE_CALLBACK"]="ide_callback;ide_callback" + ["$PG_ROLE_IDE_CALLBACK"]="ide_callback;ide_callback,agent_kv_callback" ) declare -rA PG_QUEUE_MEMBERS=( ["$PG_QUEUE_CONSUMER_TYPE"]=1 @@ -168,7 +168,10 @@ declare -A WORKER_QUEUES=( ["notification"]="notifications,notifications_webhook,notifications_email,notifications_sms,notifications_priority" ["scheduler"]="scheduler" ["${EXECUTOR_WORKER_TYPE}"]="celery_executor_legacy" - ["${IDE_CALLBACK_WORKER_TYPE}"]="${IDE_CALLBACK_WORKER_TYPE}" + # agent_kv_callback carries the Agent-KV terminal callbacks + # (agent_kv_complete/agent_kv_error, spec §5.3) dispatched by + # backend/agent_kv/dispatch.py; ide_callback owns both queues. + ["${IDE_CALLBACK_WORKER_TYPE}"]="${IDE_CALLBACK_WORKER_TYPE},agent_kv_callback" # The PG queue (in pg_queue_message) this consumer polls — exported as # WORKER_PG_QUEUE_CONSUMER_QUEUE, not a Celery --queues value. ["$PG_QUEUE_CONSUMER_TYPE"]="notifications" diff --git a/workers/sample.env b/workers/sample.env index b6c1078571..bffe936a26 100644 --- a/workers/sample.env +++ b/workers/sample.env @@ -398,6 +398,18 @@ UNSTRACT_RUNNER_API_BACKOFF_FACTOR=3 # File Storage Credentials (MinIO) WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' API_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' +# Bucket-rooted root the backend stages Agent-KV inputs under +# ({AGENT_KV_STORAGE_DIR_PREFIX}/{org_id}/{job_id}/...); the executor plugin reads +# `input_ref` from it and keys its OCR cache under {prefix}/{org_id}/cache/. +# MUST be identical to backend/sample.env's value -- the first segment is the +# bucket (must exist; the MinIO dev bootstrap creates `unstract`). +AGENT_KV_STORAGE_DIR_PREFIX="unstract/agent_kv" +# Read directly by the cloud agentic_table executor plugin's blind-API +# operation (workers/plugins/agentic_table, +# not checked in here) to stage/fetch Agent-KV job input+result files -- same +# shape and bucket as its siblings above. Must match docker/sample.env's copy +# (the backend, which also reads this key, gets its value from there). +AGENT_KV_FILE_STORAGE_CREDENTIALS='{"provider": "minio", "credentials": {"endpoint_url": "http://unstract-minio:9000", "key": "minio", "secret": "minio123"}}' # Remote storage for Prompt Studio / IDE flows. Must match backend/sample.env. # Required by executor and ide-callback workers; missing/empty values raise FileStorageError in EnvHelper.get_storage(). @@ -527,6 +539,7 @@ GOOGLE_OAUTH2_SECRET= # UNSTRACT_RUNNER_HOST=http://localhost # WORKFLOW_EXECUTION_FILE_STORAGE_CREDENTIALS={"provider": "minio", "credentials": {"endpoint_url": "http://localhost:9000", "key": "minio", "secret": "minio123"}} # API_FILE_STORAGE_CREDENTIALS={"provider": "minio", "credentials": {"endpoint_url": "http://localhost:9000", "key": "minio", "secret": "minio123"}} +# AGENT_KV_FILE_STORAGE_CREDENTIALS={"provider": "minio", "credentials": {"endpoint_url": "http://localhost:9000", "key": "minio", "secret": "minio123"}} # Flipt Service FLIPT_SERVICE_AVAILABLE=False @@ -549,3 +562,8 @@ PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python # BACKOFF = base seconds, doubling each attempt, with each sleep capped at 5.0s. # WORKER_PG_QUEUE_CONNECT_RETRIES=3 # WORKER_PG_QUEUE_CONNECT_BACKOFF=0.5 + +# Test/dev ONLY: waive the completion-webhook SSRF guards (https scheme + +# public host) on the ide-callback worker so the e2e lane can deliver to a +# receiver on the compose host. NEVER set in production. +# AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE=false diff --git a/workers/scheduler/agent_kv_tasks.py b/workers/scheduler/agent_kv_tasks.py new file mode 100644 index 0000000000..0f266f7e46 --- /dev/null +++ b/workers/scheduler/agent_kv_tasks.py @@ -0,0 +1,71 @@ +"""Thin scheduler-side tasks for the Agent-KV maintenance periodics (spec §5.4). + +Mirrors ``dashboard_metrics_tasks.py``'s shape and role in this worker (UN-3796): +these do **no** work themselves, each just calls a backend internal endpoint that +does the real work, because the workers image has no Django and the ORM-heavy +job-row work (terminalizing stuck/never-dispatched jobs, deleting staged files) +can only run there. + +Unlike the dashboard-metrics proxies, the calls here go through the shared +``InternalAPIClient`` facade (``shared.api.InternalAPIClient``) rather than a +bespoke ``httpx`` client -- ``agent_kv_finalize`` already lives on that facade +(Task 11) for the Agent-KV terminal callbacks +(``ide_callback/agent_kv_tasks.py::_get_api_client``), and the sweep/TTL-cleanup +calls added here (``agent_kv_sweep``/``agent_kv_ttl_cleanup``, +``workers/shared/api/internal_client.py``) are its siblings on the same client, +not a second HTTP stack for the same feature. + +Two internal endpoints, both platform-wide (no ``org_id``, spec §5.4): + +- ``POST /internal/v1/agent-kv/sweep/`` -- terminalizes never-dispatched + ``PENDING`` jobs and stuck ``DISPATCHED``/``RUNNING`` jobs. +- ``POST /internal/v1/agent-kv/ttl-cleanup/`` -- deletes staged input/result + files for jobs past ``expires_at`` and blanks their refs. + +Both handlers are idempotent and batch-capped server-side +(``backend/agent_kv/internal_views.py``), so redelivery/backlog from a missed +tick is safe without extra guards here -- exactly like the dashboard-metrics +proxies' own redelivery story. + +**Not registered under a dotted Beat-mirroring name for a Beat row that +exists** (contrast ``dashboard_metrics_tasks.py``, whose names are pinned to +verbatim Beat rows it mirrors): there is no pre-existing Beat/PG-scheduler +entry for these two tasks to match, since Agent-KV shipped with the internal +endpoints but no periodic registration (spec §5.4, this feature's own +deploy-checklist gap). The ``agent_kv.sweep``/``agent_kv.ttl_cleanup`` wire +names below are this task pair's own new contract -- an operator registers a +``PgPeriodicTask`` row against them (see `docs/agent-kv-api.md`'s deploy +checklist for the exact shape), the same PG-scheduler mechanism the +dashboard-metrics periodics use. +""" + +import logging +from typing import Any + +from queue_backend import worker_task + +logger = logging.getLogger(__name__) + + +def _get_api_client(): + """Lazily build an InternalAPIClient. + + Mirrors ``ide_callback.agent_kv_tasks._get_api_client``'s lazy import + + plain instantiation (no shared config/session) for the same reason: keeps + this module import-cycle-free at load time. + """ + from shared.api import InternalAPIClient + + return InternalAPIClient() + + +@worker_task(name="agent_kv.sweep") +def agent_kv_sweep() -> dict[str, Any]: + """Terminalize never-dispatched PENDING and stuck Agent-KV jobs (spec §5.4).""" + return _get_api_client().agent_kv_sweep() + + +@worker_task(name="agent_kv.ttl_cleanup") +def agent_kv_ttl_cleanup() -> dict[str, Any]: + """Delete staged Agent-KV input/result files past their TTL (spec §5.4).""" + return _get_api_client().agent_kv_ttl_cleanup() diff --git a/workers/scheduler/tasks.py b/workers/scheduler/tasks.py index d79f7bf758..56bcebcfb5 100644 --- a/workers/scheduler/tasks.py +++ b/workers/scheduler/tasks.py @@ -23,7 +23,10 @@ # worker-general (this happened — the bare form shipped and broke the general # worker at flag-off, where PG is not even involved). # `/app` is on PYTHONPATH (run-worker-docker.sh), so `scheduler.` resolves under both. -from scheduler import dashboard_metrics_tasks # noqa: F401, E402 (side-effect import) +# +# The same reasoning covers agent_kv_tasks, which registers the Agent-KV +# sweep/TTL-cleanup periodics (spec §5.4) on this worker type. +from scheduler import agent_kv_tasks, dashboard_metrics_tasks from shared.enums.status_enums import PipelineStatus from shared.enums.worker_enums import QueueName from shared.infrastructure.config import WorkerConfig @@ -47,6 +50,28 @@ # Import the exact backend logic to ensure consistency +# Both modules above are imported ONLY for the side effect of registering their +# @worker_task entries on this worker; nothing here calls into them. Naming them +# in a module-level tuple is what keeps them imported. +# +# A bare `noqa` directive is NOT enough, and that is not a style preference — it +# reproduced cause of this exact line being deleted once already. Two hooks +# interact: +# 1. ruff's isort (I001) merges two `from scheduler import X` statements into +# one parenthesised statement, which relocates each trailing noqa directive onto +# a MEMBER line. +# 2. F401 is reported against the statement, whose first line now carries no +# directive, so pycln (`[tool.pycln] all = true` — it removes side-effect +# imports by design) deletes both members. +# That is what happened in a79e9d69: pre-commit.ci silently dropped BOTH this +# import and the pre-existing dashboard-metrics one, un-registering the +# dashboard-metrics periodics that had been live since UN-3796 as collateral. +# A genuine reference cannot be fixed away by either hook. +# +# Deleting this tuple re-arms that failure, and it fails SILENTLY: the worker +# starts fine and only the scheduled messages are rejected as unknown tasks. +_PERIODIC_TASK_MODULES = (dashboard_metrics_tasks, agent_kv_tasks) + logger = WorkerLogger.get_logger(__name__) # Initialize worker configuration diff --git a/workers/shared/api/internal_client.py b/workers/shared/api/internal_client.py index ee9fbca7ac..5a82c61b60 100644 --- a/workers/shared/api/internal_client.py +++ b/workers/shared/api/internal_client.py @@ -1608,6 +1608,115 @@ def get_configuration( error=str(e), ) + # Agent-KV client methods (spec §5.3/§5.4) + def agent_kv_finalize( + self, + job_id: str, + org_id: str, + success: bool, + result: dict[str, Any] | None = None, + error: str = "", + usage_summary: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Terminalize an Agent-KV job via the internal finalize endpoint (Task 11). + + Idempotent server-side: a job already in a terminal state short-circuits + to ``{"finalized": False, ...}`` instead of rewriting its result. + + Args: + job_id: Agent-KV job ID. + org_id: Organization ID (required by the endpoint's body, not the + URL — mirrors ``StageReportView``/``FinalizeView``). + success: Whether the executor run succeeded. Must be a real bool — + the endpoint 400s on a non-bool value. + result: Engine result to persist as the job's output (success only). + error: Failure reason to persist (failure only). + usage_summary: Optional usage/cost summary to persist (success only). + + Returns: + Backend response: ``{"finalized": bool, "webhook_url": str, "status": str}``. + """ + payload: dict[str, Any] = {"org_id": org_id, "success": success} + if success: + payload["result"] = result or {} + if usage_summary is not None: + payload["usage_summary"] = usage_summary + else: + payload["error"] = error + return self.post( + f"v1/agent-kv/jobs/{job_id}/finalize/", + data=payload, + organization_id=org_id, + ) + + def agent_kv_stage_report( + self, + job_id: str, + org_id: str, + stage: str, + status: str, + seconds: float | None = None, + counters: dict[str, Any] | None = None, + ) -> dict[str, Any]: + """Report a pipeline stage for an Agent-KV job via the internal + stage-report endpoint (spec §5.3/§5.4). + + Idempotent server-side, mirroring ``agent_kv_finalize``: a job already + in a terminal state short-circuits to ``{"ok": True, "noop": True}`` + instead of recording the stage. + + Args: + job_id: Agent-KV job ID. + org_id: Organization ID (required by the endpoint's body, not the + URL — mirrors ``StageReportView``/``FinalizeView``). + stage: Pipeline stage name (e.g. ``"extract"``). + status: Stage status (e.g. ``"started"``, ``"completed"``). + seconds: Optional stage duration in seconds. + counters: Optional stage counters to record. + + Returns: + Backend response: ``{"ok": True}`` or ``{"ok": True, "noop": True}``. + """ + payload: dict[str, Any] = {"org_id": org_id, "stage": stage, "status": status} + if seconds is not None: + payload["seconds"] = seconds + if counters: + payload["counters"] = counters + return self.post( + f"v1/agent-kv/jobs/{job_id}/stage/", + data=payload, + organization_id=org_id, + ) + + def agent_kv_sweep(self) -> dict[str, Any]: + """Trigger the Agent-KV maintenance sweep via the internal endpoint + (spec §5.4, Fix 5/Fix 8). + + Platform-wide, like the endpoint itself (``SweepView``): no + ``org_id`` -- it terminalizes never-dispatched ``PENDING`` jobs and + stuck ``DISPATCHED``/``RUNNING`` jobs across every org in one call. + Idempotent and batch-capped server-side, so safe to call on a tight + schedule. + + Returns: + Backend response: ``{"swept": int, "timed_out": int}``. + """ + return self.post("v1/agent-kv/sweep/", data={}) + + def agent_kv_ttl_cleanup(self) -> dict[str, Any]: + """Trigger the Agent-KV TTL cleanup via the internal endpoint + (spec §5.4, Fix 5). + + Platform-wide, like the endpoint itself (``TTLCleanupView``): no + ``org_id`` -- it deletes staged input/result files for jobs past + ``expires_at`` and blanks their refs, across every org in one call. + Idempotent and batch-capped server-side. + + Returns: + Backend response: ``{"cleaned": int}``. + """ + return self.post("v1/agent-kv/ttl-cleanup/", data={}) + # Usage client methods (delegate to UsageAPIClient) def get_aggregated_token_count( self, file_execution_id: str, organization_id: str | None = None diff --git a/workers/shared/enums/worker_enums_base.py b/workers/shared/enums/worker_enums_base.py index 65465659c0..2620b8f5fc 100644 --- a/workers/shared/enums/worker_enums_base.py +++ b/workers/shared/enums/worker_enums_base.py @@ -185,6 +185,10 @@ class QueueName(str, Enum): # IDE callback queue (prompt studio post-execution callbacks) IDE_CALLBACK = "ide_callback" + # Agent-KV terminal-callback queue (agent_kv_complete / agent_kv_error; + # spec §5.3). Consumed by the IDE_CALLBACK worker alongside IDE_CALLBACK. + AGENT_KV_CALLBACK = "agent_kv_callback" + def to_env_var_name(self) -> str: """Convert queue name to environment variable name. diff --git a/workers/shared/infrastructure/config/registry.py b/workers/shared/infrastructure/config/registry.py index bfdfcf2e50..01c39aee73 100644 --- a/workers/shared/infrastructure/config/registry.py +++ b/workers/shared/infrastructure/config/registry.py @@ -70,6 +70,9 @@ class WorkerRegistry: ), WorkerType.IDE_CALLBACK: WorkerQueueConfig( primary_queue=QueueName.IDE_CALLBACK, + # Agent-KV terminal callbacks (agent_kv_complete/agent_kv_error) + # ride the same worker on their own dedicated queue (spec §5.3). + additional_queues=[QueueName.AGENT_KV_CALLBACK], ), } @@ -156,6 +159,8 @@ class WorkerRegistry: TaskRoute("ide_prompt_complete", QueueName.IDE_CALLBACK), TaskRoute("ide_prompt_error", QueueName.IDE_CALLBACK), TaskRoute("ide_callback.tasks.*", QueueName.IDE_CALLBACK), + TaskRoute("agent_kv_complete", QueueName.AGENT_KV_CALLBACK), + TaskRoute("agent_kv_error", QueueName.AGENT_KV_CALLBACK), ], ), } diff --git a/workers/shared/infrastructure/config/tests/__init__.py b/workers/shared/infrastructure/config/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/workers/shared/infrastructure/config/tests/test_registry.py b/workers/shared/infrastructure/config/tests/test_registry.py new file mode 100644 index 0000000000..80a7c63117 --- /dev/null +++ b/workers/shared/infrastructure/config/tests/test_registry.py @@ -0,0 +1,44 @@ +"""Tests for worker registry configuration. + +The registry is where a queue stops being a string in a shell script and +becomes something a worker actually subscribes to. For Agent-KV that matters +more than usual: `agent_kv_callback` carries the terminal callbacks, and a job +whose callback is never consumed runs to SUCCESS in the executor and then sits +in RUNNING forever -- the result is never persisted, the staged input is never +deleted, the concurrency slot is never released and the webhook never fires. +Nothing errors, because nothing failed. +""" + +from shared.enums.worker_enums import WorkerType +from shared.enums.worker_enums_base import QueueName +from shared.infrastructure.config.registry import WorkerRegistry + + +def test_ide_callback_worker_also_subscribes_to_the_agent_kv_callback_queue(): + cfg = WorkerRegistry._QUEUE_CONFIGS[WorkerType.IDE_CALLBACK] + assert QueueName.AGENT_KV_CALLBACK in cfg.additional_queues, ( + "the ide_callback worker does not subscribe to agent_kv_callback; " + "Agent-KV jobs would execute and then never terminalize" + ) + + +def test_both_agent_kv_terminal_callbacks_route_to_that_queue(): + """`dispatch_job` attaches both as Celery link/link_error. A route missing + for either leaves one half of the outcome space unhandled -- most visibly, + a failed job that never moves out of RUNNING. + """ + routing = WorkerRegistry._TASK_ROUTES[WorkerType.IDE_CALLBACK] + routed = {r.pattern: r.queue for r in routing.routes} + + assert routed["agent_kv_complete"] == QueueName.AGENT_KV_CALLBACK + assert routed["agent_kv_error"] == QueueName.AGENT_KV_CALLBACK + + +def test_no_worker_type_claims_the_unshipped_sandbox_queue(): + """This deployment ships no codegen sandbox (UN-4215 is the fast-follow). + + A registry entry for a worker that is never deployed is how a queue ends up + advertised and undrained, so the absence is asserted rather than assumed. + """ + assert not hasattr(QueueName, "SANDBOX_CODEGEN") + assert not hasattr(WorkerType, "SANDBOX") diff --git a/workers/shared/tests/test_agent_kv_client.py b/workers/shared/tests/test_agent_kv_client.py new file mode 100644 index 0000000000..86f02b8f35 --- /dev/null +++ b/workers/shared/tests/test_agent_kv_client.py @@ -0,0 +1,90 @@ +"""Tests for ``InternalAPIClient.agent_kv_stage_report`` (Agent-KV cloud +executor Task 2) and the worker package's dependency on +``unstract-agent-kv-schema``. + +Mirrors ``workers/tests/test_agent_kv_scheduler_tasks.py``'s +``TestInternalClientAgentKvMethods`` pattern for patching the client's HTTP +layer: ``patch.object(InternalAPIClient, "post", ...)`` plus +``InternalAPIClient.__new__(InternalAPIClient)`` to avoid running the real +``__init__`` (session/config setup unrelated to this method). +""" + +from unittest.mock import patch + +from shared.api import InternalAPIClient + + +class TestAgentKvStageReport: + def test_posts_minimal_body_to_the_stage_endpoint(self): + """seconds/counters are omitted from the body when not provided.""" + with patch.object(InternalAPIClient, "post", return_value={"ok": True}) as m_post: + client = InternalAPIClient.__new__(InternalAPIClient) + result = client.agent_kv_stage_report("job-1", "org-1", "extract", "started") + + m_post.assert_called_once_with( + "v1/agent-kv/jobs/job-1/stage/", + data={"org_id": "org-1", "stage": "extract", "status": "started"}, + organization_id="org-1", + ) + assert result == {"ok": True} + + def test_posts_full_body_with_seconds_and_counters(self): + with patch.object(InternalAPIClient, "post", return_value={"ok": True}) as m_post: + client = InternalAPIClient.__new__(InternalAPIClient) + result = client.agent_kv_stage_report( + "job-2", + "org-2", + "extract", + "completed", + seconds=1.5, + counters={"rows": 3}, + ) + + m_post.assert_called_once_with( + "v1/agent-kv/jobs/job-2/stage/", + data={ + "org_id": "org-2", + "stage": "extract", + "status": "completed", + "seconds": 1.5, + "counters": {"rows": 3}, + }, + organization_id="org-2", + ) + assert result == {"ok": True} + + def test_returns_the_parsed_noop_response_for_a_terminal_job(self): + """The backend answers {"ok": true, "noop": true} for a terminal job; + the client returns it unchanged (idempotent, no special-casing). + """ + with patch.object( + InternalAPIClient, "post", return_value={"ok": True, "noop": True} + ): + client = InternalAPIClient.__new__(InternalAPIClient) + result = client.agent_kv_stage_report("job-3", "org-3", "extract", "started") + + assert result == {"ok": True, "noop": True} + + def test_empty_counters_dict_is_omitted_like_none(self): + """``if counters:`` (not ``is not None``) mirrors the brief's + implementation exactly -- an empty dict is falsy and left out. + """ + with patch.object(InternalAPIClient, "post", return_value={"ok": True}) as m_post: + client = InternalAPIClient.__new__(InternalAPIClient) + client.agent_kv_stage_report( + "job-4", "org-4", "extract", "started", counters={} + ) + + m_post.assert_called_once_with( + "v1/agent-kv/jobs/job-4/stage/", + data={"org_id": "org-4", "stage": "extract", "status": "started"}, + organization_id="org-4", + ) + + +class TestAgentKvSchemaImport: + def test_agent_kv_schema_package_importable(self): + """The worker venv depends on unstract-agent-kv-schema (path dependency, + editable) so the schema compiler is importable inside worker code. + """ + import unstract.agent_kv_schema # noqa: F401 diff --git a/workers/shared/tests/test_webhook_notify.py b/workers/shared/tests/test_webhook_notify.py new file mode 100644 index 0000000000..1c0fddfb9b --- /dev/null +++ b/workers/shared/tests/test_webhook_notify.py @@ -0,0 +1,221 @@ +"""Tests for the SSRF-guarded webhook sender (spec §6.7). + +``requests`` is patched at the ``webhook_notify`` import site; DNS is patched +inside ``unstract.core.network.ssrf``, which is where the egress decision now +lives (this module no longer has a resolver of its own -- Greptile review #4 +replaced its hand-rolled ``_host_is_public`` with the shared guard). So no real +network/DNS activity occurs. + +The behaviours asserted here are therefore the SINK's contract -- scheme +policy, the ``allow_insecure`` waiver, status-code handling, and the fact that +an unsafe destination never reaches ``requests.post``. The guard's own +address-classification rules are covered by +``unstract/core/tests/test_ssrf_guard.py``; the cases kept below are the ones +this sink must keep refusing regardless of which implementation is behind it. +""" + +import socket +from unittest import mock + +from shared.utils import webhook_notify as wn + +from unstract.core.network import ssrf + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_private_ip_refused(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("10.0.0.5", 443))] + assert wn.send_webhook("https://internal.example/x", {"a": 1}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_metadata_ip_refused(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("169.254.169.254", 80))] + assert wn.send_webhook("https://md.example/x", {}) is False + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_cgnat_shared_address_space_refused(m_gai, m_requests): + """RFC 6598 100.64.0.0/10. `ipaddress.is_private` does not cover this + range, which is why the old local guard needed a hand-maintained + `_CGNAT_RANGE` constant for it (and why SonarCloud flagged that hardcoded + address). The shared guard gets it from `is_global`, an allowlist kept + against the IANA registries, so the range is covered without naming it. + """ + m_gai.return_value = [(2, 1, 6, "", ("100.64.0.1", 443))] + assert wn.send_webhook("https://cgnat.example/x", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_nat64_embedded_loopback_refused(m_gai, m_requests): + """``64:ff9b::7f00:1`` is the RFC 6052 NAT64 prefix carrying 127.0.0.1 in + its low 32 bits; on a deployment with a NAT64 route, that is what gets + dialled. + + A REGRESSION GUARD, not a fixed bug: the previous local guard refused this + too, via ``ipaddress.is_reserved`` (checked on 3.12.9 -- the claim in + Greptile review #4 that it was delivered to does not hold). It is pinned + here because that refusal was incidental to a version-dependent flag, + whereas the shared guard refuses it deliberately by re-checking the + embedded IPv4. Both implementations pass; only one of them means to. + """ + m_gai.return_value = [(socket.AF_INET6, 1, 6, "", ("64:ff9b::7f00:1", 443, 0, 0))] + assert wn.send_webhook("https://nat64.example/x", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_nat64_embedded_public_ip_still_allowed(m_gai, m_requests): + """The companion to the test above, and the case that actually CHANGES + behaviour. + + ``64:ff9b::5db8:d822`` embeds the public 93.184.216.34 -- a legitimate + destination through a NAT64 translator. The old guard refused it, by the + same blanket ``is_reserved`` that refused the loopback one: it could not + tell the two apart, so it rejected the whole prefix. This test fails + against that implementation and passes against the shared guard, which + judges the embedded address. + """ + m_gai.return_value = [(socket.AF_INET6, 1, 6, "", ("64:ff9b::5db8:d822", 443, 0, 0))] + m_requests.post.return_value.status_code = 200 + assert wn.send_webhook("https://nat64-ok.example/hook", {}) is True + + +@mock.patch.object(wn, "requests") +def test_localhost_subdomain_refused_without_dns(m_requests): + """RFC 6761 reserves ``*.localhost`` to loopback, and the shared guard + decides that WITHOUT a resolver. + + No resolver is patched in this test, which is the point: the old guard had + no such rule and could only reach the same answer through DNS, so its + verdict depended on the image's libc (glibc's nss-myhostname answers for + the whole subtree, musl only for the bare apex, a minimal container for + neither). Same outcome, reached by luck rather than by rule. + """ + assert wn.send_webhook("https://api.localhost/hook", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_normal_public_ip_still_allowed(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 443))] + m_requests.post.return_value.status_code = 200 + assert wn.send_webhook("https://cgnat-sibling.example/x", {}) is True + + +def test_http_scheme_refused_by_default(): + assert wn.send_webhook("http://example.com/x", {}) is False + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_public_host_posted_no_redirects(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 443))] + m_requests.post.return_value.status_code = 200 + assert wn.send_webhook("https://example.com/hook", {"job_id": "j"}) is True + kw = m_requests.post.call_args.kwargs + assert kw["allow_redirects"] is False + assert kw["timeout"] == 10 + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_delivery_error_returns_false_never_raises(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 443))] + m_requests.post.side_effect = Exception("boom") + assert wn.send_webhook("https://example.com/hook", {}) is False + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_ipv6_loopback_refused(m_gai, m_requests): + """Pins the 4-tuple IPv6 sockaddr shape: (addr, port, flowinfo, scope_id).""" + m_gai.return_value = [(socket.AF_INET6, 1, 6, "", ("::1", 443, 0, 0))] + assert wn.send_webhook("https://v6.example/x", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_ipv6_public_posted(m_gai, m_requests): + m_gai.return_value = [(socket.AF_INET6, 1, 6, "", ("2606:4700::1111", 443, 0, 0))] + m_requests.post.return_value.status_code = 200 + assert wn.send_webhook("https://v6.example/hook", {}) is True + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_empty_resolution_refused(m_gai, m_requests): + m_gai.return_value = [] + assert wn.send_webhook("https://nowhere.example/x", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_multi_record_any_unsafe_refuses(m_gai, m_requests): + """One public + one private record for the same host: must refuse (ALL + resolved addresses must be public, not just the first). + """ + m_gai.return_value = [ + (2, 1, 6, "", ("93.184.216.34", 443)), + (2, 1, 6, "", ("10.0.0.5", 443)), + ] + assert wn.send_webhook("https://mixed.example/x", {}) is False + assert not m_requests.post.called + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_server_error_status_returns_false(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 443))] + m_requests.post.return_value.status_code = 500 + assert wn.send_webhook("https://example.com/hook", {}) is False + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_not_found_status_returns_false(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 443))] + m_requests.post.return_value.status_code = 404 + assert wn.send_webhook("https://example.com/hook", {}) is False + + +@mock.patch.object(wn, "requests") +@mock.patch.object(ssrf.socket, "getaddrinfo") +def test_allow_http_true_posts_public_host(m_gai, m_requests): + m_gai.return_value = [(2, 1, 6, "", ("93.184.216.34", 80))] + m_requests.post.return_value.status_code = 200 + assert wn.send_webhook("http://example.com/hook", {}, allow_http=True) is True + assert m_requests.post.called + + +class TestAllowInsecure: + """``allow_insecure`` waives scheme + public-host guards (test stacks only).""" + + @mock.patch.object(wn, "requests") + def test_http_private_host_delivered_when_insecure(self, m_requests): + m_requests.post.return_value = mock.Mock(status_code=200) + ok = wn.send_webhook( + "http://host.docker.internal:18099/hook", + {"job_id": "j", "status": "completed"}, + allow_insecure=True, + ) + assert ok is True + m_requests.post.assert_called_once() + + @mock.patch.object(wn, "requests") + def test_default_still_refuses_http_and_private(self, m_requests): + assert ( + wn.send_webhook("http://host.docker.internal:18099/hook", {"job_id": "j"}) + is False + ) + assert not m_requests.post.called diff --git a/workers/shared/utils/webhook_notify.py b/workers/shared/utils/webhook_notify.py new file mode 100644 index 0000000000..da41233119 --- /dev/null +++ b/workers/shared/utils/webhook_notify.py @@ -0,0 +1,85 @@ +"""Terminal-state webhook delivery for Agent-KV (spec §6.7). + +The egress decision is NOT made here. It belongs to +``unstract.core.network.ssrf``, the single place in the codebase that decides +whether a tenant-supplied URL may be dialled -- the same guard behind the +prompt-postprocessing and pipeline-notification sinks, which exists (per its own +docstring) "so a new sink does not carry its own copy of the rules". + +This module shipped with its own ``_host_is_public``, built from negative +``ipaddress`` flags. Greptile review #4 asked for the shared guard on the +grounds that a NAT64 literal such as ``64:ff9b::7f00:1`` -- the RFC 6052 +prefix carrying 127.0.0.1 in its low 32 bits -- slipped past it. **That +specific claim does not hold**: on the runtime this worker uses (3.12), +``ipaddress`` reports ``is_reserved`` for ``64:ff9b::/96``, so the local guard +already refused it. Verified against 3.12.9 rather than assumed. + +The move is still right, for the reasons the local copy's existence was wrong: + +- It refused in the WRONG DIRECTION too. ``64:ff9b::5db8:d822`` embeds the + public 93.184.216.34 -- a legitimate destination for a deployment with a + NAT64 route -- and the same ``is_reserved`` flag refused that as well. The + shared guard re-checks the embedded IPv4 (``_EMBEDS_IPV4``) and so separates + the two cases instead of rejecting the prefix wholesale. +- Its NAT64 refusal was ACCIDENTAL. ``is_reserved`` is not an SSRF control; it + is a version-dependent table (its IPv6 entries have moved between CPython + releases), and nothing recorded that this guard leaned on it. The shared + guard refuses on ``is_global`` -- an allowlist maintained against the IANA + registries -- plus an explicit embedded-IPv4 re-check. +- Enumerating what to refuse misses whatever belongs to none of the flags, and + this copy had already been patched once for RFC 6598 shared address space for + exactly that reason. The shared guard gets that range from ``is_global`` for + free, which also retires the hardcoded ``100.64.0.0`` SonarCloud flagged. +- It also brings rules this sink never had: ``*.localhost`` decided without a + resolver, credentials-in-URL, and the urlparse/urllib3 host disagreement that + determines which host the socket actually connects to. + +Residual accepted risk, unchanged and shared with every other sink: DNS is +resolved for the check and again by ``requests``, so a rebinding window exists. +The control for that is pod egress policy, not application code. The payload +carries only {job_id, status} and the response body is never read. +""" + +import json +import logging + +import requests + +from unstract.core.network.ssrf import is_safe_webhook_url, safe_host + +logger = logging.getLogger(__name__) + +_TIMEOUT = 10 + + +def send_webhook( + url: str, payload: dict, *, allow_http: bool = False, allow_insecure: bool = False +) -> bool: + """``allow_insecure`` waives BOTH guards (http scheme and non-public host). + + Test/dev stacks only -- it exists so the e2e lane can deliver to a + receiver on the compose host (host.docker.internal is a private + address). Production never sets it; the SSRF guards stay mandatory. + """ + try: + if allow_insecure: + # Scheme and destination both unchecked: this is the e2e escape + # hatch, and the only path on which `requests.post` is reached + # without the shared guard having approved the URL. + pass + elif not is_safe_webhook_url( + url, allowed_schemes=("http", "https") if allow_http else ("https",) + ): + # is_safe_webhook_url already logged the refusal reason and host. + return False + resp = requests.post( + url, + data=json.dumps(payload), + headers={"Content-Type": "application/json"}, + timeout=_TIMEOUT, + allow_redirects=False, + ) + return 200 <= resp.status_code < 300 + except Exception: + logger.warning("webhook delivery failed (host=%s)", safe_host(url), exc_info=True) + return False diff --git a/workers/tests/test_agent_kv_scheduler_tasks.py b/workers/tests/test_agent_kv_scheduler_tasks.py new file mode 100644 index 0000000000..7718a63f77 --- /dev/null +++ b/workers/tests/test_agent_kv_scheduler_tasks.py @@ -0,0 +1,95 @@ +"""Tests for the thin Agent-KV maintenance-periodic proxy tasks (spec §5.4). + +Mirrors ``test_dashboard_metrics_tasks.py``'s style: these tasks do nothing but +call the shared ``InternalAPIClient`` facade, so what's worth pinning is the +registered wire name (a mismatch means the PG consumer drops the message as an +unknown task -- the failure mode with no error at the enqueue site) and which +client method each task calls. +""" + +from __future__ import annotations + +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +# Import via the PACKAGE, matching `scheduler/tasks.py`'s +# `from scheduler import agent_kv_tasks` -- see that file's comment and +# `test_dashboard_metrics_tasks.py`'s identical note on why this form (not a +# bare `import agent_kv_tasks`) is the one both runtime import mechanisms +# converge on. +_WORKERS_ROOT = Path(__file__).resolve().parent.parent +if str(_WORKERS_ROOT) not in sys.path: + sys.path.insert(0, str(_WORKERS_ROOT)) + +from scheduler import agent_kv_tasks as akt # noqa: E402 +from shared.api import InternalAPIClient # noqa: E402 + + +class TestRegistration: + """The wire name is this task pair's own new contract (no pre-existing + Beat/PG-scheduler row to mirror) -- a rename here silently breaks whatever + PgPeriodicTask row an operator registered against the old name. + """ + + @pytest.mark.parametrize( + "name,func", + [ + ("agent_kv.sweep", "agent_kv_sweep"), + ("agent_kv.ttl_cleanup", "agent_kv_ttl_cleanup"), + ], + ) + def test_task_is_registered_under_its_wire_name(self, name, func): + assert getattr(akt, func).name == name + + +class TestApiClientAcquisition: + def test_get_api_client_returns_an_internal_api_client(self): + with patch("shared.api.InternalAPIClient") as mock_cls: + akt._get_api_client() + mock_cls.assert_called_once_with() + + +class TestCallContract: + def test_sweep_calls_the_sweep_client_method(self): + mock_api = MagicMock() + mock_api.agent_kv_sweep.return_value = {"swept": 3, "timed_out": 1} + with patch.object(akt, "_get_api_client", return_value=mock_api): + result = akt.agent_kv_sweep() + mock_api.agent_kv_sweep.assert_called_once_with() + assert result == {"swept": 3, "timed_out": 1} + + def test_ttl_cleanup_calls_the_ttl_cleanup_client_method(self): + mock_api = MagicMock() + mock_api.agent_kv_ttl_cleanup.return_value = {"cleaned": 2} + with patch.object(akt, "_get_api_client", return_value=mock_api): + result = akt.agent_kv_ttl_cleanup() + mock_api.agent_kv_ttl_cleanup.assert_called_once_with() + assert result == {"cleaned": 2} + + +class TestInternalClientAgentKvMethods: + """The client methods (``workers/shared/api/internal_client.py``) the tasks + above call -- pinned separately so a change to either the endpoint path or + the call contract fails at the right layer. + """ + + def test_agent_kv_sweep_posts_to_the_sweep_endpoint(self): + with patch.object( + InternalAPIClient, "post", return_value={"swept": 1, "timed_out": 0} + ) as m_post: + client = InternalAPIClient.__new__(InternalAPIClient) + result = client.agent_kv_sweep() + m_post.assert_called_once_with("v1/agent-kv/sweep/", data={}) + assert result == {"swept": 1, "timed_out": 0} + + def test_agent_kv_ttl_cleanup_posts_to_the_ttl_cleanup_endpoint(self): + with patch.object( + InternalAPIClient, "post", return_value={"cleaned": 1} + ) as m_post: + client = InternalAPIClient.__new__(InternalAPIClient) + result = client.agent_kv_ttl_cleanup() + m_post.assert_called_once_with("v1/agent-kv/ttl-cleanup/", data={}) + assert result == {"cleaned": 1} diff --git a/workers/tests/test_queue_consumer_wiring.py b/workers/tests/test_queue_consumer_wiring.py new file mode 100644 index 0000000000..0c496bdd32 --- /dev/null +++ b/workers/tests/test_queue_consumer_wiring.py @@ -0,0 +1,190 @@ +"""Guard: every queue we dispatch to must have a consumer in the deployed fleet. + +This is the OSS analogue of the cloud chart's ``validate-pg-worker-fleet.yaml``. +It exists because the failure it catches is **silent**: since UN-4046 made the +PG transport unconditional, a queue whose consumer is not configured still +*accepts* work -- ``enqueue_task`` succeeds, rows land in ``pg_queue_message``, +and nothing errors at the producer. The job simply sits in DISPATCHED forever. + +That is exactly what happened to ``celery_executor_agentic_kv``: the Agent-KV +branch was written when the Celery executor was live, so its queue was wired +into ``CELERY_QUEUES_EXECUTOR`` -- a variable read only by +``workers/executor/worker.py`` (the now-disabled Celery worker). The live +``pg-queue-consumer`` reads ``WORKER_PG_QUEUE_CONSUMER_QUEUE``, which did not +list it. + +Each test therefore asserts the queue is present in the variable the *running* +consumer actually reads, at every site that configures one. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +import pytest + +yaml = pytest.importorskip("yaml") + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEV_COMPOSE = REPO_ROOT / "docker" / "docker-compose.yaml" +TEST_COMPOSE = REPO_ROOT / "tests" / "compose" / "docker-compose.test.yaml" +RUN_WORKER = REPO_ROOT / "workers" / "run-worker.sh" + +# The queue the `kv` extractor WOULD dispatch onto, and which this deployment +# deliberately does not serve: the `agentic_kv` plugin ships on a separate PR. +# `kv` is absent from EXTRACTOR_ROUTES so a submit is refused at the serializer +# with a 400 -- these assertions are the second half of that, pinning that no +# fleet advertises a consumer for it. A queue that is listed but unserved +# accepts work and drains nothing, with no error at the producer: the job sits +# in DISPATCHED forever. That failure has already cost this team ~30 hours of +# firings with zero executions, which is why it is guarded from both ends. +UNSERVED_KV_QUEUE = "celery_executor_agentic_kv" + +# The terminal-callback queue. Its consumer is what moves a job out of +# RUNNING: finalize persists the result, deletes the staged input, releases +# the concurrency slot and fires the webhook. +AGENT_KV_CALLBACK_QUEUE = "agent_kv_callback" + +# The queue the backend dispatches Agent-KV TABLE extraction onto. Same queue +# the IDE table path already uses -- ExecutionContext(executor_name= +# "agentic_table") -> celery_executor_agentic_table -- which is exactly why the +# API path reuses that executor name rather than introducing one of its own. +# It was already wired for the IDE; these assertions keep it wired now that a +# second, paid entry point depends on it. +AGENT_TABLE_QUEUE = "celery_executor_agentic_table" + +# The env var the live PG consumer reads. Named here so a future rename has to +# touch this constant rather than silently bypassing every assertion below. +PG_QUEUE_VAR = "WORKER_PG_QUEUE_CONSUMER_QUEUE" + +# Read only by the disabled Celery executor. Setting a queue here does NOT give +# it a consumer -- asserting its absence is what makes this suite catch the +# original bug rather than a cosmetic rename of it. +DEAD_CELERY_VAR = "CELERY_QUEUES_EXECUTOR" + + +def _service_env(compose_path: Path, service: str) -> dict[str, str]: + """Return ``service``'s environment as a dict, from a compose file.""" + doc = yaml.safe_load(compose_path.read_text()) + env = (doc.get("services", {}).get(service) or {}).get("environment") or [] + if isinstance(env, dict): # mapping form + return {k: str(v) for k, v in env.items()} + out: dict[str, str] = {} + for item in env: # list form: "KEY=value" + key, _, value = str(item).partition("=") + out[key] = value + return out + + +def _queues(raw: str) -> set[str]: + """Queue names from a consumer queue list, ignoring any ``${VAR:-default}`` + wrapper the compose file uses to keep the value overridable. + """ + inner = re.sub(r"^\$\{[^:}]+:-(.*)\}$", r"\1", raw.strip()) + return {q.strip() for q in inner.split(",") if q.strip()} + + +@pytest.mark.parametrize( + ("compose_path", "label"), + [(DEV_COMPOSE, "dev compose"), (TEST_COMPOSE, "e2e test compose")], +) +def test_no_fleet_advertises_the_unserved_kv_queue(compose_path, label): + """The executor fleet must not list a queue with no plugin behind it.""" + env = _service_env(compose_path, "worker-pg-executor") + raw = env.get(PG_QUEUE_VAR) + assert raw is not None, ( + f"{label}: worker-pg-executor sets no {PG_QUEUE_VAR}; the running " + f"pg-queue-consumer would fall back to a default of unknown content" + ) + assert UNSERVED_KV_QUEUE not in _queues(raw), ( + f"{label}: {UNSERVED_KV_QUEUE} is advertised but this deployment " + f"carries no agentic_kv plugin to drain it. Work routed there is " + f"accepted and never runs, silently. Remove it from {PG_QUEUE_VAR}, " + f"or ship the plugin." + ) + + +def test_test_compose_does_not_wire_the_queue_onto_the_dead_celery_var(): + """Regression: the e2e override once set the Celery-only variable. + + That drained nothing on the PG transport. If someone re-adds it, the queue + looks configured while the lane still hangs -- so fail loudly. + """ + env = _service_env(TEST_COMPOSE, "worker-pg-executor") + assert DEAD_CELERY_VAR not in env, ( + f"{DEAD_CELERY_VAR} is read only by the disabled Celery executor " + f"(workers/executor/worker.py). Queues belong in {PG_QUEUE_VAR}." + ) + + +def test_run_worker_pg_executor_role_omits_the_unserved_kv_queue(): + """The host-run fleet's hardcoded role default must match the compose stacks. + + ``run-worker.sh`` carries its own PG_CONSUMER_ROLES map; a queue listed + here but not in compose is just as unconsumed for anyone running the fleet + directly. + """ + text = RUN_WORKER.read_text() + match = re.search(r'\["\$PG_ROLE_EXECUTOR"\]="executor;([^"]+)"', text) + assert match, "could not find the PG_ROLE_EXECUTOR entry in run-worker.sh" + assert UNSERVED_KV_QUEUE not in _queues(match.group(1)), ( + f"run-worker.sh's pg-executor role advertises {UNSERVED_KV_QUEUE}; a " + f"host-run fleet would accept Agent-KV work and never drain it." + ) + + +@pytest.mark.parametrize( + ("compose_path", "label"), + [(DEV_COMPOSE, "dev compose"), (TEST_COMPOSE, "e2e test compose")], +) +def test_pg_executor_consumes_the_agent_table_queue(compose_path, label): + """Agent-KV table jobs ride the IDE table executor's queue (spec R1).""" + env = _service_env(compose_path, "worker-pg-executor") + raw = env.get(PG_QUEUE_VAR) + assert raw is not None, ( + f"{label}: worker-pg-executor sets no {PG_QUEUE_VAR}; the running " + f"pg-queue-consumer would fall back to a default that omits " + f"{AGENT_TABLE_QUEUE}" + ) + assert AGENT_TABLE_QUEUE in _queues(raw), ( + f"{label}: {AGENT_TABLE_QUEUE} has no consumer -- both IDE table " + f"prompts and Agent-KV table jobs will sit in DISPATCHED forever with " + f"no error at the producer. Add it to {PG_QUEUE_VAR}." + ) + + +def test_run_worker_pg_executor_role_lists_the_agent_table_queue(): + text = RUN_WORKER.read_text() + match = re.search(r'\["\$PG_ROLE_EXECUTOR"\]="executor;([^"]+)"', text) + assert match, "could not find the PG_ROLE_EXECUTOR entry in run-worker.sh" + assert AGENT_TABLE_QUEUE in _queues(match.group(1)), ( + f"run-worker.sh's pg-executor role omits {AGENT_TABLE_QUEUE}; a " + f"host-run fleet would accept table work and never drain it." + ) + + +def test_pg_ide_callback_drains_the_agent_kv_callback_queue(): + """Without this the job runs to SUCCESS and then never completes. + + The executor enqueues its terminal continuation onto `agent_kv_callback`; + if no consumer drains it the row sits in pg_queue_message and the job stays + RUNNING forever, with nothing logged at the producer. The Celery + ide_callback worker has carried both queues since the callbacks landed -- + its PG twin did not, and the cloud chart wires it correctly, so only the + OSS stacks were affected. Found by running a real job end to end. + """ + env = _service_env(DEV_COMPOSE, "worker-pg-ide-callback") + raw = env.get(PG_QUEUE_VAR) + assert raw is not None, "worker-pg-ide-callback sets no consumer queue" + assert AGENT_KV_CALLBACK_QUEUE in _queues(raw), ( + f"{AGENT_KV_CALLBACK_QUEUE} has no consumer -- Agent-KV jobs will " + f"finish executing and never leave RUNNING." + ) + + +def test_run_worker_pg_ide_callback_role_drains_the_callback_queue(): + text = RUN_WORKER.read_text() + match = re.search(r'\["\$PG_ROLE_IDE_CALLBACK"\]="ide_callback;([^"]+)"', text) + assert match, "could not find the PG_ROLE_IDE_CALLBACK entry in run-worker.sh" + assert AGENT_KV_CALLBACK_QUEUE in _queues(match.group(1)) diff --git a/workers/tests/test_worker_task_module_imports.py b/workers/tests/test_worker_task_module_imports.py index df54943500..021fd8e0e1 100644 --- a/workers/tests/test_worker_task_module_imports.py +++ b/workers/tests/test_worker_task_module_imports.py @@ -27,6 +27,7 @@ from __future__ import annotations import json +import os import subprocess import sys from pathlib import Path @@ -81,8 +82,11 @@ def _run_probe(source: str, workers: list[str] | None = None) -> dict[str, str]: proc = subprocess.run( - [sys.executable, "-c", - source.format(root=str(_WORKERS_ROOT), workers=workers or _TASK_MODULES)], + [ + sys.executable, + "-c", + source.format(root=str(_WORKERS_ROOT), workers=workers or _TASK_MODULES), + ], capture_output=True, text=True, cwd=str(_WORKERS_ROOT), @@ -90,7 +94,9 @@ def _run_probe(source: str, workers: list[str] | None = None) -> dict[str, str]: ) marker = [ln for ln in proc.stdout.splitlines() if ln.startswith("RESULT")] if not marker: - pytest.fail(f"probe did not complete.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}") + pytest.fail( + f"probe did not complete.\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}" + ) return json.loads(marker[-1][len("RESULT") :]) @@ -122,35 +128,55 @@ def test_every_tasks_module_imports_by_file_path() -> None: ) -def test_scheduler_registers_the_metrics_proxies_under_their_wire_names() -> None: - """The broken import is a SIDE-EFFECT import — it must actually register. - - Asserting only that scheduler.tasks imports would still pass if someone 'fixed' it - by deleting the import, silently unregistering the three dashboard-metrics proxies - that the PG metrics consumer resolves BY NAME. +def test_scheduler_tasks_registers_every_periodic_under_its_wire_name() -> None: + """Loading ``scheduler/tasks.py`` must REGISTER the periodic proxies, not + merely import without error. + + This replaces a test that stated this exact intent -- "asserting only that + scheduler.tasks imports would still pass if someone 'fixed' it by deleting + the import" -- and then imported ``scheduler.dashboard_metrics_tasks`` + DIRECTLY, which registers the tasks by itself. So it passed no matter what + ``scheduler/tasks.py`` contained, and it passed through the very regression + it was written to catch: in a79e9d69 pre-commit.ci's auto-fix deleted BOTH + side-effect imports from scheduler/tasks.py (ruff's isort merged the two + `from scheduler import ...` statements, which relocated each `# noqa` onto a + member line where it no longer suppresses the statement-level F401, and + pycln -- `all = true` -- then removed both). Nothing went red. + + The failure mode is silent: the worker boots fine and only the scheduled + messages are rejected as unknown tasks, so dashboard metrics stop updating + and Agent-KV jobs stop being swept with no error anywhere near the cause. + Hence going through ``scheduler.tasks`` -- what a booting worker actually + loads -- and asserting against the Celery registry rather than the modules. """ source = """ import importlib, json, sys sys.path.insert(0, {root!r}) -m = importlib.import_module("scheduler.dashboard_metrics_tasks") -print("RESULT" + json.dumps({{ - "aggregate": m.dashboard_metrics_aggregate.name, - "hourly": m.dashboard_metrics_cleanup_hourly.name, - "daily": m.dashboard_metrics_cleanup_daily.name, -}})) +importlib.import_module("scheduler.tasks") +from celery import current_app +print("RESULT" + json.dumps(sorted( + n for n in current_app.tasks + if n.startswith("dashboard_metrics.") or n.startswith("agent_kv.") +))) """ proc = subprocess.run( [sys.executable, "-c", source.format(root=str(_WORKERS_ROOT))], capture_output=True, text=True, cwd=str(_WORKERS_ROOT), + # Resolved at import with no default; unset, scheduler.tasks raises + # before any registration and this would fail for the wrong reason. + env={**os.environ, "INTERNAL_API_BASE_URL": "http://localhost:8000"}, timeout=300, ) marker = [ln for ln in proc.stdout.splitlines() if ln.startswith("RESULT")] assert marker, f"probe failed:\n{proc.stdout}\n{proc.stderr}" - names = json.loads(marker[-1][len("RESULT") :]) - assert names == { - "aggregate": "dashboard_metrics.aggregate_from_sources", - "hourly": "dashboard_metrics.cleanup_hourly_data", - "daily": "dashboard_metrics.cleanup_daily_data", - } + assert json.loads(marker[-1][len("RESULT") :]) == [ + # Pinned to verbatim Beat/PG-scheduler rows -- the consumer resolves + # these BY NAME, so a rename here is a silent un-registration too. + "agent_kv.sweep", + "agent_kv.ttl_cleanup", + "dashboard_metrics.aggregate_from_sources", + "dashboard_metrics.cleanup_daily_data", + "dashboard_metrics.cleanup_hourly_data", + ] diff --git a/workers/uv.lock b/workers/uv.lock index d80d0c223f..ec842d77db 100644 --- a/workers/uv.lock +++ b/workers/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.14'", @@ -19,9 +19,9 @@ dependencies = [ { name = "azure-storage-blob" }, { name = "fsspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/1e/6d5146676044247af566fa5843b335b1a647e6446070cec9c8b61c31b369/adlfs-2024.7.0.tar.gz", hash = "sha256:106995b91f0eb5e775bcd5957d180d9a14faef3271a063b1f65c66fd5ab05ddf", size = 48588 } +sdist = { url = "https://files.pythonhosted.org/packages/b4/1e/6d5146676044247af566fa5843b335b1a647e6446070cec9c8b61c31b369/adlfs-2024.7.0.tar.gz", hash = "sha256:106995b91f0eb5e775bcd5957d180d9a14faef3271a063b1f65c66fd5ab05ddf", size = 48588, upload-time = "2024-07-22T12:10:33.849Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/51/a71c457bd0bc8af3e522b6999ff300852c7c446e384fd9904b0794f875df/adlfs-2024.7.0-py3-none-any.whl", hash = "sha256:2005c8e124fda3948f2a6abb2dbebb2c936d2d821acaca6afd61932edfa9bc07", size = 41349 }, + { url = "https://files.pythonhosted.org/packages/6f/51/a71c457bd0bc8af3e522b6999ff300852c7c446e384fd9904b0794f875df/adlfs-2024.7.0-py3-none-any.whl", hash = "sha256:2005c8e124fda3948f2a6abb2dbebb2c936d2d821acaca6afd61932edfa9bc07", size = 41349, upload-time = "2024-07-22T12:10:32.226Z" }, ] [[package]] @@ -34,9 +34,9 @@ dependencies = [ { name = "botocore" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/17/2f6305cc52976dea8156b56badc3602f162f86693a6cc8badc20d2c5cfe6/aiobotocore-2.13.3.tar.gz", hash = "sha256:ac5620f93cc3e7c2aef7c67ba2bb74035ff8d49ee2325821daed13b3dd82a473", size = 106736 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/17/2f6305cc52976dea8156b56badc3602f162f86693a6cc8badc20d2c5cfe6/aiobotocore-2.13.3.tar.gz", hash = "sha256:ac5620f93cc3e7c2aef7c67ba2bb74035ff8d49ee2325821daed13b3dd82a473", size = 106736, upload-time = "2024-08-22T20:42:13.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/98/1c1a1ba5d74f87cbabbf207d36a98551b082b8b187dc35f7808b43f4cf1a/aiobotocore-2.13.3-py3-none-any.whl", hash = "sha256:1272f765fd9414e1a68f8add71978367db94e17e36c3bf629cf1153eb5141fb9", size = 77194 }, + { url = "https://files.pythonhosted.org/packages/81/98/1c1a1ba5d74f87cbabbf207d36a98551b082b8b187dc35f7808b43f4cf1a/aiobotocore-2.13.3-py3-none-any.whl", hash = "sha256:1272f765fd9414e1a68f8add71978367db94e17e36c3bf629cf1153eb5141fb9", size = 77194, upload-time = "2024-08-22T20:42:09.784Z" }, ] [package.optional-dependencies] @@ -48,9 +48,9 @@ boto3 = [ name = "aiohappyeyeballs" version = "2.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, ] [[package]] @@ -67,99 +67,99 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690 }, - { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484 }, - { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949 }, - { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282 }, - { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511 }, - { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680 }, - { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646 }, - { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122 }, - { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127 }, - { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210 }, - { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848 }, - { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102 }, - { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205 }, - { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219 }, - { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629 }, - { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481 }, - { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845 }, - { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050 }, - { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269 }, - { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166 }, - { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263 }, - { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299 }, - { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235 }, - { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883 }, - { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473 }, - { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190 }, - { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478 }, - { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092 }, - { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546 }, - { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250 }, - { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289 }, - { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706 }, - { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652 }, - { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239 }, - { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161 }, - { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759 }, - { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025 }, - { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477 }, - { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069 }, - { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518 }, - { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676 }, - { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067 }, - { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774 }, - { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134 }, - { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201 }, - { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766 }, - { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557 }, - { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168 }, - { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957 }, - { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149 }, - { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685 }, - { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911 }, - { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929 }, - { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112 }, - { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500 }, - { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940 }, - { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413 }, - { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748 }, - { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564 }, - { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080 }, - { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813 }, - { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872 }, - { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030 }, - { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669 }, - { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403 }, - { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413 }, - { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135 }, - { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742 }, - { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371 }, - { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623 }, - { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515 }, - { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906 }, - { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849 }, - { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496 }, - { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746 }, - { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810 }, - { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895 }, - { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833 }, - { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251 }, - { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180 }, - { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528 }, - { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280 }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] name = "aioitertools" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322 } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182 }, + { url = "https://files.pythonhosted.org/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" }, ] [[package]] @@ -170,18 +170,18 @@ dependencies = [ { name = "frozenlist" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] name = "aiosqlite" version = "0.22.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821 } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405 }, + { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] [[package]] @@ -191,27 +191,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/fc/ec94a357dfc6683d8c86f8b4cfa5416a4c36b28052ec8260c77aca96a443/amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432", size = 129013 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/99/fc813cd978842c26c82534010ea849eee9ab3a13ea2b74e95cb9c99e747b/amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2", size = 50944 }, + { 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" }, ] [[package]] name = "annotated-doc" version = "0.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] name = "annotated-types" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] [[package]] @@ -222,76 +222,76 @@ dependencies = [ { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] name = "appdirs" version = "1.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 }, + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, ] [[package]] name = "asn1crypto" version = "1.5.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 } +sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080, upload-time = "2022-03-15T14:46:52.889Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 }, + { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045, upload-time = "2022-03-15T14:46:51.055Z" }, ] [[package]] name = "asyncpg" version = "0.31.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042 }, - { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504 }, - { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241 }, - { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321 }, - { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685 }, - { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858 }, - { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852 }, - { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175 }, - { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111 }, - { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928 }, - { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067 }, - { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156 }, - { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636 }, - { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079 }, - { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606 }, - { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569 }, - { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867 }, - { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349 }, - { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428 }, - { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678 }, - { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505 }, - { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744 }, - { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251 }, - { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901 }, - { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280 }, - { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931 }, - { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608 }, - { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738 }, - { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026 }, - { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426 }, - { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495 }, - { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062 }, +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, ] [[package]] name = "attrs" version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] @@ -301,9 +301,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/30/6691fdc63b35f54a5a65e04fa1e59d827f4d4e8f4a39678ba7d3088ce0c8/authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd", size = 165368 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/30/6691fdc63b35f54a5a65e04fa1e59d827f4d4e8f4a39678ba7d3088ce0c8/authlib-1.6.12.tar.gz", hash = "sha256:0656d8482f28fc8221929d5f35b2bde5d13e10555ebc06b4561b0d622e83b1bd", size = 165368, upload-time = "2026-05-04T08:11:31.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/51/9b0b5cd4cf683a02db937a6f9bbebcdc9c56558a7bb3763ce7d3512103c3/authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab", size = 244473 }, + { url = "https://files.pythonhosted.org/packages/cd/51/9b0b5cd4cf683a02db937a6f9bbebcdc9c56558a7bb3763ce7d3512103c3/authlib-1.6.12-py2.py3-none-any.whl", hash = "sha256:e9229ad7fde610b139dd12f5edbe97eab9ee78bfb85691247e767727850b99ab", size = 244473, upload-time = "2026-05-04T08:11:30.354Z" }, ] [[package]] @@ -314,9 +314,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/fe/5c7710bc611a4070d06ba801de9a935cc87c3d4b689c644958047bdf2cba/azure_core-1.38.2.tar.gz", hash = "sha256:67562857cb979217e48dc60980243b61ea115b77326fa93d83b729e7ff0482e7", size = 363734 } +sdist = { url = "https://files.pythonhosted.org/packages/00/fe/5c7710bc611a4070d06ba801de9a935cc87c3d4b689c644958047bdf2cba/azure_core-1.38.2.tar.gz", hash = "sha256:67562857cb979217e48dc60980243b61ea115b77326fa93d83b729e7ff0482e7", size = 363734, upload-time = "2026-02-18T19:33:05.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/23/6371a551800d3812d6019cd813acd985f9fac0fedc1290129211a73da4ae/azure_core-1.38.2-py3-none-any.whl", hash = "sha256:074806c75cf239ea284a33a66827695ef7aeddac0b4e19dda266a93e4665ead9", size = 217957 }, + { url = "https://files.pythonhosted.org/packages/42/23/6371a551800d3812d6019cd813acd985f9fac0fedc1290129211a73da4ae/azure_core-1.38.2-py3-none-any.whl", hash = "sha256:074806c75cf239ea284a33a66827695ef7aeddac0b4e19dda266a93e4665ead9", size = 217957, upload-time = "2026-02-18T19:33:07.696Z" }, ] [[package]] @@ -328,9 +328,9 @@ dependencies = [ { name = "msal" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/ff/61369d06422b5ac48067215ff404841342651b14a89b46c8d8e1507c8f17/azure-datalake-store-0.0.53.tar.gz", hash = "sha256:05b6de62ee3f2a0a6e6941e6933b792b800c3e7f6ffce2fc324bc19875757393", size = 71430 } +sdist = { url = "https://files.pythonhosted.org/packages/22/ff/61369d06422b5ac48067215ff404841342651b14a89b46c8d8e1507c8f17/azure-datalake-store-0.0.53.tar.gz", hash = "sha256:05b6de62ee3f2a0a6e6941e6933b792b800c3e7f6ffce2fc324bc19875757393", size = 71430, upload-time = "2023-05-10T21:17:05.665Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/2a/75f56b14f115189155cf12e46b366ad1fe3357af5a1a7c09f7446662d617/azure_datalake_store-0.0.53-py2.py3-none-any.whl", hash = "sha256:a30c902a6e360aa47d7f69f086b426729784e71c536f330b691647a51dc42b2b", size = 55308 }, + { url = "https://files.pythonhosted.org/packages/88/2a/75f56b14f115189155cf12e46b366ad1fe3357af5a1a7c09f7446662d617/azure_datalake_store-0.0.53-py2.py3-none-any.whl", hash = "sha256:a30c902a6e360aa47d7f69f086b426729784e71c536f330b691647a51dc42b2b", size = 55308, upload-time = "2023-05-10T21:17:02.629Z" }, ] [[package]] @@ -344,9 +344,9 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/3a/439a32a5e23e45f6a91f0405949dc66cfe6834aba15a430aebfc063a81e7/azure_identity-1.25.2.tar.gz", hash = "sha256:030dbaa720266c796221c6cdbd1999b408c079032c919fef725fcc348a540fe9", size = 284709 } +sdist = { url = "https://files.pythonhosted.org/packages/c2/3a/439a32a5e23e45f6a91f0405949dc66cfe6834aba15a430aebfc063a81e7/azure_identity-1.25.2.tar.gz", hash = "sha256:030dbaa720266c796221c6cdbd1999b408c079032c919fef725fcc348a540fe9", size = 284709, upload-time = "2026-02-11T01:55:42.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/77/f658c76f9e9a52c784bd836aaca6fd5b9aae176f1f53273e758a2bcda695/azure_identity-1.25.2-py3-none-any.whl", hash = "sha256:1b40060553d01a72ba0d708b9a46d0f61f56312e215d8896d836653ffdc6753d", size = 191423 }, + { url = "https://files.pythonhosted.org/packages/9b/77/f658c76f9e9a52c784bd836aaca6fd5b9aae176f1f53273e758a2bcda695/azure_identity-1.25.2-py3-none-any.whl", hash = "sha256:1b40060553d01a72ba0d708b9a46d0f61f56312e215d8896d836653ffdc6753d", size = 191423, upload-time = "2026-02-11T01:55:44.245Z" }, ] [[package]] @@ -359,18 +359,18 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225 } +sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225, upload-time = "2026-01-06T23:48:57.282Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499 }, + { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499, upload-time = "2026-01-06T23:48:58.995Z" }, ] [[package]] name = "backoff" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] [[package]] @@ -385,75 +385,75 @@ dependencies = [ { name = "platformdirs" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bd/51/08fb68d23f4b0f6256fe85dc86e9576941550f890b079352fba719e07b39/banks-2.4.2.tar.gz", hash = "sha256:cda6013bd377ea7b701933578bfb9370fc21ad70bc13cedfc3f5cb2c034ca3dc", size = 188633 } +sdist = { url = "https://files.pythonhosted.org/packages/bd/51/08fb68d23f4b0f6256fe85dc86e9576941550f890b079352fba719e07b39/banks-2.4.2.tar.gz", hash = "sha256:cda6013bd377ea7b701933578bfb9370fc21ad70bc13cedfc3f5cb2c034ca3dc", size = 188633, upload-time = "2026-04-27T12:15:22.021Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b6/8dc5477681b782e2f99de703e7a99828883364b9e03a60d3e2c47053d56a/banks-2.4.2-py3-none-any.whl", hash = "sha256:5fe407cc48c101f3e13d1cf732b83b8246003337612f13c0705d2e81f6faffb7", size = 35050 }, + { url = "https://files.pythonhosted.org/packages/00/b6/8dc5477681b782e2f99de703e7a99828883364b9e03a60d3e2c47053d56a/banks-2.4.2-py3-none-any.whl", hash = "sha256:5fe407cc48c101f3e13d1cf732b83b8246003337612f13c0705d2e81f6faffb7", size = 35050, upload-time = "2026-04-27T12:15:20.785Z" }, ] [[package]] name = "bcrypt" version = "5.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806 }, - { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626 }, - { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853 }, - { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793 }, - { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930 }, - { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194 }, - { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381 }, - { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750 }, - { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757 }, - { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740 }, - { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197 }, - { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974 }, - { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498 }, - { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853 }, - { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626 }, - { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862 }, - { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544 }, - { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787 }, - { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753 }, - { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587 }, - { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178 }, - { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295 }, - { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700 }, - { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034 }, - { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766 }, - { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449 }, - { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310 }, - { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761 }, - { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553 }, - { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009 }, - { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029 }, - { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907 }, - { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500 }, - { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412 }, - { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486 }, - { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940 }, - { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776 }, - { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922 }, - { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367 }, - { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187 }, - { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752 }, - { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881 }, - { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931 }, - { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313 }, - { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290 }, - { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253 }, - { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084 }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185 }, - { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656 }, - { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662 }, - { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240 }, - { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152 }, - { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284 }, - { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643 }, - { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698 }, - { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725 }, - { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912 }, - { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953 }, +sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, + { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, + { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, + { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, + { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, + { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, + { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, + { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, + { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, + { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, + { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, + { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, + { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, + { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, + { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, + { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, + { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, ] [[package]] @@ -464,27 +464,27 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721 }, + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, ] [[package]] name = "bidict" version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, + { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, ] [[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 } +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 }, + { 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]] @@ -499,24 +499,24 @@ dependencies = [ { name = "platformdirs" }, { name = "pytokens" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920 }, - { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499 }, - { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994 }, - { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867 }, - { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124 }, - { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034 }, - { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503 }, - { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557 }, - { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766 }, - { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140 }, - { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234 }, - { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522 }, - { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824 }, - { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855 }, - { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109 }, - { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542 }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -528,9 +528,9 @@ dependencies = [ { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/c1/f80cfbe564c89cdb080cd9ac2079ce05a2fac869bf8fbc45929ed3190da9/boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f", size = 108585 } +sdist = { url = "https://files.pythonhosted.org/packages/74/c1/f80cfbe564c89cdb080cd9ac2079ce05a2fac869bf8fbc45929ed3190da9/boto3-1.34.162.tar.gz", hash = "sha256:873f8f5d2f6f85f1018cbb0535b03cceddc7b655b61f66a0a56995238804f41f", size = 108585, upload-time = "2024-08-15T19:25:38.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/41/faa5081761be3bac3999f912996c14c4dc9d06eab86c234bd6441f54bd64/boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590", size = 139174 }, + { url = "https://files.pythonhosted.org/packages/65/41/faa5081761be3bac3999f912996c14c4dc9d06eab86c234bd6441f54bd64/boto3-1.34.162-py3-none-any.whl", hash = "sha256:d6f6096bdab35a0c0deff469563b87d184a28df7689790f7fe7be98502b7c590", size = 139174, upload-time = "2024-08-15T19:25:35.384Z" }, ] [[package]] @@ -542,9 +542,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/de/17d672eac6725da49bd5832e3bd2f74c4d212311cd393fd56b59f51a4e86/botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3", size = 12676693 } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/17d672eac6725da49bd5832e3bd2f74c4d212311cd393fd56b59f51a4e86/botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3", size = 12676693, upload-time = "2024-08-15T19:25:25.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/47/e35f788047c91110f48703a6254e5c84e33111b3291f7b57a653ca00accf/botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be", size = 12468049 }, + { url = "https://files.pythonhosted.org/packages/bc/47/e35f788047c91110f48703a6254e5c84e33111b3291f7b57a653ca00accf/botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be", size = 12468049, upload-time = "2024-08-15T19:25:18.301Z" }, ] [[package]] @@ -555,9 +555,9 @@ dependencies = [ { name = "boxsdk", extra = ["jwt"] }, { name = "fsspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/de/1c5e0faec600538f6a1d41c7ce7834cacddb2237923e30ddb225254b74b9/boxfs-0.2.1.tar.gz", hash = "sha256:c1889e12f53be3216b44f088237ac0f367a7a759a53b01b0c0edf2b3d694e50f", size = 9523 } +sdist = { url = "https://files.pythonhosted.org/packages/be/de/1c5e0faec600538f6a1d41c7ce7834cacddb2237923e30ddb225254b74b9/boxfs-0.2.1.tar.gz", hash = "sha256:c1889e12f53be3216b44f088237ac0f367a7a759a53b01b0c0edf2b3d694e50f", size = 9523, upload-time = "2023-08-23T19:24:35.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/bb/243d10169c8397051bad6bdd10beb2407fa490bfe01216f5fad09e066191/boxfs-0.2.1-py3-none-any.whl", hash = "sha256:ae796c30309bd5a02654fff9eddf1ed320356225568fad0e109e1942beaef72a", size = 9358 }, + { url = "https://files.pythonhosted.org/packages/86/bb/243d10169c8397051bad6bdd10beb2407fa490bfe01216f5fad09e066191/boxfs-0.2.1-py3-none-any.whl", hash = "sha256:ae796c30309bd5a02654fff9eddf1ed320356225568fad0e109e1942beaef72a", size = 9358, upload-time = "2023-08-23T19:24:34.066Z" }, ] [[package]] @@ -571,9 +571,9 @@ dependencies = [ { name = "requests-toolbelt" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bf/d7/c1a95bb602d7f90a85a68d8e6f11954e50c255110d39e2167c7796252622/boxsdk-3.14.0.tar.gz", hash = "sha256:7918b1929368724662474fffa417fa0457a523d089b8185260efbedd28c4f9b1", size = 232630 } +sdist = { url = "https://files.pythonhosted.org/packages/bf/d7/c1a95bb602d7f90a85a68d8e6f11954e50c255110d39e2167c7796252622/boxsdk-3.14.0.tar.gz", hash = "sha256:7918b1929368724662474fffa417fa0457a523d089b8185260efbedd28c4f9b1", size = 232630, upload-time = "2025-04-09T15:07:15.181Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/5d/4e15511e0f4f2f9fbbf4646a8d0e138e5c53a3d428f1724e7dc3c8acf556/boxsdk-3.14.0-py2.py3-none-any.whl", hash = "sha256:0314e2f172b050e98489955f2e9001263de79c3dd751e6feee19f2195fdf7c01", size = 141329 }, + { url = "https://files.pythonhosted.org/packages/4d/5d/4e15511e0f4f2f9fbbf4646a8d0e138e5c53a3d428f1724e7dc3c8acf556/boxsdk-3.14.0-py2.py3-none-any.whl", hash = "sha256:0314e2f172b050e98489955f2e9001263de79c3dd751e6feee19f2195fdf7c01", size = 141329, upload-time = "2025-04-09T15:07:13.295Z" }, ] [package.optional-dependencies] @@ -596,18 +596,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/7d/6c289f407d219ba36d8b384b42489ebdd0c84ce9c413875a8aae0c85f35b/celery-5.5.3.tar.gz", hash = "sha256:6c972ae7968c2b5281227f01c3a3f984037d21c5129d07bf3550cc2afc6b10a5", size = 1667144, upload-time = "2025-06-01T11:08:12.563Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775 }, + { url = "https://files.pythonhosted.org/packages/c9/af/0dcccc7fdcdf170f9a1585e5e96b6fb0ba1749ef6be8c89a6202284759bd/celery-5.5.3-py3-none-any.whl", hash = "sha256:0b5761a07057acee94694464ca482416b959568904c9dfa41ce8413a7d65d525", size = 438775, upload-time = "2025-06-01T11:08:09.94Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -617,111 +617,111 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] @@ -731,9 +731,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] @@ -743,9 +743,9 @@ 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 } +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 }, + { 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]] @@ -755,9 +755,9 @@ 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 } +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 }, + { 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]] @@ -768,102 +768,102 @@ 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 } +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 }, + { 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" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449 }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810 }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308 }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052 }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165 }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432 }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716 }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089 }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232 }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299 }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796 }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673 }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990 }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800 }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415 }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474 }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844 }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832 }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434 }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676 }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807 }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058 }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805 }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766 }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923 }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591 }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364 }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010 }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818 }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438 }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165 }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516 }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804 }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885 }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308 }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452 }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057 }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875 }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500 }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212 }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398 }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584 }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688 }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746 }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003 }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522 }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855 }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887 }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396 }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745 }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055 }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911 }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754 }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720 }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994 }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531 }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189 }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258 }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073 }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638 }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246 }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514 }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877 }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004 }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408 }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544 }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980 }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871 }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472 }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210 }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319 }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638 }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040 }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148 }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172 }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242 }, +sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, + { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, + { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, + { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, + { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, + { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, + { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, + { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, + { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, + { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, + { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, + { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, + { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, + { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, + { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, + { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, + { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, + { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, + { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, + { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, + { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, + { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, + { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, + { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, + { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, + { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, ] [[package]] @@ -873,9 +873,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762 } +sdist = { url = "https://files.pythonhosted.org/packages/df/de/5832661ed55107b8a09af3f0a2e71e0957226a59eb1dcf0a445cce6daf20/croniter-6.2.2.tar.gz", hash = "sha256:ba60832a5ec8e12e51b8691c3309a113d1cf6526bdf1a48150ce8ec7a532d0ab", size = 113762, upload-time = "2026-03-15T08:43:48.112Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422 }, + { url = "https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl", hash = "sha256:a5d17b1060974d36251ea4faf388233eca8acf0d09cbd92d35f4c4ac8f279960", size = 45422, upload-time = "2026-03-15T08:43:46.626Z" }, ] [[package]] @@ -885,50 +885,50 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324 }, - { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243 }, - { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235 }, - { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323 }, - { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085 }, - { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137 }, - { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867 }, - { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805 }, - { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461 }, - { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488 }, - { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256 }, - { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117 }, - { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154 }, - { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138 }, - { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830 }, - { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201 }, - { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822 }, - { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875 }, - { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385 }, - { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082 }, - { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328 }, - { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530 }, - { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046 }, - { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660 }, - { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229 }, - { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364 }, - { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498 }, - { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790 }, - { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408 }, - { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196 }, - { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782 }, - { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618 }, - { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970 }, - { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873 }, - { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804 }, - { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217 }, - { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252 }, - { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388 }, - { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186 }, - { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539 }, - { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307 }, - { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408 }, +sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" }, + { url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" }, + { url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" }, + { url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" }, + { url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" }, + { url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" }, + { url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" }, + { url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" }, + { url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" }, + { url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" }, + { url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" }, + { url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" }, ] [[package]] @@ -939,48 +939,48 @@ dependencies = [ { name = "marshmallow" }, { name = "typing-inspect" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227 } +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686 }, + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, ] [[package]] name = "debugpy" version = "1.8.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686 }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588 }, - { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372 }, - { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835 }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560 }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272 }, - { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208 }, - { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930 }, - { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066 }, - { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425 }, - { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407 }, - { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521 }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658 }, + { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, + { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, + { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, + { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, + { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, + { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, + { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, + { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, + { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, ] [[package]] name = "decorator" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711 } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190 }, + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] [[package]] name = "defusedxml" version = "0.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604 }, + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] [[package]] @@ -990,9 +990,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } +sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, + { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" }, ] [[package]] @@ -1002,27 +1002,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178 }, + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, ] [[package]] name = "dirtyjson" version = "1.0.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782 } +sdist = { url = "https://files.pythonhosted.org/packages/db/04/d24f6e645ad82ba0ef092fa17d9ef7a21953781663648a01c9371d9e8e98/dirtyjson-1.0.8.tar.gz", hash = "sha256:90ca4a18f3ff30ce849d100dcf4a003953c79d3a2348ef056f1d9c22231a25fd", size = 30782, upload-time = "2022-11-28T23:32:33.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197 }, + { url = "https://files.pythonhosted.org/packages/68/69/1bcf70f81de1b4a9f21b3a62ec0c83bdff991c88d6cc2267d02408457e88/dirtyjson-1.0.8-py3-none-any.whl", hash = "sha256:125e27248435a58acace26d5c2c4c11a1c0de0a9c5124c5a94ba78e517d74f53", size = 25197, upload-time = "2022-11-28T23:32:31.219Z" }, ] [[package]] name = "distro" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] [[package]] @@ -1036,9 +1036,9 @@ dependencies = [ { name = "urllib3" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/73/f7c9a14e88e769f38cb7fb45aa88dfd795faa8e18aea11bababf6e068d5e/docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20", size = 259301 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/73/f7c9a14e88e769f38cb7fb45aa88dfd795faa8e18aea11bababf6e068d5e/docker-6.1.3.tar.gz", hash = "sha256:aa6d17830045ba5ef0168d5eaa34d37beeb113948c413affe1d5991fc11f9a20", size = 259301, upload-time = "2023-06-01T14:24:49.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/be/3032490fa33b36ddc8c4b1da3252c6f974e7133f1a50de00c6b85cca203a/docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9", size = 148096 }, + { url = "https://files.pythonhosted.org/packages/db/be/3032490fa33b36ddc8c4b1da3252c6f974e7133f1a50de00c6b85cca203a/docker-6.1.3-py3-none-any.whl", hash = "sha256:aecd2277b8bf8e506e484f6ab7aec39abe0038e29fa4a6d3ba86c3fe01844ed9", size = 148096, upload-time = "2023-06-01T14:24:47.769Z" }, ] [[package]] @@ -1050,9 +1050,9 @@ dependencies = [ { name = "six" }, { name = "stone" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/56/ac085f58e8e0d0bcafdf98c2605e454ac946e3d0c72679669ae112dc30be/dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962", size = 560236 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/56/ac085f58e8e0d0bcafdf98c2605e454ac946e3d0c72679669ae112dc30be/dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962", size = 560236, upload-time = "2024-06-03T16:45:30.448Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/de/95d8204d9a20fbdb353c5f8e4229b0fcb90f22b96f8246ff1f47c8a45fd5/dropbox-12.0.2-py3-none-any.whl", hash = "sha256:c5b7e9c2668adb6b12dcecd84342565dc50f7d35ab6a748d155cb79040979d1c", size = 572076 }, + { url = "https://files.pythonhosted.org/packages/2d/de/95d8204d9a20fbdb353c5f8e4229b0fcb90f22b96f8246ff1f47c8a45fd5/dropbox-12.0.2-py3-none-any.whl", hash = "sha256:c5b7e9c2668adb6b12dcecd84342565dc50f7d35ab6a748d155cb79040979d1c", size = 572076, upload-time = "2024-06-03T16:45:28.153Z" }, ] [[package]] @@ -1064,7 +1064,7 @@ dependencies = [ { name = "fsspec" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/15/6d8f4c3033ad2bc364b8bb613c52c96653f2268f32ecff4f3ab5f1d7c19b/dropboxdrivefs-1.4.1.tar.gz", hash = "sha256:6f3c6061d045813553ce91ed0e2b682f1d70bec74011943c92b3181faacefd34", size = 7413 } +sdist = { url = "https://files.pythonhosted.org/packages/97/15/6d8f4c3033ad2bc364b8bb613c52c96653f2268f32ecff4f3ab5f1d7c19b/dropboxdrivefs-1.4.1.tar.gz", hash = "sha256:6f3c6061d045813553ce91ed0e2b682f1d70bec74011943c92b3181faacefd34", size = 7413, upload-time = "2024-05-27T14:04:37.648Z" } [[package]] name = "factory-boy" @@ -1073,9 +1073,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "faker" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146 } +sdist = { url = "https://files.pythonhosted.org/packages/ba/98/75cacae9945f67cfe323829fc2ac451f64517a8a330b572a06a323997065/factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03", size = 164146, upload-time = "2025-02-03T09:49:04.433Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036 }, + { url = "https://files.pythonhosted.org/packages/27/8d/2bc5f5546ff2ccb3f7de06742853483ab75bf74f36a92254702f8baecc79/factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc", size = 37036, upload-time = "2025-02-03T09:49:01.659Z" }, ] [[package]] @@ -1085,68 +1085,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/2a/96fff3edcb10f6505143448a4b91535f77b74865cec45be52690ee280443/faker-40.5.1.tar.gz", hash = "sha256:70222361cd82aa10cb86066d1a4e8f47f2bcdc919615c412045a69c4e6da0cd3", size = 1952684 } +sdist = { url = "https://files.pythonhosted.org/packages/03/2a/96fff3edcb10f6505143448a4b91535f77b74865cec45be52690ee280443/faker-40.5.1.tar.gz", hash = "sha256:70222361cd82aa10cb86066d1a4e8f47f2bcdc919615c412045a69c4e6da0cd3", size = 1952684, upload-time = "2026-02-23T21:34:38.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/a9/1eed4db92d0aec2f9bfdf1faae0ab0418b5e121dda5701f118a7a4f0cd6a/faker-40.5.1-py3-none-any.whl", hash = "sha256:c69640c1e13bad49b4bcebcbf1b52f9f1a872b6ea186c248ada34d798f1661bf", size = 1987053 }, + { url = "https://files.pythonhosted.org/packages/4d/a9/1eed4db92d0aec2f9bfdf1faae0ab0418b5e121dda5701f118a7a4f0cd6a/faker-40.5.1-py3-none-any.whl", hash = "sha256:c69640c1e13bad49b4bcebcbf1b52f9f1a872b6ea186c248ada34d798f1661bf", size = 1987053, upload-time = "2026-02-23T21:34:36.418Z" }, ] [[package]] name = "fastuuid" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164 }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837 }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370 }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766 }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105 }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564 }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659 }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430 }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894 }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374 }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550 }, - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720 }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024 }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679 }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862 }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278 }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788 }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819 }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546 }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921 }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559 }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539 }, - { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600 }, - { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069 }, - { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543 }, - { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798 }, - { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283 }, - { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627 }, - { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778 }, - { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605 }, - { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837 }, - { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532 }, - { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457 }, +sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, + { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, + { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, + { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, + { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, + { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, + { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, + { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, + { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, + { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/8c7660d1fe3862e3f8acabd9be7fc9ad71eb270f1c65cce9a2b7a31329ab/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b852a870a61cfc26c884af205d502881a2e59cc07076b60ab4a951cc0c94d1ad", size = 510600, upload-time = "2025-10-19T22:43:44.17Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f4/a989c82f9a90d0ad995aa957b3e572ebef163c5299823b4027986f133dfb/fastuuid-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c7502d6f54cd08024c3ea9b3514e2d6f190feb2f46e6dbcd3747882264bb5f7b", size = 262069, upload-time = "2025-10-19T22:43:38.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a1a24f73574ac995482b1326cf7ab41301af0fabaa3e37eeb6b3df00e6e2/fastuuid-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ca61b592120cf314cfd66e662a5b54a578c5a15b26305e1b8b618a6f22df714", size = 251543, upload-time = "2025-10-19T22:32:22.537Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/2a9b59185ba7a6c7b37808431477c2d739fcbdabbf63e00243e37bd6bf49/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa75b6657ec129d0abded3bec745e6f7ab642e6dba3a5272a68247e85f5f316f", size = 277798, upload-time = "2025-10-19T22:33:53.821Z" }, + { url = "https://files.pythonhosted.org/packages/ef/33/4105ca574f6ded0af6a797d39add041bcfb468a1255fbbe82fcb6f592da2/fastuuid-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8a0dfea3972200f72d4c7df02c8ac70bad1bb4c58d7e0ec1e6f341679073a7f", size = 278283, upload-time = "2025-10-19T22:29:02.812Z" }, + { url = "https://files.pythonhosted.org/packages/fe/8c/fca59f8e21c4deb013f574eae05723737ddb1d2937ce87cb2a5d20992dc3/fastuuid-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1bf539a7a95f35b419f9ad105d5a8a35036df35fdafae48fb2fd2e5f318f0d75", size = 301627, upload-time = "2025-10-19T22:35:54.985Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e2/f78c271b909c034d429218f2798ca4e89eeda7983f4257d7865976ddbb6c/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:9a133bf9cc78fdbd1179cb58a59ad0100aa32d8675508150f3658814aeefeaa4", size = 459778, upload-time = "2025-10-19T22:28:00.999Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f0/5ff209d865897667a2ff3e7a572267a9ced8f7313919f6d6043aed8b1caa/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:f54d5b36c56a2d5e1a31e73b950b28a0d83eb0c37b91d10408875a5a29494bad", size = 478605, upload-time = "2025-10-19T22:36:21.764Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c8/2ce1c78f983a2c4987ea865d9516dbdfb141a120fd3abb977ae6f02ba7ca/fastuuid-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:ec27778c6ca3393ef662e2762dba8af13f4ec1aaa32d08d77f71f2a70ae9feb8", size = 450837, upload-time = "2025-10-19T22:34:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/df/60/dad662ec9a33b4a5fe44f60699258da64172c39bd041da2994422cdc40fe/fastuuid-0.14.0-cp314-cp314-win32.whl", hash = "sha256:e23fc6a83f112de4be0cc1990e5b127c27663ae43f866353166f87df58e73d06", size = 154532, upload-time = "2025-10-19T22:35:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/da4db31001e854025ffd26bc9ba0740a9cbba2c3259695f7c5834908b336/fastuuid-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:df61342889d0f5e7a32f7284e55ef95103f2110fee433c2ae7c2c0956d76ac8a", size = 156457, upload-time = "2025-10-19T22:33:44.579Z" }, ] [[package]] name = "filelock" version = "3.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158 } +sdist = { url = "https://files.pythonhosted.org/packages/77/18/a1fd2231c679dcb9726204645721b12498aeac28e1ad0601038f94b42556/filelock-3.25.0.tar.gz", hash = "sha256:8f00faf3abf9dc730a1ffe9c354ae5c04e079ab7d3a683b7c32da5dd05f26af3", size = 40158, upload-time = "2026-03-01T15:08:45.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427 }, + { url = "https://files.pythonhosted.org/packages/f9/0b/de6f54d4a8bedfe8645c41497f3c18d749f0bd3218170c667bf4b81d0cdd/filelock-3.25.0-py3-none-any.whl", hash = "sha256:5ccf8069f7948f494968fc0713c10e5c182a9c9d9eef3a636307a20c2490f047", size = 26427, upload-time = "2026-03-01T15:08:44.593Z" }, ] [[package]] name = "filetype" version = "1.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970 }, + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] [[package]] @@ -1158,107 +1158,107 @@ dependencies = [ { name = "pycodestyle" }, { name = "pyflakes" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922 }, + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, ] [[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, +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/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, ] [[package]] name = "fsspec" version = "2024.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/52/f16a068ebadae42526484c31f4398e62962504e5724a8ba5dc3409483df2/fsspec-2024.10.0.tar.gz", hash = "sha256:eda2d8a4116d4f2429db8550f2457da57279247dd930bb12f821b58391359493", size = 286853, upload-time = "2024-10-21T01:21:16.969Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641 }, + { url = "https://files.pythonhosted.org/packages/c6/b2/454d6e7f0158951d8a78c2e1eb4f69ae81beb8dca5fee9809c6c99e9d0d0/fsspec-2024.10.0-py3-none-any.whl", hash = "sha256:03b9a6785766a4de40368b88906366755e2819e758b83705c88cd7cb5fe81871", size = 179641, upload-time = "2024-10-21T01:21:14.793Z" }, ] [package.optional-dependencies] @@ -1270,9 +1270,9 @@ sftp = [ name = "funcy" version = "2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931 } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931, upload-time = "2023-03-28T06:22:46.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891 }, + { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, ] [[package]] @@ -1288,9 +1288,9 @@ dependencies = [ { name = "google-cloud-storage" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/1e/1d8c4593d9e2eb04918fec43253ab152823d67ad51ad9e3ab6b3a78c431a/gcsfs-2024.10.0.tar.gz", hash = "sha256:5df54cfe568e8fdeea5aafa7fed695cdc69a9a674e991ca8c1ce634f5df1d314", size = 79588 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/1e/1d8c4593d9e2eb04918fec43253ab152823d67ad51ad9e3ab6b3a78c431a/gcsfs-2024.10.0.tar.gz", hash = "sha256:5df54cfe568e8fdeea5aafa7fed695cdc69a9a674e991ca8c1ce634f5df1d314", size = 79588, upload-time = "2024-10-21T13:43:26.163Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/96/d60e835fb7d10166c77aef0c1fa30e634153c03a0f486786977b95f88fde/gcsfs-2024.10.0-py2.py3-none-any.whl", hash = "sha256:bb2d23547e61203ea2dda5fa6c4b91a0c34b74ebe8bb6ab1926f6c33381bceb2", size = 34953 }, + { url = "https://files.pythonhosted.org/packages/dc/96/d60e835fb7d10166c77aef0c1fa30e634153c03a0f486786977b95f88fde/gcsfs-2024.10.0-py2.py3-none-any.whl", hash = "sha256:bb2d23547e61203ea2dda5fa6c4b91a0c34b74ebe8bb6ab1926f6c33381bceb2", size = 34953, upload-time = "2024-10-21T13:43:24.951Z" }, ] [[package]] @@ -1304,9 +1304,9 @@ dependencies = [ { name = "protobuf" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266 } +sdist = { url = "https://files.pythonhosted.org/packages/09/cd/63f1557235c2440fe0577acdbc32577c5c002684c58c7f4d770a92366a24/google_api_core-2.25.2.tar.gz", hash = "sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300", size = 166266, upload-time = "2025-10-03T00:07:34.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/d8/894716a5423933f5c8d2d5f04b16f052a515f78e815dab0c2c6f1fd105dc/google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7", size = 162489 }, + { url = "https://files.pythonhosted.org/packages/c8/d8/894716a5423933f5c8d2d5f04b16f052a515f78e815dab0c2c6f1fd105dc/google_api_core-2.25.2-py3-none-any.whl", hash = "sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7", size = 162489, upload-time = "2025-10-03T00:07:32.924Z" }, ] [package.optional-dependencies] @@ -1326,9 +1326,9 @@ dependencies = [ { name = "httplib2" }, { name = "uritemplate" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/4ab3e3516b93bb50ed7814738ea61d49cba3f72f4e331dc9518ae2731e92/google_api_python_client-2.190.0.tar.gz", hash = "sha256:5357f34552e3724d80d2604c8fa146766e0a9d6bb0afada886fafed9feafeef6", size = 14111143, upload-time = "2026-02-12T00:38:03.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070 }, + { url = "https://files.pythonhosted.org/packages/07/ad/223d5f4b0b987669ffeb3eadd7e9f85ece633aa7fd3246f1e2f6238e1e05/google_api_python_client-2.190.0-py3-none-any.whl", hash = "sha256:d9b5266758f96c39b8c21d9bbfeb4e58c14dbfba3c931f7c5a8d7fdcd292dd57", size = 14682070, upload-time = "2026-02-12T00:38:00.974Z" }, ] [[package]] @@ -1339,9 +1339,9 @@ dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379 } +sdist = { url = "https://files.pythonhosted.org/packages/81/1c/70b23fc52b2bb3c70b379f3bd05c4a60ab3a873e30c6bd21c57e0154848a/google_auth-2.55.0.tar.gz", hash = "sha256:fcd3a130f575fa36403d38774af1c64a4fbfbca09215f0589d2372b5119697cb", size = 349379, upload-time = "2026-06-15T22:33:16.466Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400 }, + { url = "https://files.pythonhosted.org/packages/44/71/c0321dc6d63d99946da45f7c06299b934e4f7f7da5c4f14d101bcb39adf1/google_auth-2.55.0-py3-none-any.whl", hash = "sha256:a17cef9dedf98c4ebae2fb0c48c8f75952c877cbc2efe09f329ef16c2783d88a", size = 252400, upload-time = "2026-06-15T22:33:14.992Z" }, ] [[package]] @@ -1352,9 +1352,9 @@ dependencies = [ { name = "google-auth" }, { name = "httplib2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134 } +sdist = { url = "https://files.pythonhosted.org/packages/d5/ad/c1f2b1175096a8d04cf202ad5ea6065f108d26be6fc7215876bde4a7981d/google_auth_httplib2-0.3.0.tar.gz", hash = "sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b", size = 11134, upload-time = "2025-12-15T22:13:51.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529 }, + { url = "https://files.pythonhosted.org/packages/99/d5/3c97526c8796d3caf5f4b3bed2b05e8a7102326f00a334e7a438237f3b22/google_auth_httplib2-0.3.0-py3-none-any.whl", hash = "sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776", size = 9529, upload-time = "2025-12-15T22:13:51.048Z" }, ] [[package]] @@ -1365,9 +1365,9 @@ dependencies = [ { name = "google-auth" }, { name = "requests-oauthlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/b4/1b19567e4c567b796f5c593d89895f3cfae5a38e04f27c6af87618fd0942/google_auth_oauthlib-1.3.0.tar.gz", hash = "sha256:cd39e807ac7229d6b8b9c1e297321d36fcc8a9e4857dff4301870985df51a528", size = 21777 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/b4/1b19567e4c567b796f5c593d89895f3cfae5a38e04f27c6af87618fd0942/google_auth_oauthlib-1.3.0.tar.gz", hash = "sha256:cd39e807ac7229d6b8b9c1e297321d36fcc8a9e4857dff4301870985df51a528", size = 21777, upload-time = "2026-02-27T14:13:01.489Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/56/909fd5632226d3fba31d7aeffd4754410735d49362f5809956fe3e9af344/google_auth_oauthlib-1.3.0-py3-none-any.whl", hash = "sha256:386b3fb85cf4a5b819c6ad23e3128d975216b4cac76324de1d90b128aaf38f29", size = 19308 }, + { url = "https://files.pythonhosted.org/packages/2f/56/909fd5632226d3fba31d7aeffd4754410735d49362f5809956fe3e9af344/google_auth_oauthlib-1.3.0-py3-none-any.whl", hash = "sha256:386b3fb85cf4a5b819c6ad23e3128d975216b4cac76324de1d90b128aaf38f29", size = 19308, upload-time = "2026-02-27T14:12:47.865Z" }, ] [[package]] @@ -1385,9 +1385,9 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/ff/2c520952db184dec31e2ee988cfa37fa9e7776935a3f2eccc44252ecab5f/google-cloud-bigquery-3.11.4.tar.gz", hash = "sha256:697df117241a2283bcbb93b21e10badc14e51c9a90800d2a7e1a3e1c7d842974", size = 410777 } +sdist = { url = "https://files.pythonhosted.org/packages/62/ff/2c520952db184dec31e2ee988cfa37fa9e7776935a3f2eccc44252ecab5f/google-cloud-bigquery-3.11.4.tar.gz", hash = "sha256:697df117241a2283bcbb93b21e10badc14e51c9a90800d2a7e1a3e1c7d842974", size = 410777, upload-time = "2023-07-19T23:12:12.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/6a/d0ef792288f2fa2cfea80899a82de302b3332dfda41984fe114e2cfbf700/google_cloud_bigquery-3.11.4-py2.py3-none-any.whl", hash = "sha256:5fa7897743a0ed949ade25a0942fc9e7557d8fce307c6f8a76d1b604cf27f1b1", size = 219607 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/d0ef792288f2fa2cfea80899a82de302b3332dfda41984fe114e2cfbf700/google_cloud_bigquery-3.11.4-py2.py3-none-any.whl", hash = "sha256:5fa7897743a0ed949ade25a0942fc9e7557d8fce307c6f8a76d1b604cf27f1b1", size = 219607, upload-time = "2023-07-19T23:12:09.449Z" }, ] [[package]] @@ -1398,9 +1398,9 @@ dependencies = [ { name = "google-api-core" }, { name = "google-auth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469 }, + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, ] [[package]] @@ -1413,9 +1413,9 @@ dependencies = [ { name = "proto-plus" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/6b/92b705f408c1d928526b65d1259be4254ef1f45e620f01f8665156b4d781/google-cloud-secret-manager-2.16.1.tar.gz", hash = "sha256:149d11ce9be7ea81d4ac3544d3fcd4c716a9edb2cb775d9c075231570b079fbb", size = 128884 } +sdist = { url = "https://files.pythonhosted.org/packages/48/6b/92b705f408c1d928526b65d1259be4254ef1f45e620f01f8665156b4d781/google-cloud-secret-manager-2.16.1.tar.gz", hash = "sha256:149d11ce9be7ea81d4ac3544d3fcd4c716a9edb2cb775d9c075231570b079fbb", size = 128884, upload-time = "2023-03-27T14:51:09.684Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/e3/c3aade516eaf544bd7d86694178de9c2da8eff8fc40326d0265acc65991d/google_cloud_secret_manager-2.16.1-py2.py3-none-any.whl", hash = "sha256:dad28c24921fb62961aafe808be0e7935a99096f03ac29eeeefa04b85534c1f3", size = 116749 }, + { url = "https://files.pythonhosted.org/packages/6c/e3/c3aade516eaf544bd7d86694178de9c2da8eff8fc40326d0265acc65991d/google_cloud_secret_manager-2.16.1-py2.py3-none-any.whl", hash = "sha256:dad28c24921fb62961aafe808be0e7935a99096f03ac29eeeefa04b85534c1f3", size = 116749, upload-time = "2023-03-27T14:51:07.661Z" }, ] [[package]] @@ -1429,32 +1429,32 @@ dependencies = [ { name = "google-resumable-media" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/50/c9998f84fd8ce8799d7f8020466bbc5c9e3b1126b04a09fdb02378d451b0/google-cloud-storage-2.9.0.tar.gz", hash = "sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3", size = 5498811 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/50/c9998f84fd8ce8799d7f8020466bbc5c9e3b1126b04a09fdb02378d451b0/google-cloud-storage-2.9.0.tar.gz", hash = "sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3", size = 5498811, upload-time = "2023-05-04T17:56:46.265Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl", hash = "sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94", size = 113512 }, + { url = "https://files.pythonhosted.org/packages/74/fb/3770e7f44cf6133f502e1b8503b6739351b53272cf8313b47f1de6cf4960/google_cloud_storage-2.9.0-py2.py3-none-any.whl", hash = "sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94", size = 113512, upload-time = "2023-05-04T17:56:43.929Z" }, ] [[package]] name = "google-crc32c" version = "1.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192 } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300 }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867 }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364 }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740 }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437 }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297 }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867 }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344 }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694 }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435 }, - { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301 }, - { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868 }, - { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381 }, - { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734 }, - { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878 }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, ] [[package]] @@ -1464,9 +1464,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-crc32c" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265 } +sdist = { url = "https://files.pythonhosted.org/packages/64/d7/520b62a35b23038ff005e334dba3ffc75fcf583bee26723f1fd8fd4b6919/google_resumable_media-2.8.0.tar.gz", hash = "sha256:f1157ed8b46994d60a1bc432544db62352043113684d4e030ee02e77ebe9a1ae", size = 2163265, upload-time = "2025-11-17T15:38:06.659Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340 }, + { url = "https://files.pythonhosted.org/packages/1f/0b/93afde9cfe012260e9fe1522f35c9b72d6ee222f316586b1f23ecf44d518/google_resumable_media-2.8.0-py3-none-any.whl", hash = "sha256:dd14a116af303845a8d932ddae161a26e86cc229645bc98b39f026f9b1717582", size = 81340, upload-time = "2025-11-17T15:38:05.594Z" }, ] [[package]] @@ -1476,9 +1476,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] [package.optional-dependencies] @@ -1490,43 +1490,43 @@ grpc = [ name = "greenlet" version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358 }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217 }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792 }, - { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250 }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875 }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467 }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001 }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081 }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331 }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120 }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238 }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219 }, - { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268 }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774 }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277 }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455 }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961 }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221 }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650 }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295 }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163 }, - { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371 }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160 }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181 }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713 }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034 }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437 }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617 }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189 }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225 }, - { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581 }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907 }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857 }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010 }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086 }, +sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, + { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, + { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, + { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, + { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, + { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, + { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, + { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, + { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, + { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, + { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, + { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, + { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, + { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] [[package]] @@ -1537,9 +1537,9 @@ dependencies = [ { name = "griffecli" }, { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/56/28a0accac339c164b52a92c6cfc45a903acc0c174caa5c1713803467b533/griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f", size = 293906 } +sdist = { url = "https://files.pythonhosted.org/packages/04/56/28a0accac339c164b52a92c6cfc45a903acc0c174caa5c1713803467b533/griffe-2.0.0.tar.gz", hash = "sha256:c68979cd8395422083a51ea7cf02f9c119d889646d99b7b656ee43725de1b80f", size = 293906, upload-time = "2026-03-23T21:06:53.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214 }, + { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, ] [[package]] @@ -1550,18 +1550,18 @@ dependencies = [ { name = "colorama" }, { name = "griffelib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/f8/2e129fd4a86e52e58eefe664de05e7d502decf766e7316cc9e70fdec3e18/griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef", size = 56213 } +sdist = { url = "https://files.pythonhosted.org/packages/a4/f8/2e129fd4a86e52e58eefe664de05e7d502decf766e7316cc9e70fdec3e18/griffecli-2.0.0.tar.gz", hash = "sha256:312fa5ebb4ce6afc786356e2d0ce85b06c1c20d45abc42d74f0cda65e159f6ef", size = 56213, upload-time = "2026-03-23T21:06:54.8Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345 }, + { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, ] [[package]] name = "griffelib" version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312 } +sdist = { url = "https://files.pythonhosted.org/packages/ad/06/eccbd311c9e2b3ca45dbc063b93134c57a1ccc7607c5e545264ad092c4a9/griffelib-2.0.0.tar.gz", hash = "sha256:e504d637a089f5cab9b5daf18f7645970509bf4f53eda8d79ed71cce8bd97934", size = 166312, upload-time = "2026-03-23T21:06:55.954Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004 }, + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, ] [[package]] @@ -1573,9 +1573,9 @@ dependencies = [ { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745 } +sdist = { url = "https://files.pythonhosted.org/packages/76/1e/1011451679a983f2f5c6771a1682542ecb027776762ad031fd0d7129164b/grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389", size = 23745, upload-time = "2025-10-15T21:14:53.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690 }, + { url = "https://files.pythonhosted.org/packages/4a/bd/330a1bbdb1afe0b96311249e699b6dc9cfc17916394fd4503ac5aca2514b/grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6", size = 32690, upload-time = "2025-10-15T21:14:51.72Z" }, ] [[package]] @@ -1585,38 +1585,38 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985 }, - { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853 }, - { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766 }, - { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027 }, - { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766 }, - { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161 }, - { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303 }, - { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222 }, - { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123 }, - { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657 }, - { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143 }, - { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926 }, - { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628 }, - { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574 }, - { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639 }, - { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838 }, - { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878 }, - { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412 }, - { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899 }, - { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393 }, - { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591 }, - { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685 }, - { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803 }, - { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206 }, - { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826 }, - { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897 }, - { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404 }, - { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837 }, - { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439 }, - { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852 }, +sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, + { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, + { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, + { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, + { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, + { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, + { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, + { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, + { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, + { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, + { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, + { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, + { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, ] [[package]] @@ -1628,9 +1628,9 @@ dependencies = [ { name = "grpcio" }, { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/d7/013ef01c5a1c2fd0932c27c904934162f69f41ca0f28396d3ffe4d386123/grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485", size = 13063 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/d7/013ef01c5a1c2fd0932c27c904934162f69f41ca0f28396d3ffe4d386123/grpcio-status-1.62.3.tar.gz", hash = "sha256:289bdd7b2459794a12cf95dc0cb727bd4a1742c37bd823f760236c937e53a485", size = 13063, upload-time = "2024-08-06T00:37:08.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/40/972271de05f9315c0d69f9f7ebbcadd83bc85322f538637d11bb8c67803d/grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8", size = 14448 }, + { url = "https://files.pythonhosted.org/packages/90/40/972271de05f9315c0d69f9f7ebbcadd83bc85322f538637d11bb8c67803d/grpcio_status-1.62.3-py3-none-any.whl", hash = "sha256:f9049b762ba8de6b1086789d8315846e094edac2c50beaf462338b301a8fd4b8", size = 14448, upload-time = "2024-08-06T00:30:15.702Z" }, ] [[package]] @@ -1642,25 +1642,25 @@ dependencies = [ { name = "protobuf" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/54/fa/b69bd8040eafc09b88bb0ec0fea59e8aacd1a801e688af087cead213b0d0/grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833", size = 4538520 } +sdist = { url = "https://files.pythonhosted.org/packages/54/fa/b69bd8040eafc09b88bb0ec0fea59e8aacd1a801e688af087cead213b0d0/grpcio-tools-1.62.3.tar.gz", hash = "sha256:7c7136015c3d62c3eef493efabaf9e3380e3e66d24ee8e94c01cb71377f57833", size = 4538520, upload-time = "2024-08-06T00:37:11.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/a5/d6887eba415ce318ae5005e8dfac3fa74892400b54b6d37b79e8b4f14f5e/grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5", size = 5147690 }, - { url = "https://files.pythonhosted.org/packages/8a/7c/3cde447a045e83ceb4b570af8afe67ffc86896a2fe7f59594dc8e5d0a645/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133", size = 2720538 }, - { url = "https://files.pythonhosted.org/packages/88/07/f83f2750d44ac4f06c07c37395b9c1383ef5c994745f73c6bfaf767f0944/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa", size = 3071571 }, - { url = "https://files.pythonhosted.org/packages/37/74/40175897deb61e54aca716bc2e8919155b48f33aafec8043dda9592d8768/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0", size = 2806207 }, - { url = "https://files.pythonhosted.org/packages/ec/ee/d8de915105a217cbcb9084d684abdc032030dcd887277f2ef167372287fe/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d", size = 3685815 }, - { url = "https://files.pythonhosted.org/packages/fd/d9/4360a6c12be3d7521b0b8c39e5d3801d622fbb81cc2721dbd3eee31e28c8/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc", size = 3298378 }, - { url = "https://files.pythonhosted.org/packages/29/3b/7cdf4a9e5a3e0a35a528b48b111355cd14da601413a4f887aa99b6da468f/grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b", size = 910416 }, - { url = "https://files.pythonhosted.org/packages/6c/66/dd3ec249e44c1cc15e902e783747819ed41ead1336fcba72bf841f72c6e9/grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7", size = 1052856 }, + { url = "https://files.pythonhosted.org/packages/2a/a5/d6887eba415ce318ae5005e8dfac3fa74892400b54b6d37b79e8b4f14f5e/grpcio_tools-1.62.3-cp312-cp312-macosx_10_10_universal2.whl", hash = "sha256:d102b9b21c4e1e40af9a2ab3c6d41afba6bd29c0aa50ca013bf85c99cdc44ac5", size = 5147690, upload-time = "2024-08-06T00:31:16.436Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/3cde447a045e83ceb4b570af8afe67ffc86896a2fe7f59594dc8e5d0a645/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:0a52cc9444df978438b8d2332c0ca99000521895229934a59f94f37ed896b133", size = 2720538, upload-time = "2024-08-06T00:31:18.905Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/f83f2750d44ac4f06c07c37395b9c1383ef5c994745f73c6bfaf767f0944/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141d028bf5762d4a97f981c501da873589df3f7e02f4c1260e1921e565b376fa", size = 3071571, upload-time = "2024-08-06T00:31:21.684Z" }, + { url = "https://files.pythonhosted.org/packages/37/74/40175897deb61e54aca716bc2e8919155b48f33aafec8043dda9592d8768/grpcio_tools-1.62.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47a5c093ab256dec5714a7a345f8cc89315cb57c298b276fa244f37a0ba507f0", size = 2806207, upload-time = "2024-08-06T00:31:24.208Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/d8de915105a217cbcb9084d684abdc032030dcd887277f2ef167372287fe/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f6831fdec2b853c9daa3358535c55eed3694325889aa714070528cf8f92d7d6d", size = 3685815, upload-time = "2024-08-06T00:31:26.917Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/4360a6c12be3d7521b0b8c39e5d3801d622fbb81cc2721dbd3eee31e28c8/grpcio_tools-1.62.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e02d7c1a02e3814c94ba0cfe43d93e872c758bd8fd5c2797f894d0c49b4a1dfc", size = 3298378, upload-time = "2024-08-06T00:31:30.401Z" }, + { url = "https://files.pythonhosted.org/packages/29/3b/7cdf4a9e5a3e0a35a528b48b111355cd14da601413a4f887aa99b6da468f/grpcio_tools-1.62.3-cp312-cp312-win32.whl", hash = "sha256:b881fd9505a84457e9f7e99362eeedd86497b659030cf57c6f0070df6d9c2b9b", size = 910416, upload-time = "2024-08-06T00:31:33.118Z" }, + { url = "https://files.pythonhosted.org/packages/6c/66/dd3ec249e44c1cc15e902e783747819ed41ead1336fcba72bf841f72c6e9/grpcio_tools-1.62.3-cp312-cp312-win_amd64.whl", hash = "sha256:11c625eebefd1fd40a228fc8bae385e448c7e32a6ae134e43cf13bbc23f902b7", size = 1052856, upload-time = "2024-08-06T00:31:36.519Z" }, ] [[package]] name = "h11" version = "0.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] @@ -1671,50 +1671,50 @@ dependencies = [ { name = "hpack" }, { name = "hyperframe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 } +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 }, + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, ] [[package]] name = "hf-xet" version = "1.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019 }, - { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565 }, - { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494 }, - { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601 }, - { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770 }, - { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161 }, - { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377 }, - { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875 }, - { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076 }, - { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745 }, - { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301 }, - { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437 }, - { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535 }, - { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891 }, - { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583 }, - { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977 }, - { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961 }, - { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171 }, - { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750 }, - { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035 }, - { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378 }, - { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020 }, - { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624 }, - { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976 }, +sdist = { url = "https://files.pythonhosted.org/packages/8b/cb/9bb543bd987ffa1ee48202cc96a756951b734b79a542335c566148ade36c/hf_xet-1.3.2.tar.gz", hash = "sha256:e130ee08984783d12717444e538587fa2119385e5bd8fc2bb9f930419b73a7af", size = 643646, upload-time = "2026-02-27T17:26:08.051Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/75/462285971954269432aad2e7938c5c7ff9ec7d60129cec542ab37121e3d6/hf_xet-1.3.2-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:335a8f36c55fd35a92d0062f4e9201b4015057e62747b7e7001ffb203c0ee1d2", size = 3761019, upload-time = "2026-02-27T17:25:49.441Z" }, + { url = "https://files.pythonhosted.org/packages/35/56/987b0537ddaf88e17192ea09afa8eca853e55f39a4721578be436f8409df/hf_xet-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c1ae4d3a716afc774e66922f3cac8206bfa707db13f6a7e62dfff74bfc95c9a8", size = 3521565, upload-time = "2026-02-27T17:25:47.469Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5c/7e4a33a3d689f77761156cc34558047569e54af92e4d15a8f493229f6767/hf_xet-1.3.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6dbdf231efac0b9b39adcf12a07f0c030498f9212a18e8c50224d0e84ab803d", size = 4176494, upload-time = "2026-02-27T17:25:40.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b3/71e856bf9d9a69b3931837e8bf22e095775f268c8edcd4a9e8c355f92484/hf_xet-1.3.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c1980abfb68ecf6c1c7983379ed7b1e2b49a1aaf1a5aca9acc7d48e5e2e0a961", size = 3955601, upload-time = "2026-02-27T17:25:38.376Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/aecf97b3f0a981600a67ff4db15e2d433389d698a284bb0ea5d8fcdd6f7f/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1c88fbd90ad0d27c46b77a445f0a436ebaa94e14965c581123b68b1c52f5fd30", size = 4154770, upload-time = "2026-02-27T17:25:56.756Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e1/3af961f71a40e09bf5ee909842127b6b00f5ab4ee3817599dc0771b79893/hf_xet-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:35b855024ca37f2dd113ac1c08993e997fbe167b9d61f9ef66d3d4f84015e508", size = 4394161, upload-time = "2026-02-27T17:25:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c3/859509bade9178e21b8b1db867b8e10e9f817ab9ac1de77cb9f461ced765/hf_xet-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:31612ba0629046e425ba50375685a2586e11fb9144270ebabd75878c3eaf6378", size = 3637377, upload-time = "2026-02-27T17:26:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/05/7f/724cfbef4da92d577b71f68bf832961c8919f36c60d28d289a9fc9d024d4/hf_xet-1.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:433c77c9f4e132b562f37d66c9b22c05b5479f243a1f06a120c1c06ce8b1502a", size = 3497875, upload-time = "2026-02-27T17:26:09.034Z" }, + { url = "https://files.pythonhosted.org/packages/ba/75/9d54c1ae1d05fb704f977eca1671747babf1957f19f38ae75c5933bc2dc1/hf_xet-1.3.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:c34e2c7aefad15792d57067c1c89b2b02c1bbaeabd7f8456ae3d07b4bbaf4094", size = 3761076, upload-time = "2026-02-27T17:25:55.42Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/08a24b6c6f52b5d26848c16e4b6d790bb810d1bf62c3505bed179f7032d3/hf_xet-1.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4bc995d6c41992831f762096020dc14a65fdf3963f86ffed580b596d04de32e3", size = 3521745, upload-time = "2026-02-27T17:25:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/b5/db/a75cf400dd8a1a8acf226a12955ff6ee999f272dfc0505bafd8079a61267/hf_xet-1.3.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:959083c89dee30f7d6f890b36cdadda823386c4de63b1a30384a75bfd2ae995d", size = 4176301, upload-time = "2026-02-27T17:25:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/01/40/6c4c798ffdd83e740dd3925c4e47793b07442a9efa3bc3866ba141a82365/hf_xet-1.3.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cfa760888633b08c01b398d212ce7e8c0d7adac6c86e4b20dfb2397d8acd78ee", size = 3955437, upload-time = "2026-02-27T17:25:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/0c/09/9a3aa7c5f07d3e5cc57bb750d12a124ffa72c273a87164bd848f9ac5cc14/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3155a02e083aa21fd733a7485c7c36025e49d5975c8d6bda0453d224dd0b0ac4", size = 4154535, upload-time = "2026-02-27T17:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e0/831f7fa6d90cb47a230bc23284b502c700e1483bbe459437b3844cdc0776/hf_xet-1.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91b1dc03c31cbf733d35dc03df7c5353686233d86af045e716f1e0ea4a2673cf", size = 4393891, upload-time = "2026-02-27T17:26:06.607Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/6ed472fdce7f8b70f5da6e3f05be76816a610063003bfd6d9cea0bbb58a3/hf_xet-1.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:211f30098512d95e85ad03ae63bd7dd2c4df476558a5095d09f9e38e78cbf674", size = 3637583, upload-time = "2026-02-27T17:26:17.349Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/a069edc4570b3f8e123c0b80fadc94530f3d7b01394e1fc1bb223339366c/hf_xet-1.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:4a6817c41de7c48ed9270da0b02849347e089c5ece9a0e72ae4f4b3a57617f82", size = 3497977, upload-time = "2026-02-27T17:26:14.966Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/dbb024e2e3907f6f3052847ca7d1a2f7a3972fafcd53ff79018977fcb3e4/hf_xet-1.3.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f93b7595f1d8fefddfede775c18b5c9256757824f7f6832930b49858483cd56f", size = 3763961, upload-time = "2026-02-27T17:25:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/e4/71/b99aed3823c9d1795e4865cf437d651097356a3f38c7d5877e4ac544b8e4/hf_xet-1.3.2-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a85d3d43743174393afe27835bde0cd146e652b5fcfdbcd624602daef2ef3259", size = 3526171, upload-time = "2026-02-27T17:25:50.968Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ca/907890ce6ef5598b5920514f255ed0a65f558f820515b18db75a51b2f878/hf_xet-1.3.2-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c2a054a97c44e136b1f7f5a78f12b3efffdf2eed3abc6746fc5ea4b39511633", size = 4180750, upload-time = "2026-02-27T17:25:43.125Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ad/bc7f41f87173d51d0bce497b171c4ee0cbde1eed2d7b4216db5d0ada9f50/hf_xet-1.3.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:06b724a361f670ae557836e57801b82c75b534812e351a87a2c739f77d1e0635", size = 3961035, upload-time = "2026-02-27T17:25:41.837Z" }, + { url = "https://files.pythonhosted.org/packages/73/38/600f4dda40c4a33133404d9fe644f1d35ff2d9babb4d0435c646c63dd107/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:305f5489d7241a47e0458ef49334be02411d1d0f480846363c1c8084ed9916f7", size = 4161378, upload-time = "2026-02-27T17:26:00.365Z" }, + { url = "https://files.pythonhosted.org/packages/00/b3/7bc1ff91d1ac18420b7ad1e169b618b27c00001b96310a89f8a9294fe509/hf_xet-1.3.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:06cdbde243c85f39a63b28e9034321399c507bcd5e7befdd17ed2ccc06dfe14e", size = 4398020, upload-time = "2026-02-27T17:26:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/99bfd948a3ed3620ab709276df3ad3710dcea61976918cce8706502927af/hf_xet-1.3.2-cp37-abi3-win_amd64.whl", hash = "sha256:9298b47cce6037b7045ae41482e703c471ce36b52e73e49f71226d2e8e5685a1", size = 3641624, upload-time = "2026-02-27T17:26:13.542Z" }, + { url = "https://files.pythonhosted.org/packages/cc/02/9a6e4ca1f3f73a164c0cd48e41b3cc56585dcc37e809250de443d673266f/hf_xet-1.3.2-cp37-abi3-win_arm64.whl", hash = "sha256:83d8ec273136171431833a6957e8f3af496bee227a0fe47c7b8b39c106d1749a", size = 3503976, upload-time = "2026-02-27T17:26:12.123Z" }, ] [[package]] name = "hpack" version = "4.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, ] [[package]] @@ -1725,9 +1725,9 @@ dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] @@ -1737,9 +1737,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800 } +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099 }, + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, ] [[package]] @@ -1752,9 +1752,9 @@ dependencies = [ { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [package.optional-dependencies] @@ -1777,27 +1777,27 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/76/b5efb3033d8499b17f9386beaf60f64c461798e1ee16d10bc9c0077beba5/huggingface_hub-1.5.0.tar.gz", hash = "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d", size = 695872 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/76/b5efb3033d8499b17f9386beaf60f64c461798e1ee16d10bc9c0077beba5/huggingface_hub-1.5.0.tar.gz", hash = "sha256:f281838db29265880fb543de7a23b0f81d3504675de82044307ea3c6c62f799d", size = 695872, upload-time = "2026-02-26T15:35:32.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/74/2bc951622e2dbba1af9a460d93c51d15e458becd486e62c29cc0ccb08178/huggingface_hub-1.5.0-py3-none-any.whl", hash = "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", size = 596261 }, + { url = "https://files.pythonhosted.org/packages/ec/74/2bc951622e2dbba1af9a460d93c51d15e458becd486e62c29cc0ccb08178/huggingface_hub-1.5.0-py3-none-any.whl", hash = "sha256:c9c0b3ab95a777fc91666111f3b3ede71c0cdced3614c553a64e98920585c4ee", size = 596261, upload-time = "2026-02-26T15:35:31.1Z" }, ] [[package]] name = "hyperframe" version = "6.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] [[package]] name = "idna" version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245 } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340 }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -1807,45 +1807,45 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304 } +sdist = { url = "https://files.pythonhosted.org/packages/cd/12/33e59336dca5be0c398a7482335911a33aa0e20776128f038019f1a95f1b/importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7", size = 55304, upload-time = "2024-09-11T14:56:08.937Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514 }, + { url = "https://files.pythonhosted.org/packages/a0/d9/a1e041c5e7caa9a05c925f4bdbdfb7f006d1f74996af53467bc394c97be7/importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", size = 26514, upload-time = "2024-09-11T14:56:07.019Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] name = "invoke" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762 } +sdist = { url = "https://files.pythonhosted.org/packages/de/bd/b461d3424a24c80490313fd77feeb666ca4f6a28c7e72713e3d9095719b4/invoke-2.2.1.tar.gz", hash = "sha256:515bf49b4a48932b79b024590348da22f39c4942dff991ad1fb8b8baea1be707", size = 304762, upload-time = "2025-10-11T00:36:35.172Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287 }, + { url = "https://files.pythonhosted.org/packages/32/4b/b99e37f88336009971405cbb7630610322ed6fbfa31e1d7ab3fbf3049a2d/invoke-2.2.1-py3-none-any.whl", hash = "sha256:2413bc441b376e5cd3f55bb5d364f973ad8bdd7bf87e53c79de3c11bf3feecc8", size = 160287, upload-time = "2025-10-11T00:36:33.703Z" }, ] [[package]] name = "isodate" version = "0.7.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] [[package]] name = "isort" version = "8.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592 } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733 }, + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, ] [[package]] @@ -1855,95 +1855,95 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] name = "jiter" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958 }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597 }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821 }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163 }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709 }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480 }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735 }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814 }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990 }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021 }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024 }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424 }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818 }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897 }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507 }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560 }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232 }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727 }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799 }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120 }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664 }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543 }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262 }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630 }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602 }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939 }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616 }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850 }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551 }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950 }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852 }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804 }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787 }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880 }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702 }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319 }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289 }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165 }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634 }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933 }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842 }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108 }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027 }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199 }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423 }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438 }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774 }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238 }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892 }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309 }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607 }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986 }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756 }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196 }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215 }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152 }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169 }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808 }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384 }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768 }, +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, ] [[package]] name = "jmespath" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419 }, + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] [[package]] name = "joblib" version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] @@ -1956,9 +1956,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -1968,9 +1968,9 @@ 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 } +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 }, + { 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]] @@ -1983,69 +1983,69 @@ dependencies = [ { name = "tzdata" }, { name = "vine" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d3/5ff936d8319ac86b9c409f1501b07c426e6ad41966fedace9ef1b966e23f/kombu-5.5.4.tar.gz", hash = "sha256:886600168275ebeada93b888e831352fe578168342f0d1d5833d88ba0d847363", size = 461992, upload-time = "2025-06-01T10:19:22.281Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034 }, + { url = "https://files.pythonhosted.org/packages/ef/70/a07dcf4f62598c8ad579df241af55ced65bed76e42e45d3c368a6d82dbc1/kombu-5.5.4-py3-none-any.whl", hash = "sha256:a12ed0557c238897d8e518f1d1fdf84bd1516c5e305af2dacd85c2015115feb8", size = 210034, upload-time = "2025-06-01T10:19:20.436Z" }, ] [[package]] name = "librt" version = "0.8.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516 }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634 }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941 }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991 }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476 }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518 }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116 }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751 }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378 }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199 }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917 }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017 }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441 }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529 }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669 }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279 }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288 }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809 }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075 }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486 }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219 }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750 }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624 }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969 }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000 }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495 }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081 }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309 }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804 }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907 }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217 }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622 }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987 }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132 }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195 }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946 }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689 }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875 }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058 }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313 }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994 }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770 }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409 }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473 }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866 }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248 }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629 }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615 }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001 }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328 }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722 }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755 }, +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, ] [[package]] @@ -2067,15 +2067,15 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/fe/d03be8a6be6914fabacef29457a2b3d02128c52c448cc6f3019b8e63488b/litellm-1.96.2.tar.gz", hash = "sha256:80d477ae092b05ce023b5084542cb3cb75999b52b1dd2e4d834fb348effc9399", size = 17682558 } +sdist = { url = "https://files.pythonhosted.org/packages/79/fe/d03be8a6be6914fabacef29457a2b3d02128c52c448cc6f3019b8e63488b/litellm-1.96.2.tar.gz", hash = "sha256:80d477ae092b05ce023b5084542cb3cb75999b52b1dd2e4d834fb348effc9399", size = 17682558, upload-time = "2026-08-11T21:12:24.117Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/85/653d0095a163cc9a1f2f1aa629cf4493c57c3abe13bd806290fc5d57458b/litellm-1.96.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0aa667c6fc58b20ff04fe5efaac5cf48b1e224bac32dbf22ced2e5ee695bc4ba", size = 24102871 }, - { url = "https://files.pythonhosted.org/packages/29/a1/1322630271fbb291e97b54eb46baa8c4f512130a620a8428c7460b4ef414/litellm-1.96.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e5d96d16b37a043e482a134a71b0c1df4e700603fc14df22e4bd27dc9e38f5c4", size = 23760722 }, - { url = "https://files.pythonhosted.org/packages/17/0e/e0e2734194072a438298e94dbc3c37879d335fdf50881ce8995daa6878d1/litellm-1.96.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1a42d68b903f6b605bd7744e9700b16a725f897c5edcc448e491cec14cd5a9db", size = 23898969 }, - { url = "https://files.pythonhosted.org/packages/b5/84/993c40c116c699383741fd27da515c71b395609859148ea32720925eaeaf/litellm-1.96.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76e9c72cb6757bb7c35cd23e3e4fc028910795910ae92491076bd50f7b61f205", size = 24264588 }, - { url = "https://files.pythonhosted.org/packages/d8/02/21dc260ae3fd72b23544abce23ad5f3d563e3d39a4f617bba6d98f4b9c31/litellm-1.96.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1f304b8854e385946469d2dcf8ef88e24357120671caeaddea11b085b0302308", size = 23973160 }, - { url = "https://files.pythonhosted.org/packages/64/5d/e79f86b43f07aff7f796faf3c6d1025f6ab9da43f72b7c555a26811af191/litellm-1.96.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8c21a57a0f3507176492a4f65361a4302af1a0723c97e96961c7ed0e96934832", size = 24360615 }, - { url = "https://files.pythonhosted.org/packages/3e/6a/c2e3058fd3766eebe5685c688d9e85adb9749e946580f46214a651661522/litellm-1.96.2-cp310-abi3-win_amd64.whl", hash = "sha256:0168e49cffecb0b45a0d044ecb7e5f7f6fc9e14d974b50f54edfa9d8e0db9c1d", size = 24154302 }, + { url = "https://files.pythonhosted.org/packages/46/85/653d0095a163cc9a1f2f1aa629cf4493c57c3abe13bd806290fc5d57458b/litellm-1.96.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0aa667c6fc58b20ff04fe5efaac5cf48b1e224bac32dbf22ced2e5ee695bc4ba", size = 24102871, upload-time = "2026-08-11T21:12:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/29/a1/1322630271fbb291e97b54eb46baa8c4f512130a620a8428c7460b4ef414/litellm-1.96.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e5d96d16b37a043e482a134a71b0c1df4e700603fc14df22e4bd27dc9e38f5c4", size = 23760722, upload-time = "2026-08-11T21:12:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/e0e2734194072a438298e94dbc3c37879d335fdf50881ce8995daa6878d1/litellm-1.96.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1a42d68b903f6b605bd7744e9700b16a725f897c5edcc448e491cec14cd5a9db", size = 23898969, upload-time = "2026-08-11T21:12:11.68Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/993c40c116c699383741fd27da515c71b395609859148ea32720925eaeaf/litellm-1.96.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76e9c72cb6757bb7c35cd23e3e4fc028910795910ae92491076bd50f7b61f205", size = 24264588, upload-time = "2026-08-11T21:12:14.18Z" }, + { url = "https://files.pythonhosted.org/packages/d8/02/21dc260ae3fd72b23544abce23ad5f3d563e3d39a4f617bba6d98f4b9c31/litellm-1.96.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1f304b8854e385946469d2dcf8ef88e24357120671caeaddea11b085b0302308", size = 23973160, upload-time = "2026-08-11T21:12:16.616Z" }, + { url = "https://files.pythonhosted.org/packages/64/5d/e79f86b43f07aff7f796faf3c6d1025f6ab9da43f72b7c555a26811af191/litellm-1.96.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8c21a57a0f3507176492a4f65361a4302af1a0723c97e96961c7ed0e96934832", size = 24360615, upload-time = "2026-08-11T21:12:18.947Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6a/c2e3058fd3766eebe5685c688d9e85adb9749e946580f46214a651661522/litellm-1.96.2-cp310-abi3-win_amd64.whl", hash = "sha256:0168e49cffecb0b45a0d044ecb7e5f7f6fc9e14d974b50f54edfa9d8e0db9c1d", size = 24154302, upload-time = "2026-08-11T21:12:21.221Z" }, ] [[package]] @@ -2087,9 +2087,9 @@ dependencies = [ { name = "httpx" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9b/72/816e6e900448e1b4a8137d90e65876b296c5264a23db6ae888bd3e6660ba/llama_cloud-0.1.35.tar.gz", hash = "sha256:200349d5d57424d7461f304cdb1355a58eea3e6ca1e6b0d75c66b2e937216983", size = 106403 } +sdist = { url = "https://files.pythonhosted.org/packages/9b/72/816e6e900448e1b4a8137d90e65876b296c5264a23db6ae888bd3e6660ba/llama_cloud-0.1.35.tar.gz", hash = "sha256:200349d5d57424d7461f304cdb1355a58eea3e6ca1e6b0d75c66b2e937216983", size = 106403, upload-time = "2025-07-28T17:22:06.41Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/d2/8d18a021ab757cea231428404f21fe3186bf1ebaac3f57a73c379483fd3f/llama_cloud-0.1.35-py3-none-any.whl", hash = "sha256:b7abab4423118e6f638d2f326749e7a07c6426543bea6da99b623c715b22af71", size = 303280 }, + { url = "https://files.pythonhosted.org/packages/1d/d2/8d18a021ab757cea231428404f21fe3186bf1ebaac3f57a73c379483fd3f/llama_cloud-0.1.35-py3-none-any.whl", hash = "sha256:b7abab4423118e6f638d2f326749e7a07c6426543bea6da99b623c715b22af71", size = 303280, upload-time = "2025-07-28T17:22:04.946Z" }, ] [[package]] @@ -2105,9 +2105,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/0c/8ca87d33bea0340a8ed791f36390112aeb29fd3eebfd64b6aef6204a03f0/llama_cloud_services-0.6.54.tar.gz", hash = "sha256:baf65d9bffb68f9dca98ac6e22908b6675b2038b021e657ead1ffc0e43cbd45d", size = 53468 } +sdist = { url = "https://files.pythonhosted.org/packages/8a/0c/8ca87d33bea0340a8ed791f36390112aeb29fd3eebfd64b6aef6204a03f0/llama_cloud_services-0.6.54.tar.gz", hash = "sha256:baf65d9bffb68f9dca98ac6e22908b6675b2038b021e657ead1ffc0e43cbd45d", size = 53468, upload-time = "2025-08-01T20:09:20.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/48/4e295e3f791b279885a2e584f71e75cbe4ac84e93bba3c36e2668f60a8ac/llama_cloud_services-0.6.54-py3-none-any.whl", hash = "sha256:07f595f7a0ba40c6a1a20543d63024ca7600fe65c4811d1951039977908997be", size = 63874 }, + { url = "https://files.pythonhosted.org/packages/7f/48/4e295e3f791b279885a2e584f71e75cbe4ac84e93bba3c36e2668f60a8ac/llama_cloud_services-0.6.54-py3-none-any.whl", hash = "sha256:07f595f7a0ba40c6a1a20543d63024ca7600fe65c4811d1951039977908997be", size = 63874, upload-time = "2025-08-01T20:09:20.076Z" }, ] [[package]] @@ -2124,9 +2124,9 @@ dependencies = [ { name = "llama-index-readers-llama-parse" }, { name = "nltk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/25/d74e56acf693c608bfa2269adfd5a58128973aaa7fd1e77ccf9d5f616f32/llama_index-0.14.15.tar.gz", hash = "sha256:079f65e72af87c72dd8b516aa2dd520b52eb2128722d66ecce1e5148cee357c0", size = 8472 } +sdist = { url = "https://files.pythonhosted.org/packages/20/25/d74e56acf693c608bfa2269adfd5a58128973aaa7fd1e77ccf9d5f616f32/llama_index-0.14.15.tar.gz", hash = "sha256:079f65e72af87c72dd8b516aa2dd520b52eb2128722d66ecce1e5148cee357c0", size = 8472, upload-time = "2026-02-18T19:06:38.527Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/94/b338e8985313e6e3a5321638f3d7d457310da6cb4ab1298eea3b323cb06c/llama_index-0.14.15-py3-none-any.whl", hash = "sha256:469bf8ff77a445dbf402ed08978a0c8ebf59d40fcd15d289e07e5791e0513cea", size = 7264 }, + { url = "https://files.pythonhosted.org/packages/02/94/b338e8985313e6e3a5321638f3d7d457310da6cb4ab1298eea3b323cb06c/llama_index-0.14.15-py3-none-any.whl", hash = "sha256:469bf8ff77a445dbf402ed08978a0c8ebf59d40fcd15d289e07e5791e0513cea", size = 7264, upload-time = "2026-02-18T19:06:39.54Z" }, ] [[package]] @@ -2138,9 +2138,9 @@ dependencies = [ { name = "llama-index-embeddings-openai" }, { name = "llama-index-llms-openai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/84/41e820efffbe327c38228d3b37fe42512a37e0c3ee4ff6bf97a394e9577a/llama_index_cli-0.5.3.tar.gz", hash = "sha256:ebaf39e785efbfa8d50d837f60cb0f95125c04bf73ed1f92092a2a5f506172f8", size = 24821 } +sdist = { url = "https://files.pythonhosted.org/packages/67/84/41e820efffbe327c38228d3b37fe42512a37e0c3ee4ff6bf97a394e9577a/llama_index_cli-0.5.3.tar.gz", hash = "sha256:ebaf39e785efbfa8d50d837f60cb0f95125c04bf73ed1f92092a2a5f506172f8", size = 24821, upload-time = "2025-09-29T18:03:10.798Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/81/b7b3778aa8662913760fbbee77578daf4407aeaa677ccbf0125c4cfa2e67/llama_index_cli-0.5.3-py3-none-any.whl", hash = "sha256:7deb1e953e582bd885443881ce8bd6ab2817b594fef00079dce9993c47d990f7", size = 28173 }, + { url = "https://files.pythonhosted.org/packages/54/81/b7b3778aa8662913760fbbee77578daf4407aeaa677ccbf0125c4cfa2e67/llama_index_cli-0.5.3-py3-none-any.whl", hash = "sha256:7deb1e953e582bd885443881ce8bd6ab2817b594fef00079dce9993c47d990f7", size = 28173, upload-time = "2025-09-29T18:03:10.024Z" }, ] [[package]] @@ -2177,9 +2177,9 @@ dependencies = [ { name = "typing-inspect" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/4f/7c714bdf94dd229707b43e7f8cedf3aed0a99938fd46a9ad8a418c199988/llama_index_core-0.14.15.tar.gz", hash = "sha256:3766aeeb95921b3a2af8c2a51d844f75f404215336e1639098e3652db52c68ce", size = 11593505 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/4f/7c714bdf94dd229707b43e7f8cedf3aed0a99938fd46a9ad8a418c199988/llama_index_core-0.14.15.tar.gz", hash = "sha256:3766aeeb95921b3a2af8c2a51d844f75f404215336e1639098e3652db52c68ce", size = 11593505, upload-time = "2026-02-18T19:05:48.274Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/9e/262f6465ee4fffa40698b3cc2177e377ce7d945d3bd8b7d9c6b09448625d/llama_index_core-0.14.15-py3-none-any.whl", hash = "sha256:e02b321c10673871a38aaefdc4a93d5ae8ec324cad4408683189e5a1aa1e3d52", size = 11937002 }, + { url = "https://files.pythonhosted.org/packages/41/9e/262f6465ee4fffa40698b3cc2177e377ce7d945d3bd8b7d9c6b09448625d/llama_index_core-0.14.15-py3-none-any.whl", hash = "sha256:e02b321c10673871a38aaefdc4a93d5ae8ec324cad4408683189e5a1aa1e3d52", size = 11937002, upload-time = "2026-02-18T19:05:45.855Z" }, ] [[package]] @@ -2190,9 +2190,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "openai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/36/90336d054a5061a3f5bc17ac2c18ef63d9d84c55c14d557de484e811ea4d/llama_index_embeddings_openai-0.5.1.tar.gz", hash = "sha256:1c89867a48b0d0daa3d2d44f5e76b394b2b2ef9935932daf921b9e77939ccda8", size = 7020 } +sdist = { url = "https://files.pythonhosted.org/packages/10/36/90336d054a5061a3f5bc17ac2c18ef63d9d84c55c14d557de484e811ea4d/llama_index_embeddings_openai-0.5.1.tar.gz", hash = "sha256:1c89867a48b0d0daa3d2d44f5e76b394b2b2ef9935932daf921b9e77939ccda8", size = 7020, upload-time = "2025-09-08T20:17:44.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/4a/8ab11026cf8deff8f555aa73919be0bac48332683111e5fc4290f352dc50/llama_index_embeddings_openai-0.5.1-py3-none-any.whl", hash = "sha256:a2fcda3398bbd987b5ce3f02367caee8e84a56b930fdf43cc1d059aa9fd20ca5", size = 7011 }, + { url = "https://files.pythonhosted.org/packages/23/4a/8ab11026cf8deff8f555aa73919be0bac48332683111e5fc4290f352dc50/llama_index_embeddings_openai-0.5.1-py3-none-any.whl", hash = "sha256:a2fcda3398bbd987b5ce3f02367caee8e84a56b930fdf43cc1d059aa9fd20ca5", size = 7011, upload-time = "2025-09-08T20:17:44.015Z" }, ] [[package]] @@ -2204,9 +2204,9 @@ dependencies = [ { name = "llama-cloud" }, { name = "llama-index-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/4a/79044fcb3209583d1ffe0c2a7c19dddfb657a03faeb9fe0cf5a74027e646/llama_index_indices_managed_llama_cloud-0.9.4.tar.gz", hash = "sha256:b5e00752ab30564abf19c57595a2107f5697c3b03b085817b4fca84a38ebbd59", size = 15146 } +sdist = { url = "https://files.pythonhosted.org/packages/61/4a/79044fcb3209583d1ffe0c2a7c19dddfb657a03faeb9fe0cf5a74027e646/llama_index_indices_managed_llama_cloud-0.9.4.tar.gz", hash = "sha256:b5e00752ab30564abf19c57595a2107f5697c3b03b085817b4fca84a38ebbd59", size = 15146, upload-time = "2025-09-08T20:29:58.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/6a/0e33245df06afc9766c46a1fe92687be8a09da5d0d0128bc08d84a9f5efa/llama_index_indices_managed_llama_cloud-0.9.4-py3-none-any.whl", hash = "sha256:535a08811046803ca6ab7f8e9d510e926aa5306608b02201ad3d9d21701383bc", size = 17005 }, + { url = "https://files.pythonhosted.org/packages/a6/6a/0e33245df06afc9766c46a1fe92687be8a09da5d0d0128bc08d84a9f5efa/llama_index_indices_managed_llama_cloud-0.9.4-py3-none-any.whl", hash = "sha256:535a08811046803ca6ab7f8e9d510e926aa5306608b02201ad3d9d21701383bc", size = 17005, upload-time = "2025-09-08T20:29:57.876Z" }, ] [[package]] @@ -2217,9 +2217,9 @@ dependencies = [ { name = "deprecated" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/b9/a7a74de6d8aacf4be329329495983d78d96b1a6e69b6d9fcf4a233febd4b/llama_index_instrumentation-0.4.2.tar.gz", hash = "sha256:dc4957b64da0922060690e85a6be9698ac08e34e0f69e90b01364ddec4f3de7f", size = 46146 } +sdist = { url = "https://files.pythonhosted.org/packages/af/b9/a7a74de6d8aacf4be329329495983d78d96b1a6e69b6d9fcf4a233febd4b/llama_index_instrumentation-0.4.2.tar.gz", hash = "sha256:dc4957b64da0922060690e85a6be9698ac08e34e0f69e90b01364ddec4f3de7f", size = 46146, upload-time = "2025-10-13T20:44:48.85Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/54/df8063b0441242e250e03d1e31ebde5dffbe24e1af32b025cb1a4544150c/llama_index_instrumentation-0.4.2-py3-none-any.whl", hash = "sha256:b4989500e6454059ab3f3c4a193575d47ab1fadb730c2e8f2b962649ae88b70b", size = 15411 }, + { url = "https://files.pythonhosted.org/packages/40/54/df8063b0441242e250e03d1e31ebde5dffbe24e1af32b025cb1a4544150c/llama_index_instrumentation-0.4.2-py3-none-any.whl", hash = "sha256:b4989500e6454059ab3f3c4a193575d47ab1fadb730c2e8f2b962649ae88b70b", size = 15411, upload-time = "2025-10-13T20:44:47.685Z" }, ] [[package]] @@ -2230,9 +2230,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "openai" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/d4/cb0ab1aa47d00fc251fad4dfe30093fc964dba9b168a88aaacd0411b5a42/llama_index_llms_openai-0.6.23.tar.gz", hash = "sha256:4208404bbf6c5a18bed2079df9333f4ba931e5b8c6c6a407abe2c47c14ac7b7e", size = 26595 } +sdist = { url = "https://files.pythonhosted.org/packages/99/d4/cb0ab1aa47d00fc251fad4dfe30093fc964dba9b168a88aaacd0411b5a42/llama_index_llms_openai-0.6.23.tar.gz", hash = "sha256:4208404bbf6c5a18bed2079df9333f4ba931e5b8c6c6a407abe2c47c14ac7b7e", size = 26595, upload-time = "2026-03-01T17:51:14.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/4e/760a0cde3ffb5f4a85660441c01b80d67e579edc99b69fe4eb20799642e2/llama_index_llms_openai-0.6.23-py3-none-any.whl", hash = "sha256:48c1bb2d0adaca7bb7477c3f84c4c70f6f5c2ced942ad02773e70fa035b5b323", size = 27618 }, + { url = "https://files.pythonhosted.org/packages/55/4e/760a0cde3ffb5f4a85660441c01b80d67e579edc99b69fe4eb20799642e2/llama_index_llms_openai-0.6.23-py3-none-any.whl", hash = "sha256:48c1bb2d0adaca7bb7477c3f84c4c70f6f5c2ced942ad02773e70fa035b5b323", size = 27618, upload-time = "2026-03-01T17:51:16.452Z" }, ] [[package]] @@ -2247,9 +2247,9 @@ dependencies = [ { name = "pypdf" }, { name = "striprtf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/e5/dccfb495dbc40f50fcfb799db2287ac5dca4a16a3b09bae61a4ccb1788d3/llama_index_readers_file-0.5.6.tar.gz", hash = "sha256:1c08b14facc2dfe933622aaa26dc7d2a7a6023c42d3db896a2c948789edaf1ea", size = 32535 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/e5/dccfb495dbc40f50fcfb799db2287ac5dca4a16a3b09bae61a4ccb1788d3/llama_index_readers_file-0.5.6.tar.gz", hash = "sha256:1c08b14facc2dfe933622aaa26dc7d2a7a6023c42d3db896a2c948789edaf1ea", size = 32535, upload-time = "2025-12-24T16:04:16.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/c3/8d28eaa962e073e6735d80847dda9fd3525cb9ff5974ae82dd20621a5a02/llama_index_readers_file-0.5.6-py3-none-any.whl", hash = "sha256:32e83f9adb4e4803e6c7cef746c44fa0949013b1cb76f06f422e9491d198dbda", size = 51832 }, + { url = "https://files.pythonhosted.org/packages/fe/c3/8d28eaa962e073e6735d80847dda9fd3525cb9ff5974ae82dd20621a5a02/llama_index_readers_file-0.5.6-py3-none-any.whl", hash = "sha256:32e83f9adb4e4803e6c7cef746c44fa0949013b1cb76f06f422e9491d198dbda", size = 51832, upload-time = "2025-12-24T16:04:17.307Z" }, ] [[package]] @@ -2260,9 +2260,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "llama-parse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/77/5bfaab20e6ec8428dbf2352e18be550c957602723d69383908176b5686cd/llama_index_readers_llama_parse-0.5.1.tar.gz", hash = "sha256:2b78b73faa933e30e6c69df351e4e9f36dfe2ae142e2ab3969ddd2ac48930e37", size = 3858 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/77/5bfaab20e6ec8428dbf2352e18be550c957602723d69383908176b5686cd/llama_index_readers_llama_parse-0.5.1.tar.gz", hash = "sha256:2b78b73faa933e30e6c69df351e4e9f36dfe2ae142e2ab3969ddd2ac48930e37", size = 3858, upload-time = "2025-09-08T20:41:29.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/81/52410c7245dcbf1a54756a9ce3892cdd167ec0b884d696de1304ca3f452e/llama_index_readers_llama_parse-0.5.1-py3-none-any.whl", hash = "sha256:0d41450ed29b0c49c024e206ef6c8e662b1854e77a1c5faefed3b958be54f880", size = 3203 }, + { url = "https://files.pythonhosted.org/packages/68/81/52410c7245dcbf1a54756a9ce3892cdd167ec0b884d696de1304ca3f452e/llama_index_readers_llama_parse-0.5.1-py3-none-any.whl", hash = "sha256:0d41450ed29b0c49c024e206ef6c8e662b1854e77a1c5faefed3b958be54f880", size = 3203, upload-time = "2025-09-08T20:41:28.438Z" }, ] [[package]] @@ -2273,9 +2273,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "pymilvus" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/79/72bf70fac3b77770a5c2b1b7d441aa1998bb522d50fe88b0f9c4071854e5/llama_index_vector_stores_milvus-0.9.6.tar.gz", hash = "sha256:6d38ac5939a570e0240687f54fbee4e1ff6c5faa2d28d25377a3f38d2ca07e2b", size = 15584 } +sdist = { url = "https://files.pythonhosted.org/packages/ef/79/72bf70fac3b77770a5c2b1b7d441aa1998bb522d50fe88b0f9c4071854e5/llama_index_vector_stores_milvus-0.9.6.tar.gz", hash = "sha256:6d38ac5939a570e0240687f54fbee4e1ff6c5faa2d28d25377a3f38d2ca07e2b", size = 15584, upload-time = "2026-01-13T11:46:41.394Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/04/98359849c095b5a3eb06f0649865a30b5a733b9fdbfeac3c6951253f804c/llama_index_vector_stores_milvus-0.9.6-py3-none-any.whl", hash = "sha256:916cbd9b07035ec137905970ef6a49dd77d3ece6e0a79271db35705cca5f5f84", size = 15792 }, + { url = "https://files.pythonhosted.org/packages/51/04/98359849c095b5a3eb06f0649865a30b5a733b9fdbfeac3c6951253f804c/llama_index_vector_stores_milvus-0.9.6-py3-none-any.whl", hash = "sha256:916cbd9b07035ec137905970ef6a49dd77d3ece6e0a79271db35705cca5f5f84", size = 15792, upload-time = "2026-01-13T11:46:39.708Z" }, ] [[package]] @@ -2286,9 +2286,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "pinecone" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/a0/2e2e969a133894f10b3a55b5148feef0c546ca8047b461f51f79d115c5b9/llama_index_vector_stores_pinecone-0.7.1.tar.gz", hash = "sha256:0ab3cc44f309bca1d74e58f221dade672169da01561114b067f4734293bd0280", size = 7852 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/a0/2e2e969a133894f10b3a55b5148feef0c546ca8047b461f51f79d115c5b9/llama_index_vector_stores_pinecone-0.7.1.tar.gz", hash = "sha256:0ab3cc44f309bca1d74e58f221dade672169da01561114b067f4734293bd0280", size = 7852, upload-time = "2025-09-08T20:28:54.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/22/ae8c3073e4866a41eb53030db7cfecca1d84192c16a67d40a76f8d593e6d/llama_index_vector_stores_pinecone-0.7.1-py3-none-any.whl", hash = "sha256:861c4d01b3766cdca318f1285c03cd5e52dabf3d2f136cb38db421b16103129a", size = 8041 }, + { url = "https://files.pythonhosted.org/packages/93/22/ae8c3073e4866a41eb53030db7cfecca1d84192c16a67d40a76f8d593e6d/llama_index_vector_stores_pinecone-0.7.1-py3-none-any.whl", hash = "sha256:861c4d01b3766cdca318f1285c03cd5e52dabf3d2f136cb38db421b16103129a", size = 8041, upload-time = "2025-09-08T20:28:53.406Z" }, ] [[package]] @@ -2302,9 +2302,9 @@ dependencies = [ { name = "psycopg2-binary" }, { name = "sqlalchemy", extra = ["asyncio"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/78/04ff0cb9e14b8c1c3cb8716fab35c95ec2a4b551d769c65031c5c8624337/llama_index_vector_stores_postgres-0.7.3.tar.gz", hash = "sha256:7b5c62e462d681d7b8d8668b93e5b0023bfd3aaafcf76e2b4bfcf885dc3b49c6", size = 11950 } +sdist = { url = "https://files.pythonhosted.org/packages/ef/78/04ff0cb9e14b8c1c3cb8716fab35c95ec2a4b551d769c65031c5c8624337/llama_index_vector_stores_postgres-0.7.3.tar.gz", hash = "sha256:7b5c62e462d681d7b8d8668b93e5b0023bfd3aaafcf76e2b4bfcf885dc3b49c6", size = 11950, upload-time = "2026-01-22T15:14:13.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/de/140f678d930ea869fc989adaa0140b9267c5ee0c7b971d061112d0d5b75a/llama_index_vector_stores_postgres-0.7.3-py3-none-any.whl", hash = "sha256:65b70266cc6041ab5011d64d1183d8783112ba5b38eb32ca21e00ea5b96aa058", size = 11635 }, + { url = "https://files.pythonhosted.org/packages/cc/de/140f678d930ea869fc989adaa0140b9267c5ee0c7b971d061112d0d5b75a/llama_index_vector_stores_postgres-0.7.3-py3-none-any.whl", hash = "sha256:65b70266cc6041ab5011d64d1183d8783112ba5b38eb32ca21e00ea5b96aa058", size = 11635, upload-time = "2026-01-22T15:14:13.722Z" }, ] [[package]] @@ -2316,9 +2316,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "qdrant-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/63/646ec9d7035429d4fb5488146ab7a9d55e955fb855f0a7e7fc29f6eb136f/llama_index_vector_stores_qdrant-0.9.1.tar.gz", hash = "sha256:215e24278bde44c6746d60c7df3f8811f943c20524a496e0c954eeb6449e8319", size = 14688 } +sdist = { url = "https://files.pythonhosted.org/packages/99/63/646ec9d7035429d4fb5488146ab7a9d55e955fb855f0a7e7fc29f6eb136f/llama_index_vector_stores_qdrant-0.9.1.tar.gz", hash = "sha256:215e24278bde44c6746d60c7df3f8811f943c20524a496e0c954eeb6449e8319", size = 14688, upload-time = "2026-01-13T11:13:07.123Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/d2/e258a2b5526fe42d6ca60a054b3830948d6411e3f075de542ba492c5f5d1/llama_index_vector_stores_qdrant-0.9.1-py3-none-any.whl", hash = "sha256:de71d0e867c04c87aa81ab35b092c32d684961c26a1cda601856faf29b21a598", size = 14944 }, + { url = "https://files.pythonhosted.org/packages/1d/d2/e258a2b5526fe42d6ca60a054b3830948d6411e3f075de542ba492c5f5d1/llama_index_vector_stores_qdrant-0.9.1-py3-none-any.whl", hash = "sha256:de71d0e867c04c87aa81ab35b092c32d684961c26a1cda601856faf29b21a598", size = 14944, upload-time = "2026-01-13T11:13:08.08Z" }, ] [[package]] @@ -2329,9 +2329,9 @@ dependencies = [ { name = "llama-index-core" }, { name = "weaviate-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/9c/fd6eae66b87e736807776c6824bebebb44098630af2dac8cd5cdf5938d0d/llama_index_vector_stores_weaviate-1.5.0.tar.gz", hash = "sha256:99ba6dbdcf92e9ec56f464de2d71ed3c0503e3fc5b71f9d74dbc32da981b0cf5", size = 9679 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/9c/fd6eae66b87e736807776c6824bebebb44098630af2dac8cd5cdf5938d0d/llama_index_vector_stores_weaviate-1.5.0.tar.gz", hash = "sha256:99ba6dbdcf92e9ec56f464de2d71ed3c0503e3fc5b71f9d74dbc32da981b0cf5", size = 9679, upload-time = "2026-02-20T23:20:19.83Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/dd/09efa4551016a9e95d53dbb3cd7ae7a9bf3fd07be1a3e1ac3eada7b4c5e5/llama_index_vector_stores_weaviate-1.5.0-py3-none-any.whl", hash = "sha256:8e24d920a0cc241dcf0cdbfe29541aeca6a9f8cf29606ed90325978f972636a5", size = 10439 }, + { url = "https://files.pythonhosted.org/packages/22/dd/09efa4551016a9e95d53dbb3cd7ae7a9bf3fd07be1a3e1ac3eada7b4c5e5/llama_index_vector_stores_weaviate-1.5.0-py3-none-any.whl", hash = "sha256:8e24d920a0cc241dcf0cdbfe29541aeca6a9f8cf29606ed90325978f972636a5", size = 10439, upload-time = "2026-02-20T23:20:19.057Z" }, ] [[package]] @@ -2343,9 +2343,9 @@ dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/21/0357ededbc3aead4d170ba5a7b33345ee45e07f77423367725ef623c08a6/llama_index_workflows-2.15.0.tar.gz", hash = "sha256:06630ca3887b9bf27da776f10e42811aeedf3534591eb654796319ef898d2d7b", size = 81504 } +sdist = { url = "https://files.pythonhosted.org/packages/bb/21/0357ededbc3aead4d170ba5a7b33345ee45e07f77423367725ef623c08a6/llama_index_workflows-2.15.0.tar.gz", hash = "sha256:06630ca3887b9bf27da776f10e42811aeedf3534591eb654796319ef898d2d7b", size = 81504, upload-time = "2026-02-28T00:02:28.904Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/43/ae0902d0ec6d75b6724f8db8de7cebaac7b2e7d285d31ac5f58279973d9b/llama_index_workflows-2.15.0-py3-none-any.whl", hash = "sha256:31634e32dcc2ec248b8c4a03d54c98a8e87cefce641edd573bdbd548d89f3c0c", size = 104367 }, + { url = "https://files.pythonhosted.org/packages/b3/43/ae0902d0ec6d75b6724f8db8de7cebaac7b2e7d285d31ac5f58279973d9b/llama_index_workflows-2.15.0-py3-none-any.whl", hash = "sha256:31634e32dcc2ec248b8c4a03d54c98a8e87cefce641edd573bdbd548d89f3c0c", size = 104367, upload-time = "2026-02-28T00:02:27.074Z" }, ] [[package]] @@ -2355,9 +2355,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "llama-cloud-services" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/08/f6/93b5d123c480bc8c93e6dc3ea930f4f8df8da27f829bb011100ba3ce23dc/llama_parse-0.6.54.tar.gz", hash = "sha256:c707b31152155c9bae84e316fab790bbc8c85f4d8825ce5ee386ebeb7db258f1", size = 3577 } +sdist = { url = "https://files.pythonhosted.org/packages/08/f6/93b5d123c480bc8c93e6dc3ea930f4f8df8da27f829bb011100ba3ce23dc/llama_parse-0.6.54.tar.gz", hash = "sha256:c707b31152155c9bae84e316fab790bbc8c85f4d8825ce5ee386ebeb7db258f1", size = 3577, upload-time = "2025-08-01T20:09:23.762Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/50/c5ccd2a50daa0a10c7f3f7d4e6992392454198cd8a7d99fcb96cb60d0686/llama_parse-0.6.54-py3-none-any.whl", hash = "sha256:c66c8d51cf6f29a44eaa8595a595de5d2598afc86e5a33a4cebe5fe228036920", size = 4879 }, + { url = "https://files.pythonhosted.org/packages/05/50/c5ccd2a50daa0a10c7f3f7d4e6992392454198cd8a7d99fcb96cb60d0686/llama_parse-0.6.54-py3-none-any.whl", hash = "sha256:c66c8d51cf6f29a44eaa8595a595de5d2598afc86e5a33a4cebe5fe228036920", size = 4879, upload-time = "2025-08-01T20:09:22.651Z" }, ] [[package]] @@ -2368,9 +2368,9 @@ dependencies = [ { name = "requests" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/7a/d008832e730fe64fe3a24a1a1fcc6b6548130abd987fb6576ba60ab836f4/llmwhisperer_client-2.8.1.tar.gz", hash = "sha256:9e5ee18bf79c6b3a2ea598a4a9d80340ad6d28743fe96b0d3bae7a1b8c9b9c22", size = 3270865 } +sdist = { url = "https://files.pythonhosted.org/packages/34/7a/d008832e730fe64fe3a24a1a1fcc6b6548130abd987fb6576ba60ab836f4/llmwhisperer_client-2.8.1.tar.gz", hash = "sha256:9e5ee18bf79c6b3a2ea598a4a9d80340ad6d28743fe96b0d3bae7a1b8c9b9c22", size = 3270865, upload-time = "2026-08-18T11:48:52.316Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/e7/8e22bb7ef903b1168d0e06542e558a4f818bb0f4966dd5740e1e5358396e/llmwhisperer_client-2.8.1-py3-none-any.whl", hash = "sha256:3f261a3d343088873c4b5e34ffc016e6c53936c09fbcf8d78318f9733174ecdd", size = 12076 }, + { url = "https://files.pythonhosted.org/packages/f1/e7/8e22bb7ef903b1168d0e06542e558a4f818bb0f4966dd5740e1e5358396e/llmwhisperer_client-2.8.1-py3-none-any.whl", hash = "sha256:3f261a3d343088873c4b5e34ffc016e6c53936c09fbcf8d78318f9733174ecdd", size = 12076, upload-time = "2026-08-18T11:48:50.794Z" }, ] [[package]] @@ -2380,72 +2380,72 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] [[package]] @@ -2455,27 +2455,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095 } +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964 }, + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, ] [[package]] name = "mccabe" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -2486,10 +2486,10 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b2/acc5024c8e8b6a0b034670b8e8af306ebd633ede777dcbf557eac4785937/milvus_lite-2.5.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6b014453200ba977be37ba660cb2d021030375fa6a35bc53c2e1d92980a0c512", size = 27934713 }, - { url = "https://files.pythonhosted.org/packages/9b/2e/746f5bb1d6facd1e73eb4af6dd5efda11125b0f29d7908a097485ca6cad9/milvus_lite-2.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2e031088bf308afe5f8567850412d618cfb05a65238ed1a6117f60decccc95a", size = 24421451 }, - { url = "https://files.pythonhosted.org/packages/2e/cf/3d1fee5c16c7661cf53977067a34820f7269ed8ba99fe9cf35efc1700866/milvus_lite-2.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a13277e9bacc6933dea172e42231f7e6135bd3bdb073dd2688ee180418abd8d9", size = 45337093 }, - { url = "https://files.pythonhosted.org/packages/d3/82/41d9b80f09b82e066894d9b508af07b7b0fa325ce0322980674de49106a0/milvus_lite-2.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25ce13f4b8d46876dd2b7ac8563d7d8306da7ff3999bb0d14b116b30f71d706c", size = 55263911 }, + { url = "https://files.pythonhosted.org/packages/a9/b2/acc5024c8e8b6a0b034670b8e8af306ebd633ede777dcbf557eac4785937/milvus_lite-2.5.1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6b014453200ba977be37ba660cb2d021030375fa6a35bc53c2e1d92980a0c512", size = 27934713, upload-time = "2025-06-30T04:23:37.028Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2e/746f5bb1d6facd1e73eb4af6dd5efda11125b0f29d7908a097485ca6cad9/milvus_lite-2.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2e031088bf308afe5f8567850412d618cfb05a65238ed1a6117f60decccc95a", size = 24421451, upload-time = "2025-06-30T04:23:51.747Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/3d1fee5c16c7661cf53977067a34820f7269ed8ba99fe9cf35efc1700866/milvus_lite-2.5.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:a13277e9bacc6933dea172e42231f7e6135bd3bdb073dd2688ee180418abd8d9", size = 45337093, upload-time = "2025-06-30T04:24:06.706Z" }, + { url = "https://files.pythonhosted.org/packages/d3/82/41d9b80f09b82e066894d9b508af07b7b0fa325ce0322980674de49106a0/milvus_lite-2.5.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25ce13f4b8d46876dd2b7ac8563d7d8306da7ff3999bb0d14b116b30f71d706c", size = 55263911, upload-time = "2025-06-30T04:24:19.434Z" }, ] [[package]] @@ -2501,9 +2501,9 @@ dependencies = [ { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/ec/52e6c9ad90ad7eb3035f5e511123e89d1ecc7617f0c94653264848623c12/msal-1.35.0.tar.gz", hash = "sha256:76ab7513dbdac88d76abdc6a50110f082b7ed3ff1080aca938c53fc88bc75b51", size = 164057 } +sdist = { url = "https://files.pythonhosted.org/packages/95/ec/52e6c9ad90ad7eb3035f5e511123e89d1ecc7617f0c94653264848623c12/msal-1.35.0.tar.gz", hash = "sha256:76ab7513dbdac88d76abdc6a50110f082b7ed3ff1080aca938c53fc88bc75b51", size = 164057, upload-time = "2026-02-24T10:58:28.415Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/26/5463e615de18ad8b80d75d14c612ef3c866fcc07c1c52e8eac7948984214/msal-1.35.0-py3-none-any.whl", hash = "sha256:baf268172d2b736e5d409689424d2f321b4142cab231b4b96594c86762e7e01d", size = 120082 }, + { url = "https://files.pythonhosted.org/packages/56/26/5463e615de18ad8b80d75d14c612ef3c866fcc07c1c52e8eac7948984214/msal-1.35.0-py3-none-any.whl", hash = "sha256:baf268172d2b736e5d409689424d2f321b4142cab231b4b96594c86762e7e01d", size = 120082, upload-time = "2026-02-24T10:58:27.219Z" }, ] [[package]] @@ -2513,108 +2513,108 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "msal" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, ] [[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893 }, - { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456 }, - { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872 }, - { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018 }, - { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883 }, - { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413 }, - { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404 }, - { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456 }, - { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322 }, - { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955 }, - { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254 }, - { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059 }, - { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588 }, - { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642 }, - { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377 }, - { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887 }, - { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053 }, - { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695 }, - { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884 }, - { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770 }, - { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109 }, - { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930 }, - { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074 }, - { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008 }, - { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542 }, - { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719 }, - { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319 }, +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/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { 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]] @@ -2627,54 +2627,54 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053 }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134 }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616 }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847 }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976 }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104 }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927 }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730 }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581 }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252 }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848 }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510 }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744 }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815 }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047 }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998 }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476 }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872 }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239 }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] name = "mypy-extensions" version = "1.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 }, + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] [[package]] name = "nest-asyncio" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418 } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195 }, + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504 }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] @@ -2687,70 +2687,70 @@ dependencies = [ { name = "regex" }, { name = "tqdm" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864 } +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087 }, + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, ] [[package]] name = "numpy" version = "2.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 }, - { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 }, - { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 }, - { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 }, - { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 }, - { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 }, - { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 }, - { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 }, - { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 }, - { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 }, - { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 }, - { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 }, - { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 }, - { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 }, - { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330 }, - { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968 }, - { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311 }, - { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850 }, - { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210 }, - { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199 }, - { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848 }, - { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082 }, - { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866 }, - { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631 }, - { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254 }, - { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138 }, - { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398 }, - { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064 }, - { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680 }, - { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433 }, - { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181 }, - { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756 }, - { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092 }, - { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770 }, - { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562 }, - { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710 }, - { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205 }, - { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738 }, - { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888 }, - { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556 }, - { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899 }, - { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072 }, - { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886 }, - { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567 }, - { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372 }, - { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306 }, - { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394 }, - { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343 }, - { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045 }, - { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024 }, - { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 }, - { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 }, - { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 }, +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696, upload-time = "2026-01-31T23:11:17.516Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322, upload-time = "2026-01-31T23:11:19.883Z" }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157, upload-time = "2026-01-31T23:11:22.375Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330, upload-time = "2026-01-31T23:11:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968, upload-time = "2026-01-31T23:11:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311, upload-time = "2026-01-31T23:11:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850, upload-time = "2026-01-31T23:11:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210, upload-time = "2026-01-31T23:11:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199, upload-time = "2026-01-31T23:11:35.385Z" }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848, upload-time = "2026-01-31T23:11:38.001Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082, upload-time = "2026-01-31T23:11:40.392Z" }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866, upload-time = "2026-01-31T23:11:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631, upload-time = "2026-01-31T23:11:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254, upload-time = "2026-01-31T23:11:46.341Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138, upload-time = "2026-01-31T23:11:48.082Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398, upload-time = "2026-01-31T23:11:50.293Z" }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064, upload-time = "2026-01-31T23:11:52.927Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680, upload-time = "2026-01-31T23:11:55.22Z" }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433, upload-time = "2026-01-31T23:11:58.096Z" }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181, upload-time = "2026-01-31T23:11:59.782Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756, upload-time = "2026-01-31T23:12:02.438Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092, upload-time = "2026-01-31T23:12:04.521Z" }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770, upload-time = "2026-01-31T23:12:06.96Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562, upload-time = "2026-01-31T23:12:09.632Z" }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710, upload-time = "2026-01-31T23:12:11.969Z" }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205, upload-time = "2026-01-31T23:12:14.33Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738, upload-time = "2026-01-31T23:12:16.525Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888, upload-time = "2026-01-31T23:12:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556, upload-time = "2026-01-31T23:12:21.816Z" }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899, upload-time = "2026-01-31T23:12:24.14Z" }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072, upload-time = "2026-01-31T23:12:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886, upload-time = "2026-01-31T23:12:28.488Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567, upload-time = "2026-01-31T23:12:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372, upload-time = "2026-01-31T23:12:32.962Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306, upload-time = "2026-01-31T23:12:34.797Z" }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394, upload-time = "2026-01-31T23:12:36.565Z" }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343, upload-time = "2026-01-31T23:12:39.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045, upload-time = "2026-01-31T23:12:42.041Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024, upload-time = "2026-01-31T23:12:44.331Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937, upload-time = "2026-01-31T23:12:47.229Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844, upload-time = "2026-01-31T23:12:48.997Z" }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, ] [[package]] @@ -2764,18 +2764,18 @@ dependencies = [ { name = "rsa" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/7b/17244b1083e8e604bf154cf9b716aecd6388acd656dd01893d0d244c94d9/oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6", size = 155910 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/7b/17244b1083e8e604bf154cf9b716aecd6388acd656dd01893d0d244c94d9/oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6", size = 155910, upload-time = "2018-09-07T21:38:18.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/a9/4f25a14d23f0786b64875b91784607c2277eff25d48f915e39ff0cff505a/oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", size = 98206 }, + { url = "https://files.pythonhosted.org/packages/95/a9/4f25a14d23f0786b64875b91784607c2277eff25d48f915e39ff0cff505a/oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", size = 98206, upload-time = "2018-09-07T21:38:16.742Z" }, ] [[package]] name = "oauthlib" version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] @@ -2788,9 +2788,9 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/04/6dce2d581c54a8e55a3b128cf79a93821a68a62bb9a956e65476c5bb247e/office365_rest_python_client-2.6.2.tar.gz", hash = "sha256:ce27f5a1c0cc3ff97041ccd9b386145692be4c64739f243f7d6ac3edbe0a3c46", size = 659460 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/04/6dce2d581c54a8e55a3b128cf79a93821a68a62bb9a956e65476c5bb247e/office365_rest_python_client-2.6.2.tar.gz", hash = "sha256:ce27f5a1c0cc3ff97041ccd9b386145692be4c64739f243f7d6ac3edbe0a3c46", size = 659460, upload-time = "2025-05-11T10:24:21.895Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/a4/611155711f8af347875c15b8b83f5fd9e978bd4de45f90085b9a583b684d/Office365_REST_Python_Client-2.6.2-py3-none-any.whl", hash = "sha256:06fc6829c39b503897caa9d881db419d7f97a8e4f1c95c4c2d12db36ea6c955d", size = 1337139 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/611155711f8af347875c15b8b83f5fd9e978bd4de45f90085b9a583b684d/Office365_REST_Python_Client-2.6.2-py3-none-any.whl", hash = "sha256:06fc6829c39b503897caa9d881db419d7f97a8e4f1c95c4c2d12db36ea6c955d", size = 1337139, upload-time = "2025-05-11T10:24:18.926Z" }, ] [[package]] @@ -2807,9 +2807,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717 } +sdist = { url = "https://files.pythonhosted.org/packages/55/13/17e87641b89b74552ed408a92b231283786523edddc95f3545809fab673c/openai-2.24.0.tar.gz", hash = "sha256:1e5769f540dbd01cb33bc4716a23e67b9d695161a734aff9c5f925e2bf99a673", size = 658717, upload-time = "2026-02-24T20:02:07.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122 }, + { url = "https://files.pythonhosted.org/packages/c9/30/844dc675ee6902579b8eef01ed23917cc9319a1c9c0c14ec6e39340c96d0/openai-2.24.0-py3-none-any.whl", hash = "sha256:fed30480d7d6c884303287bde864980a4b137b60553ffbcf9ab4a233b7a73d94", size = 1120122, upload-time = "2026-02-24T20:02:05.669Z" }, ] [[package]] @@ -2820,9 +2820,9 @@ dependencies = [ { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 } +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 }, + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, ] [[package]] @@ -2834,9 +2834,9 @@ dependencies = [ { name = "opentelemetry-instrumentation" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/15/77/f0b1f2bcf451ec5bc443d53bc7437577c3fc8444b3eb0d416ac5f7558b7b/opentelemetry_distro-0.60b1.tar.gz", hash = "sha256:8b7326b83a55ff7b17bb92225a86e2736a004f6af7aff00cb5d87b2d8e5bc283", size = 2584 } +sdist = { url = "https://files.pythonhosted.org/packages/15/77/f0b1f2bcf451ec5bc443d53bc7437577c3fc8444b3eb0d416ac5f7558b7b/opentelemetry_distro-0.60b1.tar.gz", hash = "sha256:8b7326b83a55ff7b17bb92225a86e2736a004f6af7aff00cb5d87b2d8e5bc283", size = 2584, upload-time = "2025-12-11T13:36:39.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/70/78a86531495040fcad9569d7daa630eca06d27d37c825a8aad448b7c1c5b/opentelemetry_distro-0.60b1-py3-none-any.whl", hash = "sha256:581104a786f5df252f4dfe725e0ae16337a26da902acb92d8b3e7aee29f0c76e", size = 3343 }, + { url = "https://files.pythonhosted.org/packages/24/70/78a86531495040fcad9569d7daa630eca06d27d37c825a8aad448b7c1c5b/opentelemetry_distro-0.60b1-py3-none-any.whl", hash = "sha256:581104a786f5df252f4dfe725e0ae16337a26da902acb92d8b3e7aee29f0c76e", size = 3343, upload-time = "2025-12-11T13:35:28.462Z" }, ] [[package]] @@ -2847,9 +2847,9 @@ dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d9/bd/abafe13a0d77145270a39de7442d12d71b51a9f9d103d15d636110ae8a21/opentelemetry_exporter_otlp-1.15.0.tar.gz", hash = "sha256:4f7c49751d9720e2e726e13b0bb958ccade4e29122c305d92c033da432c8d2c5", size = 6126 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/bd/abafe13a0d77145270a39de7442d12d71b51a9f9d103d15d636110ae8a21/opentelemetry_exporter_otlp-1.15.0.tar.gz", hash = "sha256:4f7c49751d9720e2e726e13b0bb958ccade4e29122c305d92c033da432c8d2c5", size = 6126, upload-time = "2022-12-09T22:28:43.353Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/a2/4956610bd5348977fea8818d488793a46d1359337c0226164f093a17c61c/opentelemetry_exporter_otlp-1.15.0-py3-none-any.whl", hash = "sha256:79f22748b6a54808a0448093dfa189c8490e729f67c134d4c992533d9393b33e", size = 6976 }, + { url = "https://files.pythonhosted.org/packages/64/a2/4956610bd5348977fea8818d488793a46d1359337c0226164f093a17c61c/opentelemetry_exporter_otlp-1.15.0-py3-none-any.whl", hash = "sha256:79f22748b6a54808a0448093dfa189c8490e729f67c134d4c992533d9393b33e", size = 6976, upload-time = "2022-12-09T22:28:12.944Z" }, ] [[package]] @@ -2864,9 +2864,9 @@ dependencies = [ { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/ab/1be294b194af410f350f867a54621b4f33b7551adce2ae795e907148fc1e/opentelemetry_exporter_otlp_proto_grpc-1.15.0.tar.gz", hash = "sha256:844f2a4bb9bcda34e4eb6fe36765e5031aacb36dc60ed88c90fc246942ea26e7", size = 27262 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/ab/1be294b194af410f350f867a54621b4f33b7551adce2ae795e907148fc1e/opentelemetry_exporter_otlp_proto_grpc-1.15.0.tar.gz", hash = "sha256:844f2a4bb9bcda34e4eb6fe36765e5031aacb36dc60ed88c90fc246942ea26e7", size = 27262, upload-time = "2022-12-09T22:28:44.359Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/8f/73ad108bcfd61b4169be5ad8b76acaf9158f224740da10ab9ea3469d551a/opentelemetry_exporter_otlp_proto_grpc-1.15.0-py3-none-any.whl", hash = "sha256:c2a5492ba7d140109968135d641d06ce3c5bd73c50665f787526065d57d7fd1d", size = 20378 }, + { url = "https://files.pythonhosted.org/packages/dd/8f/73ad108bcfd61b4169be5ad8b76acaf9158f224740da10ab9ea3469d551a/opentelemetry_exporter_otlp_proto_grpc-1.15.0-py3-none-any.whl", hash = "sha256:c2a5492ba7d140109968135d641d06ce3c5bd73c50665f787526065d57d7fd1d", size = 20378, upload-time = "2022-12-09T22:28:14.623Z" }, ] [[package]] @@ -2881,9 +2881,9 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/ee/14baa8edbf6b0c8e23a93ee0807fb637d4689959a0b166e2821032fade34/opentelemetry_exporter_otlp_proto_http-1.15.0.tar.gz", hash = "sha256:11b2c814249a49b22f6cca7a06b05701f561d577b747f3660dfd67b6eb9daf9c", size = 18930 } +sdist = { url = "https://files.pythonhosted.org/packages/ad/ee/14baa8edbf6b0c8e23a93ee0807fb637d4689959a0b166e2821032fade34/opentelemetry_exporter_otlp_proto_http-1.15.0.tar.gz", hash = "sha256:11b2c814249a49b22f6cca7a06b05701f561d577b747f3660dfd67b6eb9daf9c", size = 18930, upload-time = "2022-12-09T22:28:45.366Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/12/77af459682a4f41eb9f13801af6a12420a86f5673dc568585ee49112e969/opentelemetry_exporter_otlp_proto_http-1.15.0-py3-none-any.whl", hash = "sha256:3ec2a02196c8a54bf5cbf7fe623a5238625638e83b6047a983bdf96e2bbb74c0", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b0/12/77af459682a4f41eb9f13801af6a12420a86f5673dc568585ee49112e969/opentelemetry_exporter_otlp_proto_http-1.15.0-py3-none-any.whl", hash = "sha256:3ec2a02196c8a54bf5cbf7fe623a5238625638e83b6047a983bdf96e2bbb74c0", size = 21588, upload-time = "2022-12-09T22:28:15.776Z" }, ] [[package]] @@ -2896,9 +2896,9 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706 } +sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096 }, + { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" }, ] [[package]] @@ -2908,9 +2908,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1e/80/b3b2a98039574e57b6b15982219ae025d55f8c46d50dde258865ce5601b4/opentelemetry_proto-1.15.0.tar.gz", hash = "sha256:9c4008e40ac8cab359daac283fbe7002c5c29c77ea2674ad5626a249e64e0101", size = 35713 } +sdist = { url = "https://files.pythonhosted.org/packages/1e/80/b3b2a98039574e57b6b15982219ae025d55f8c46d50dde258865ce5601b4/opentelemetry_proto-1.15.0.tar.gz", hash = "sha256:9c4008e40ac8cab359daac283fbe7002c5c29c77ea2674ad5626a249e64e0101", size = 35713, upload-time = "2022-12-09T22:28:55.409Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/56/8343d94af8f32594f6b0bd273f72a40e430fb5970a353237af53af5d3031/opentelemetry_proto-1.15.0-py3-none-any.whl", hash = "sha256:044b6d044b4d10530f250856f933442b8753a17f94ae37c207607f733fb9a844", size = 52616 }, + { url = "https://files.pythonhosted.org/packages/3a/56/8343d94af8f32594f6b0bd273f72a40e430fb5970a353237af53af5d3031/opentelemetry_proto-1.15.0-py3-none-any.whl", hash = "sha256:044b6d044b4d10530f250856f933442b8753a17f94ae37c207607f733fb9a844", size = 52616, upload-time = "2022-12-09T22:28:30.03Z" }, ] [[package]] @@ -2922,9 +2922,9 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 } +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 }, + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, ] [[package]] @@ -2935,9 +2935,9 @@ dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 } +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 }, + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, ] [[package]] @@ -2947,27 +2947,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ed/c7/12632c03022aa5059ce9b6738397cda682dfda9d9afe7008b8a4f98c6ee5/oracledb-2.4.0.tar.gz", hash = "sha256:bdd61a9d5077448b5f1c58af6a14accc287bf8032846c351a3cdde5cf64fe95b", size = 614809 } +sdist = { url = "https://files.pythonhosted.org/packages/ed/c7/12632c03022aa5059ce9b6738397cda682dfda9d9afe7008b8a4f98c6ee5/oracledb-2.4.0.tar.gz", hash = "sha256:bdd61a9d5077448b5f1c58af6a14accc287bf8032846c351a3cdde5cf64fe95b", size = 614809, upload-time = "2024-08-20T21:02:35.362Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/5b/5125e0a74a58717ac094d953ddaa4c61cfefcd926850c0ecc081e0c209f3/oracledb-2.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:03d1072db83e3f95a8792b8452c78940141902ef97f31223f1d96bfeb8ff830b", size = 3769983 }, - { url = "https://files.pythonhosted.org/packages/17/22/81eb81e15a86989acd21220480a87a3891a27b3f2d64b249098e09e002eb/oracledb-2.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fda77ace54379ad70187627ed02329f9ef4f35c1cc1052e4d27fe4ec68d38fc", size = 2081340 }, - { url = "https://files.pythonhosted.org/packages/6f/56/9cd84f67a573cc6066589d8264ab13f710a128197977205b9c4b177ee85e/oracledb-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed34cdd5037277424bb5a38987e00cbb6eea3670ce9c4fcc3cab5971fab5348", size = 2234827 }, - { url = "https://files.pythonhosted.org/packages/f5/ca/4406cfe3400735bf4a1eee951eb174c6cd8573e74d43c1aba9448066a3d2/oracledb-2.4.0-cp312-cp312-win32.whl", hash = "sha256:02e1eea36de371d7719ca02d20a8900fab767e5db71aa59be101405060cf2cfa", size = 1373933 }, - { url = "https://files.pythonhosted.org/packages/a8/e9/1a8afdbe4aaba030476c91284d7599f54fce2879232d28797a4a71d5cfe2/oracledb-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b81ec1e20d4d20b0f95a673bb73923d24673e8739d3a25a746113519612c057", size = 1681666 }, - { url = "https://files.pythonhosted.org/packages/88/ae/603c592fc7054ccad523ba06f3d186cae5fb0f18ce477552be2178d6668b/oracledb-2.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b2933b4fd324da089a15567830d81d4ff1b3e7ecc24a615f9e61b0f7fcacf32d", size = 3730093 }, - { url = "https://files.pythonhosted.org/packages/af/70/744ab12e334375808678fbce494be560269f59dbda03613f02d4c22cadeb/oracledb-2.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d721d5fd0d45bd901bc76247172eb2e00f9feb67283dbb38e763e3e50308cb0", size = 2079861 }, - { url = "https://files.pythonhosted.org/packages/eb/5d/f1606491f05337d95e92ba8d474852d9616cc43bf24d60a64cc33a5f5517/oracledb-2.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23661da50934439b88fcedd9be3c8abecb313335abde9cf9faee3162c814744", size = 2235137 }, - { url = "https://files.pythonhosted.org/packages/ca/b0/cc05876b2a0b50a528dc5f01a81eb18386beeb0aba8993b796d1d381399e/oracledb-2.4.0-cp313-cp313-win32.whl", hash = "sha256:b10998a89fc93a31a968fd34d36547f7878f3efb3491e61493c78ddd5724283f", size = 1370670 }, - { url = "https://files.pythonhosted.org/packages/ee/09/bea4244b8e040f9a31178196082ffbde34404f8bb42c780a192b28a113b2/oracledb-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e39779713558bc6f2e1ec78b71378536ec9da05dc5f95fe3ca41bfb6b878e81a", size = 1678689 }, + { url = "https://files.pythonhosted.org/packages/9e/5b/5125e0a74a58717ac094d953ddaa4c61cfefcd926850c0ecc081e0c209f3/oracledb-2.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:03d1072db83e3f95a8792b8452c78940141902ef97f31223f1d96bfeb8ff830b", size = 3769983, upload-time = "2024-08-20T21:03:08.186Z" }, + { url = "https://files.pythonhosted.org/packages/17/22/81eb81e15a86989acd21220480a87a3891a27b3f2d64b249098e09e002eb/oracledb-2.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fda77ace54379ad70187627ed02329f9ef4f35c1cc1052e4d27fe4ec68d38fc", size = 2081340, upload-time = "2024-08-20T21:03:10.988Z" }, + { url = "https://files.pythonhosted.org/packages/6f/56/9cd84f67a573cc6066589d8264ab13f710a128197977205b9c4b177ee85e/oracledb-2.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed34cdd5037277424bb5a38987e00cbb6eea3670ce9c4fcc3cab5971fab5348", size = 2234827, upload-time = "2024-08-20T21:03:13.716Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ca/4406cfe3400735bf4a1eee951eb174c6cd8573e74d43c1aba9448066a3d2/oracledb-2.4.0-cp312-cp312-win32.whl", hash = "sha256:02e1eea36de371d7719ca02d20a8900fab767e5db71aa59be101405060cf2cfa", size = 1373933, upload-time = "2024-08-20T21:03:15.514Z" }, + { url = "https://files.pythonhosted.org/packages/a8/e9/1a8afdbe4aaba030476c91284d7599f54fce2879232d28797a4a71d5cfe2/oracledb-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b81ec1e20d4d20b0f95a673bb73923d24673e8739d3a25a746113519612c057", size = 1681666, upload-time = "2024-08-20T21:03:17.366Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/603c592fc7054ccad523ba06f3d186cae5fb0f18ce477552be2178d6668b/oracledb-2.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b2933b4fd324da089a15567830d81d4ff1b3e7ecc24a615f9e61b0f7fcacf32d", size = 3730093, upload-time = "2024-08-20T21:03:20.23Z" }, + { url = "https://files.pythonhosted.org/packages/af/70/744ab12e334375808678fbce494be560269f59dbda03613f02d4c22cadeb/oracledb-2.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d721d5fd0d45bd901bc76247172eb2e00f9feb67283dbb38e763e3e50308cb0", size = 2079861, upload-time = "2024-08-20T21:03:22.954Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/f1606491f05337d95e92ba8d474852d9616cc43bf24d60a64cc33a5f5517/oracledb-2.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23661da50934439b88fcedd9be3c8abecb313335abde9cf9faee3162c814744", size = 2235137, upload-time = "2024-08-20T21:03:25.061Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b0/cc05876b2a0b50a528dc5f01a81eb18386beeb0aba8993b796d1d381399e/oracledb-2.4.0-cp313-cp313-win32.whl", hash = "sha256:b10998a89fc93a31a968fd34d36547f7878f3efb3491e61493c78ddd5724283f", size = 1370670, upload-time = "2024-08-20T21:03:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/ee/09/bea4244b8e040f9a31178196082ffbde34404f8bb42c780a192b28a113b2/oracledb-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e39779713558bc6f2e1ec78b71378536ec9da05dc5f95fe3ca41bfb6b878e81a", size = 1678689, upload-time = "2024-08-20T21:03:29.108Z" }, ] [[package]] name = "packaging" version = "26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] [[package]] @@ -2980,41 +2980,41 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 }, +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, ] [[package]] @@ -3027,18 +3027,18 @@ dependencies = [ { name = "invoke" }, { name = "pynacl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586 } +sdist = { url = "https://files.pythonhosted.org/packages/62/93/dcc25d52f49022ae6175d15e6bd751f1acc99b98bc61fc55e5155a7be2e7/paramiko-5.0.0.tar.gz", hash = "sha256:36763b5b95c2a0dcfdf1abc48e48156ee425b21efe2f0e787c2dd5a95c0e5e79", size = 1548586, upload-time = "2026-05-09T18:28:52.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919 }, + { url = "https://files.pythonhosted.org/packages/82/5b/eadf6d45de38d30ab603f49393b6cd2cbe7e233af8cf90197e32782b68a9/paramiko-5.0.0-py3-none-any.whl", hash = "sha256:b7044611c30140d9a75261653210e2002977b71a0497ff3ba0d98d7edbf62f7c", size = 208919, upload-time = "2026-05-09T18:28:50.295Z" }, ] [[package]] name = "pathspec" version = "1.0.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200 } +sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206 }, + { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, ] [[package]] @@ -3049,9 +3049,9 @@ dependencies = [ { name = "charset-normalizer" }, { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/9a/d79d8fa6d47a0338846bb558b39b9963b8eb2dfedec61867c138c1b17eeb/pdfminer_six-20251230.tar.gz", hash = "sha256:e8f68a14c57e00c2d7276d26519ea64be1b48f91db1cdc776faa80528ca06c1e", size = 8511285 } +sdist = { url = "https://files.pythonhosted.org/packages/46/9a/d79d8fa6d47a0338846bb558b39b9963b8eb2dfedec61867c138c1b17eeb/pdfminer_six-20251230.tar.gz", hash = "sha256:e8f68a14c57e00c2d7276d26519ea64be1b48f91db1cdc776faa80528ca06c1e", size = 8511285, upload-time = "2025-12-30T15:49:13.104Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/d7/b288ea32deb752a09aab73c75e1e7572ab2a2b56c3124a5d1eb24c62ceb3/pdfminer_six-20251230-py3-none-any.whl", hash = "sha256:9ff2e3466a7dfc6de6fd779478850b6b7c2d9e9405aa2a5869376a822771f485", size = 6591909 }, + { url = "https://files.pythonhosted.org/packages/65/d7/b288ea32deb752a09aab73c75e1e7572ab2a2b56c3124a5d1eb24c62ceb3/pdfminer_six-20251230-py3-none-any.whl", hash = "sha256:9ff2e3466a7dfc6de6fd779478850b6b7c2d9e9405aa2a5869376a822771f485", size = 6591909, upload-time = "2025-12-30T15:49:10.76Z" }, ] [[package]] @@ -3063,9 +3063,9 @@ dependencies = [ { name = "pillow" }, { name = "pypdfium2" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/37/9ca3519e92a8434eb93be570b131476cc0a4e840bb39c62ddb7813a39d53/pdfplumber-0.11.9.tar.gz", hash = "sha256:481224b678b2bbdbf376e2c39bf914144eef7c3d301b4a28eebf0f7f6109d6dc", size = 102768 } +sdist = { url = "https://files.pythonhosted.org/packages/38/37/9ca3519e92a8434eb93be570b131476cc0a4e840bb39c62ddb7813a39d53/pdfplumber-0.11.9.tar.gz", hash = "sha256:481224b678b2bbdbf376e2c39bf914144eef7c3d301b4a28eebf0f7f6109d6dc", size = 102768, upload-time = "2026-01-05T08:10:29.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/c8/cdbc975f5b634e249cfa6597e37c50f3078412474f21c015e508bfbfe3c3/pdfplumber-0.11.9-py3-none-any.whl", hash = "sha256:33ec5580959ba524e9100138746e090879504c42955df1b8a997604dd326c443", size = 60045 }, + { url = "https://files.pythonhosted.org/packages/8b/c8/cdbc975f5b634e249cfa6597e37c50f3078412474f21c015e508bfbfe3c3/pdfplumber-0.11.9-py3-none-any.whl", hash = "sha256:33ec5580959ba524e9100138746e090879504c42955df1b8a997604dd326c443", size = 60045, upload-time = "2026-01-05T08:10:27.512Z" }, ] [[package]] @@ -3075,78 +3075,78 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354 } +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441 }, + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, ] [[package]] name = "pillow" version = "12.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279 }, - { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490 }, - { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462 }, - { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744 }, - { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371 }, - { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215 }, - { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783 }, - { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112 }, - { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489 }, - { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129 }, - { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612 }, - { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837 }, - { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528 }, - { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401 }, - { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094 }, - { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402 }, - { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005 }, - { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669 }, - { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194 }, - { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423 }, - { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667 }, - { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580 }, - { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896 }, - { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266 }, - { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508 }, - { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927 }, - { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624 }, - { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252 }, - { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550 }, - { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114 }, - { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667 }, - { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966 }, - { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241 }, - { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592 }, - { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542 }, - { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765 }, - { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848 }, - { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515 }, - { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159 }, - { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185 }, - { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386 }, - { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384 }, - { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599 }, - { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021 }, - { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360 }, - { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628 }, - { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321 }, - { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723 }, - { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400 }, - { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835 }, - { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225 }, - { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541 }, - { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251 }, - { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807 }, - { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935 }, - { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720 }, - { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498 }, - { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413 }, - { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084 }, - { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579 }, +sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" }, + { url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" }, + { url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" }, + { url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" }, + { url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" }, + { url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" }, + { url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" }, + { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" }, + { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" }, + { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" }, + { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" }, + { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" }, + { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" }, + { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" }, + { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" }, + { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" }, + { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" }, + { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" }, + { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" }, + { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" }, + { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" }, + { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" }, + { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" }, + { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" }, + { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" }, + { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" }, + { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" }, + { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" }, ] [[package]] @@ -3160,45 +3160,45 @@ dependencies = [ { name = "typing-extensions" }, { name = "urllib3", marker = "python_full_version < '4'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bc/9d/07a7f2136ce04cabd21d69c057dc2915867082b0047e6873e424388d4475/pinecone-7.0.1.tar.gz", hash = "sha256:49ff7b0f5be4a2ddec5aaa709758a9f2df56baa58ad46507d081409e246a81ec", size = 207930 } +sdist = { url = "https://files.pythonhosted.org/packages/bc/9d/07a7f2136ce04cabd21d69c057dc2915867082b0047e6873e424388d4475/pinecone-7.0.1.tar.gz", hash = "sha256:49ff7b0f5be4a2ddec5aaa709758a9f2df56baa58ad46507d081409e246a81ec", size = 207930, upload-time = "2025-05-21T19:39:01.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/88/896221e991077d353e61991b759f46d75f3b4298eb5a4aa6534c1371f4b0/pinecone-7.0.1-py3-none-any.whl", hash = "sha256:ce7b0dab3c9f7d81e75b24c13fcbca4a51371e08021faaecaf0cd9a45ca1be6c", size = 516590 }, + { url = "https://files.pythonhosted.org/packages/81/88/896221e991077d353e61991b759f46d75f3b4298eb5a4aa6534c1371f4b0/pinecone-7.0.1-py3-none-any.whl", hash = "sha256:ce7b0dab3c9f7d81e75b24c13fcbca4a51371e08021faaecaf0cd9a45ca1be6c", size = 516590, upload-time = "2025-05-21T19:38:59.117Z" }, ] [[package]] name = "pinecone-plugin-interface" version = "0.0.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/fb/e8a4063264953ead9e2b24d9b390152c60f042c951c47f4592e9996e57ff/pinecone_plugin_interface-0.0.7.tar.gz", hash = "sha256:b8e6675e41847333aa13923cc44daa3f85676d7157324682dc1640588a982846", size = 3370 } +sdist = { url = "https://files.pythonhosted.org/packages/f4/fb/e8a4063264953ead9e2b24d9b390152c60f042c951c47f4592e9996e57ff/pinecone_plugin_interface-0.0.7.tar.gz", hash = "sha256:b8e6675e41847333aa13923cc44daa3f85676d7157324682dc1640588a982846", size = 3370, upload-time = "2024-06-05T01:57:52.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/1d/a21fdfcd6d022cb64cef5c2a29ee6691c6c103c4566b41646b080b7536a5/pinecone_plugin_interface-0.0.7-py3-none-any.whl", hash = "sha256:875857ad9c9fc8bbc074dbe780d187a2afd21f5bfe0f3b08601924a61ef1bba8", size = 6249 }, + { url = "https://files.pythonhosted.org/packages/3b/1d/a21fdfcd6d022cb64cef5c2a29ee6691c6c103c4566b41646b080b7536a5/pinecone_plugin_interface-0.0.7-py3-none-any.whl", hash = "sha256:875857ad9c9fc8bbc074dbe780d187a2afd21f5bfe0f3b08601924a61ef1bba8", size = 6249, upload-time = "2024-06-05T01:57:50.583Z" }, ] [[package]] name = "platformdirs" version = "4.9.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168 }, + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "ply" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" }, ] [[package]] @@ -3208,18 +3208,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424 }, + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, ] [[package]] name = "prometheus-client" version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057 }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -3229,93 +3229,93 @@ 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 } +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 }, + { 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" }, ] [[package]] name = "propcache" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] [[package]] @@ -3325,109 +3325,109 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929 } +sdist = { url = "https://files.pythonhosted.org/packages/3a/02/8832cde80e7380c600fbf55090b6ab7b62bd6825dbedde6d6657c15a1f8e/proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147", size = 56929, upload-time = "2026-02-02T17:34:49.035Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480 }, + { url = "https://files.pythonhosted.org/packages/5d/79/ac273cbbf744691821a9cca88957257f41afe271637794975ca090b9588b/proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc", size = 50480, upload-time = "2026-02-02T17:34:47.339Z" }, ] [[package]] name = "protobuf" version = "4.25.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920 } +sdist = { url = "https://files.pythonhosted.org/packages/df/01/34c8d2b6354906d728703cb9d546a0e534de479e25f1b581e4094c4a85cc/protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd", size = 380920, upload-time = "2025-05-28T14:22:25.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745 }, - { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736 }, - { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537 }, - { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005 }, - { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924 }, - { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757 }, + { url = "https://files.pythonhosted.org/packages/45/ff/05f34305fe6b85bbfbecbc559d423a5985605cad5eda4f47eae9e9c9c5c5/protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0", size = 392745, upload-time = "2025-05-28T14:22:10.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/35/8b8a8405c564caf4ba835b1fdf554da869954712b26d8f2a98c0e434469b/protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9", size = 413736, upload-time = "2025-05-28T14:22:13.156Z" }, + { url = "https://files.pythonhosted.org/packages/28/d7/ab27049a035b258dab43445eb6ec84a26277b16105b277cbe0a7698bdc6c/protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f", size = 394537, upload-time = "2025-05-28T14:22:14.768Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6d/a4a198b61808dd3d1ee187082ccc21499bc949d639feb948961b48be9a7e/protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7", size = 294005, upload-time = "2025-05-28T14:22:16.052Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c6/c9deaa6e789b6fc41b88ccbdfe7a42d2b82663248b715f55aa77fbc00724/protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0", size = 294924, upload-time = "2025-05-28T14:22:17.105Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c1/6aece0ab5209981a70cd186f164c133fdba2f51e124ff92b73de7fd24d78/protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59", size = 156757, upload-time = "2025-05-28T14:22:24.135Z" }, ] [[package]] name = "psutil" version = "5.9.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/c7/6dc0a455d111f68ee43f27793971cf03fe29b6ef972042549db29eec39a2/psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c", size = 503247 } +sdist = { url = "https://files.pythonhosted.org/packages/90/c7/6dc0a455d111f68ee43f27793971cf03fe29b6ef972042549db29eec39a2/psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c", size = 503247, upload-time = "2024-01-19T20:47:09.517Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e3/07ae864a636d70a8a6f58da27cb1179192f1140d5d1da10886ade9405797/psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81", size = 248702 }, - { url = "https://files.pythonhosted.org/packages/b3/bd/28c5f553667116b2598b9cc55908ec435cb7f77a34f2bff3e3ca765b0f78/psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421", size = 285242 }, - { url = "https://files.pythonhosted.org/packages/c5/4f/0e22aaa246f96d6ac87fe5ebb9c5a693fbe8877f537a1022527c47ca43c5/psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4", size = 288191 }, - { url = "https://files.pythonhosted.org/packages/6e/f5/2aa3a4acdc1e5940b59d421742356f133185667dd190b166dbcfcf5d7b43/psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0", size = 251252 }, - { url = "https://files.pythonhosted.org/packages/93/52/3e39d26feae7df0aa0fd510b14012c3678b36ed068f7d78b8d8784d61f0e/psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf", size = 255090 }, - { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898 }, + { url = "https://files.pythonhosted.org/packages/e7/e3/07ae864a636d70a8a6f58da27cb1179192f1140d5d1da10886ade9405797/psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81", size = 248702, upload-time = "2024-01-19T20:47:36.303Z" }, + { url = "https://files.pythonhosted.org/packages/b3/bd/28c5f553667116b2598b9cc55908ec435cb7f77a34f2bff3e3ca765b0f78/psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421", size = 285242, upload-time = "2024-01-19T20:47:39.65Z" }, + { url = "https://files.pythonhosted.org/packages/c5/4f/0e22aaa246f96d6ac87fe5ebb9c5a693fbe8877f537a1022527c47ca43c5/psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4", size = 288191, upload-time = "2024-01-19T20:47:43.078Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/2aa3a4acdc1e5940b59d421742356f133185667dd190b166dbcfcf5d7b43/psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0", size = 251252, upload-time = "2024-01-19T20:47:52.88Z" }, + { url = "https://files.pythonhosted.org/packages/93/52/3e39d26feae7df0aa0fd510b14012c3678b36ed068f7d78b8d8784d61f0e/psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf", size = 255090, upload-time = "2024-01-19T20:47:56.019Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" }, ] [[package]] name = "psycopg2-binary" version = "2.9.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", size = 384973 } +sdist = { url = "https://files.pythonhosted.org/packages/fc/07/e720e53bfab016ebcc34241695ccc06a9e3d91ba19b40ca81317afbdc440/psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", size = 384973, upload-time = "2023-10-03T12:48:55.128Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/d0/5f2db14e7b53552276ab613399a83f83f85b173a862d3f20580bc7231139/psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", size = 2823784 }, - { url = "https://files.pythonhosted.org/packages/18/ca/da384fd47233e300e3e485c90e7aab5d7def896d1281239f75901faf87d4/psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", size = 2553308 }, - { url = "https://files.pythonhosted.org/packages/50/66/fa53d2d3d92f6e1ef469d92afc6a4fe3f6e8a9a04b687aa28fb1f1d954ee/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", size = 2851283 }, - { url = "https://files.pythonhosted.org/packages/04/37/2429360ac5547378202db14eec0dde76edbe1f6627df5a43c7e164922859/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", size = 3081839 }, - { url = "https://files.pythonhosted.org/packages/62/2a/c0530b59d7e0d09824bc2102ecdcec0456b8ca4d47c0caa82e86fce3ed4c/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", size = 3264488 }, - { url = "https://files.pythonhosted.org/packages/19/57/9f172b900795ea37246c78b5f52e00f4779984370855b3e161600156906d/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", size = 3020700 }, - { url = "https://files.pythonhosted.org/packages/94/68/1176fc14ea76861b7b8360be5176e87fb20d5091b137c76570eb4e237324/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", size = 2355968 }, - { url = "https://files.pythonhosted.org/packages/70/bb/aec2646a705a09079d008ce88073401cd61fc9b04f92af3eb282caa3a2ec/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", size = 2536101 }, - { url = "https://files.pythonhosted.org/packages/14/33/12818c157e333cb9d9e6753d1b2463b6f60dbc1fade115f8e4dc5c52cac4/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", size = 2487064 }, - { url = "https://files.pythonhosted.org/packages/56/a2/7851c68fe8768f3c9c246198b6356ee3e4a8a7f6820cc798443faada3400/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", size = 2456257 }, - { url = "https://files.pythonhosted.org/packages/6f/ee/3ba07c6dc7c3294e717e94720da1597aedc82a10b1b180203ce183d4631a/psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", size = 1024709 }, - { url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", size = 1163864 }, + { url = "https://files.pythonhosted.org/packages/a7/d0/5f2db14e7b53552276ab613399a83f83f85b173a862d3f20580bc7231139/psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", size = 2823784, upload-time = "2023-10-03T12:47:00.404Z" }, + { url = "https://files.pythonhosted.org/packages/18/ca/da384fd47233e300e3e485c90e7aab5d7def896d1281239f75901faf87d4/psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", size = 2553308, upload-time = "2023-11-01T10:40:33.984Z" }, + { url = "https://files.pythonhosted.org/packages/50/66/fa53d2d3d92f6e1ef469d92afc6a4fe3f6e8a9a04b687aa28fb1f1d954ee/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", size = 2851283, upload-time = "2023-10-03T12:47:02.736Z" }, + { url = "https://files.pythonhosted.org/packages/04/37/2429360ac5547378202db14eec0dde76edbe1f6627df5a43c7e164922859/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", size = 3081839, upload-time = "2023-10-03T12:47:05.027Z" }, + { url = "https://files.pythonhosted.org/packages/62/2a/c0530b59d7e0d09824bc2102ecdcec0456b8ca4d47c0caa82e86fce3ed4c/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", size = 3264488, upload-time = "2023-10-03T12:47:08.962Z" }, + { url = "https://files.pythonhosted.org/packages/19/57/9f172b900795ea37246c78b5f52e00f4779984370855b3e161600156906d/psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", size = 3020700, upload-time = "2023-10-03T12:47:12.23Z" }, + { url = "https://files.pythonhosted.org/packages/94/68/1176fc14ea76861b7b8360be5176e87fb20d5091b137c76570eb4e237324/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", size = 2355968, upload-time = "2023-10-03T12:47:14.817Z" }, + { url = "https://files.pythonhosted.org/packages/70/bb/aec2646a705a09079d008ce88073401cd61fc9b04f92af3eb282caa3a2ec/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", size = 2536101, upload-time = "2023-10-03T12:47:17.454Z" }, + { url = "https://files.pythonhosted.org/packages/14/33/12818c157e333cb9d9e6753d1b2463b6f60dbc1fade115f8e4dc5c52cac4/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", size = 2487064, upload-time = "2023-10-03T12:47:20.717Z" }, + { url = "https://files.pythonhosted.org/packages/56/a2/7851c68fe8768f3c9c246198b6356ee3e4a8a7f6820cc798443faada3400/psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", size = 2456257, upload-time = "2023-10-03T12:47:23.004Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ee/3ba07c6dc7c3294e717e94720da1597aedc82a10b1b180203ce183d4631a/psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", size = 1024709, upload-time = "2023-10-28T09:37:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/7b/08/9c66c269b0d417a0af9fb969535f0371b8c538633535a7a6a5ca3f9231e2/psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", size = 1163864, upload-time = "2023-10-28T09:37:28.155Z" }, ] [[package]] name = "pyarrow" version = "24.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559 }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654 }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394 }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122 }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032 }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490 }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660 }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759 }, - { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471 }, - { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981 }, - { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172 }, - { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733 }, - { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335 }, - { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748 }, - { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554 }, - { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301 }, - { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929 }, - { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365 }, - { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819 }, - { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252 }, - { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127 }, - { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997 }, - { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720 }, - { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852 }, - { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852 }, - { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207 }, - { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117 }, - { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155 }, - { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387 }, - { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102 }, - { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118 }, - { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765 }, - { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890 }, - { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250 }, - { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282 }, +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, ] [[package]] name = "pyasn1" version = "0.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997 }, + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, ] [[package]] @@ -3437,27 +3437,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycodestyle" version = "2.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472 } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594 }, + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, ] [[package]] name = "pycparser" version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172 }, + { 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]] @@ -3470,9 +3470,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, ] [[package]] @@ -3482,68 +3482,68 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, ] [[package]] @@ -3555,9 +3555,9 @@ dependencies = [ { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253 } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413 }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, ] [[package]] @@ -3570,9 +3570,9 @@ dependencies = [ { name = "pyopenssl" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/45/74/b591079fa588351cec61861b85ba26f7deb96f3b445556c100e17db5572b/PyDrive2-1.15.4.tar.gz", hash = "sha256:0c011b74ebc24f3c6ca72820626b77f1dfe0ae88f5740c5a5cf96e83dd79ba99", size = 60514 } +sdist = { url = "https://files.pythonhosted.org/packages/45/74/b591079fa588351cec61861b85ba26f7deb96f3b445556c100e17db5572b/PyDrive2-1.15.4.tar.gz", hash = "sha256:0c011b74ebc24f3c6ca72820626b77f1dfe0ae88f5740c5a5cf96e83dd79ba99", size = 60514, upload-time = "2023-05-21T02:25:57.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/f4/d0b40ee1c703304e8cc737e53516f834c0fbad4fe9b27aed7680d9fdf344/PyDrive2-1.15.4-py3-none-any.whl", hash = "sha256:91fe28e5f094a6dfff834495c4aee0041cbef979467ad27cd0d4b1f91afa8869", size = 45011 }, + { url = "https://files.pythonhosted.org/packages/18/f4/d0b40ee1c703304e8cc737e53516f834c0fbad4fe9b27aed7680d9fdf344/PyDrive2-1.15.4-py3-none-any.whl", hash = "sha256:91fe28e5f094a6dfff834495c4aee0041cbef979467ad27cd0d4b1f91afa8869", size = 45011, upload-time = "2023-05-21T02:25:55.265Z" }, ] [package.optional-dependencies] @@ -3587,27 +3587,27 @@ fsspec = [ name = "pyflakes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669 } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551 }, + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] [[package]] name = "pygments" version = "2.19.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyjwt" version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515 } +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274 }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -3628,47 +3628,47 @@ dependencies = [ { name = "setuptools" }, { name = "ujson" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d3/13/899185f025802ba80255faa8e45b3f3bf9cb7bab2d4235e12e3322c8e2a4/pymilvus-2.5.18.tar.gz", hash = "sha256:9e517076068e98dac51c018bc0dfe1f651d936154e2e2d9ad6c7b3dab1164e2d", size = 1285482 } +sdist = { url = "https://files.pythonhosted.org/packages/d3/13/899185f025802ba80255faa8e45b3f3bf9cb7bab2d4235e12e3322c8e2a4/pymilvus-2.5.18.tar.gz", hash = "sha256:9e517076068e98dac51c018bc0dfe1f651d936154e2e2d9ad6c7b3dab1164e2d", size = 1285482, upload-time = "2025-12-02T10:58:25.399Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9c/a2b50b2b369814003460ca12a3c195fbf11b89bc1a861c2aa737c33ad7f9/pymilvus-2.5.18-py3-none-any.whl", hash = "sha256:1b78badcfa8d62db7d0b29193fc0422e4676873ff1c745a9d75c2c885d7a7e32", size = 244089 }, + { url = "https://files.pythonhosted.org/packages/c0/9c/a2b50b2b369814003460ca12a3c195fbf11b89bc1a861c2aa737c33ad7f9/pymilvus-2.5.18-py3-none-any.whl", hash = "sha256:1b78badcfa8d62db7d0b29193fc0422e4676873ff1c745a9d75c2c885d7a7e32", size = 244089, upload-time = "2025-12-02T10:58:23.944Z" }, ] [[package]] name = "pymssql" version = "2.3.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/35/5a0b79369e42fffd5c04e4e74fa90ef034cc5c3f314e14f6d58cac646ccf/pymssql-2.3.4.tar.gz", hash = "sha256:117c82d7aa9021171aa9be98368475519f33d9c32073cdcf9b0d76231abc6436", size = 184604 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/89/5a7a4b27ee44b2dc4708de7e897311cb17f15e7c983c299e8bf97ebf98d1/pymssql-2.3.4-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:809b75aaeb9bcd061230bace41e275f80f464f70fcbf5dde2ba7ba8f0eea5298", size = 3075736 }, - { url = "https://files.pythonhosted.org/packages/43/f9/19bbb0026a47043fb239e821e10a75304b12ba986ce4af71cf8986af411c/pymssql-2.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ab1ee04754fb8ce703b6c154e54fde4f6c7f440766d397b101b748123a12df", size = 4019433 }, - { url = "https://files.pythonhosted.org/packages/a6/ac/3aca13f1f527299db4adef594fb9f14d47d68de91b93a220a67391b8ec87/pymssql-2.3.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e22bb4d5aed85b084e3b9fb5ae3463301dd69c17703cfef72e0aed746452cc9", size = 3993550 }, - { url = "https://files.pythonhosted.org/packages/b9/93/879d92f61afb974f69b9186b16ee6a97adff2abc82777e3b66c9c9efb179/pymssql-2.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2c1c8d3632630d52387e5b5b4483027494b5cb8f65401573715b74e7a3f16e5", size = 4381934 }, - { url = "https://files.pythonhosted.org/packages/6c/a6/923769b6dbb4e3a4c07a867e0c7fa8e4b230f675095cd7109d4e3eb9ddf0/pymssql-2.3.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f7f245acbdf89b96a41513ef0214b55a3ba2824f1f3119dd1945443b6cac78d3", size = 4849674 }, - { url = "https://files.pythonhosted.org/packages/7a/2d/c787f061dcd0603905bf8085dda9cddb8c3c03b18d9239d5d18c953eebba/pymssql-2.3.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9484485fb847eb67f828459b0f4857c9725b20c517c2b7f88a9788fd72b76a6a", size = 4076649 }, - { url = "https://files.pythonhosted.org/packages/c1/a2/e55d823e3ab21cf9fc88e4e2424936899392d9d2e6569d5bcce063f84dac/pymssql-2.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4a0716482cd5ecce07230925593cefd9137959c18aca4c92fc24c243d3c20e38", size = 4139477 }, - { url = "https://files.pythonhosted.org/packages/c7/7c/0fec6587b38081d0d0fca4f9ad31e85ec6c5791879e57f0e559ec6be4d3d/pymssql-2.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba4f988674b361709821c8173a6471aa6e47ee6e45b5a8e30d4dcbde1f62fb0f", size = 4653837 }, - { url = "https://files.pythonhosted.org/packages/5f/7c/77d0251f4b5ad5690226a93547fc8279c1c48bd14e3ccc820f5c580a3b73/pymssql-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:51b8ebfbd7d14d5e7c65e76ffaf31584ffabe9fb1bfd2a85f529bd707512e39d", size = 4910914 }, - { url = "https://files.pythonhosted.org/packages/4f/22/1b2ef85804872a5940010d3c012722356af1fa24f8ba6f419c0260881032/pymssql-2.3.4-cp312-cp312-win32.whl", hash = "sha256:c8f5718f5e7d2623eaf35e025d5fa288c5789916809a89f00b42346b888673da", size = 1337991 }, - { url = "https://files.pythonhosted.org/packages/0f/43/c98f34e7b3cd45653fb233a4bee83bffca0cf5e78c290c291cec34faac21/pymssql-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:d72b38b5ba66a4072c680447099bb63ac35d0425e9a29ff91b048e563b999be5", size = 2021760 }, - { url = "https://files.pythonhosted.org/packages/63/58/90dbe299359c547fcb037d4a12f2146916213b99a245d01efdf5ade52910/pymssql-2.3.4-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:36ede0bc046e18cb0a5f043828bc441c80ffb2aa4606ce0cfcbf2a3d71266f0a", size = 3064581 }, - { url = "https://files.pythonhosted.org/packages/4b/7c/15e75a74de5e392ea1a9456261632cc312c873f28ac2f9ef39dfefac8cd2/pymssql-2.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d247114853ada387415df303d24d2e990596ce28b23f5b59c46d852cfea0f2ad", size = 4013283 }, - { url = "https://files.pythonhosted.org/packages/2a/29/b9f08676145c3086db11c55b40bd58dfb0d775853f7280c1b2e15fc44fc2/pymssql-2.3.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79881cbe1a5826ddb959ccf8add015e5b82e6afbbf9cf5e281bd794278b2c2eb", size = 3996475 }, - { url = "https://files.pythonhosted.org/packages/ab/cb/54ca973c666e8402f3bf7feaf7e2037b7c80dbd732be67e224f95cb6a1cc/pymssql-2.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfcd63280b0f74124241092bdfd7889925342bcb58b4cde299e4c91cec55436", size = 4377615 }, - { url = "https://files.pythonhosted.org/packages/c1/f2/973dfded45e0df9dcf72bc1b7254cefd5ffb1492f314822020d3c066421f/pymssql-2.3.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f3b784563f2b24c4d3f0e250fa9cfe59a22791539725f4d5059139c66f072a14", size = 4839647 }, - { url = "https://files.pythonhosted.org/packages/91/cb/9d9342f0936ff6d58a59446e7449f93cc1134e59f3a1ec075e7b364e82a6/pymssql-2.3.4-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a54a018215cf0cffbaaa6edaa02215ef19fa9c9ff6a2c172e8fa563f577e2e91", size = 4079413 }, - { url = "https://files.pythonhosted.org/packages/9e/f1/79866247539144dcc9e44e9f8ad700bdc78c286863f37d879d71bbfd2c94/pymssql-2.3.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:14f2474fda3c57bc95b9ba146552463571fe77c816cbfb2e64344528d9afb755", size = 4141187 }, - { url = "https://files.pythonhosted.org/packages/9c/2d/c187ebcaeb2832cc7ac85034897eb920b361fd63bf011a5d02b31fe2f840/pymssql-2.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:145dc2b73e4fe115e6176866245921ce95a216a8d6cb0d9420c2e05ee2a911a9", size = 4661965 }, - { url = "https://files.pythonhosted.org/packages/77/59/aae5ba396d1c603325112bf7106705e1781e4604381faa45ad55161f2b0f/pymssql-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e08f1bc9e4a914c82816e3e5270b53bead13d3444435fc7bddfff9cb302b9982", size = 4903978 }, - { url = "https://files.pythonhosted.org/packages/3f/a9/25ea7056857aabbfd285c397084c571e4486f341ff8e8086b067bc2e2109/pymssql-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e31b507f4669671e8bbdeecf1c1c2ed9c092953a1decfae5af656200a74195d1", size = 1337662 }, +sdist = { url = "https://files.pythonhosted.org/packages/83/35/5a0b79369e42fffd5c04e4e74fa90ef034cc5c3f314e14f6d58cac646ccf/pymssql-2.3.4.tar.gz", hash = "sha256:117c82d7aa9021171aa9be98368475519f33d9c32073cdcf9b0d76231abc6436", size = 184604, upload-time = "2025-04-02T02:08:43.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/89/5a7a4b27ee44b2dc4708de7e897311cb17f15e7c983c299e8bf97ebf98d1/pymssql-2.3.4-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:809b75aaeb9bcd061230bace41e275f80f464f70fcbf5dde2ba7ba8f0eea5298", size = 3075736, upload-time = "2025-04-02T02:11:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/43/f9/19bbb0026a47043fb239e821e10a75304b12ba986ce4af71cf8986af411c/pymssql-2.3.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ab1ee04754fb8ce703b6c154e54fde4f6c7f440766d397b101b748123a12df", size = 4019433, upload-time = "2025-04-02T03:07:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ac/3aca13f1f527299db4adef594fb9f14d47d68de91b93a220a67391b8ec87/pymssql-2.3.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e22bb4d5aed85b084e3b9fb5ae3463301dd69c17703cfef72e0aed746452cc9", size = 3993550, upload-time = "2025-04-02T02:13:16.433Z" }, + { url = "https://files.pythonhosted.org/packages/b9/93/879d92f61afb974f69b9186b16ee6a97adff2abc82777e3b66c9c9efb179/pymssql-2.3.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2c1c8d3632630d52387e5b5b4483027494b5cb8f65401573715b74e7a3f16e5", size = 4381934, upload-time = "2025-04-02T02:12:45.424Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a6/923769b6dbb4e3a4c07a867e0c7fa8e4b230f675095cd7109d4e3eb9ddf0/pymssql-2.3.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f7f245acbdf89b96a41513ef0214b55a3ba2824f1f3119dd1945443b6cac78d3", size = 4849674, upload-time = "2025-04-02T02:13:05.245Z" }, + { url = "https://files.pythonhosted.org/packages/7a/2d/c787f061dcd0603905bf8085dda9cddb8c3c03b18d9239d5d18c953eebba/pymssql-2.3.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9484485fb847eb67f828459b0f4857c9725b20c517c2b7f88a9788fd72b76a6a", size = 4076649, upload-time = "2025-04-02T02:15:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a2/e55d823e3ab21cf9fc88e4e2424936899392d9d2e6569d5bcce063f84dac/pymssql-2.3.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4a0716482cd5ecce07230925593cefd9137959c18aca4c92fc24c243d3c20e38", size = 4139477, upload-time = "2025-04-02T02:13:42.91Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/0fec6587b38081d0d0fca4f9ad31e85ec6c5791879e57f0e559ec6be4d3d/pymssql-2.3.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba4f988674b361709821c8173a6471aa6e47ee6e45b5a8e30d4dcbde1f62fb0f", size = 4653837, upload-time = "2025-04-02T02:15:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7c/77d0251f4b5ad5690226a93547fc8279c1c48bd14e3ccc820f5c580a3b73/pymssql-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:51b8ebfbd7d14d5e7c65e76ffaf31584ffabe9fb1bfd2a85f529bd707512e39d", size = 4910914, upload-time = "2025-04-02T02:13:55.446Z" }, + { url = "https://files.pythonhosted.org/packages/4f/22/1b2ef85804872a5940010d3c012722356af1fa24f8ba6f419c0260881032/pymssql-2.3.4-cp312-cp312-win32.whl", hash = "sha256:c8f5718f5e7d2623eaf35e025d5fa288c5789916809a89f00b42346b888673da", size = 1337991, upload-time = "2025-04-02T02:29:43.394Z" }, + { url = "https://files.pythonhosted.org/packages/0f/43/c98f34e7b3cd45653fb233a4bee83bffca0cf5e78c290c291cec34faac21/pymssql-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:d72b38b5ba66a4072c680447099bb63ac35d0425e9a29ff91b048e563b999be5", size = 2021760, upload-time = "2025-04-02T02:28:06.757Z" }, + { url = "https://files.pythonhosted.org/packages/63/58/90dbe299359c547fcb037d4a12f2146916213b99a245d01efdf5ade52910/pymssql-2.3.4-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:36ede0bc046e18cb0a5f043828bc441c80ffb2aa4606ce0cfcbf2a3d71266f0a", size = 3064581, upload-time = "2025-04-02T02:09:43.911Z" }, + { url = "https://files.pythonhosted.org/packages/4b/7c/15e75a74de5e392ea1a9456261632cc312c873f28ac2f9ef39dfefac8cd2/pymssql-2.3.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d247114853ada387415df303d24d2e990596ce28b23f5b59c46d852cfea0f2ad", size = 4013283, upload-time = "2025-04-02T03:08:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/b9f08676145c3086db11c55b40bd58dfb0d775853f7280c1b2e15fc44fc2/pymssql-2.3.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79881cbe1a5826ddb959ccf8add015e5b82e6afbbf9cf5e281bd794278b2c2eb", size = 3996475, upload-time = "2025-04-02T02:13:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cb/54ca973c666e8402f3bf7feaf7e2037b7c80dbd732be67e224f95cb6a1cc/pymssql-2.3.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4bfcd63280b0f74124241092bdfd7889925342bcb58b4cde299e4c91cec55436", size = 4377615, upload-time = "2025-04-02T02:12:46.677Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f2/973dfded45e0df9dcf72bc1b7254cefd5ffb1492f314822020d3c066421f/pymssql-2.3.4-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f3b784563f2b24c4d3f0e250fa9cfe59a22791539725f4d5059139c66f072a14", size = 4839647, upload-time = "2025-04-02T02:13:07.216Z" }, + { url = "https://files.pythonhosted.org/packages/91/cb/9d9342f0936ff6d58a59446e7449f93cc1134e59f3a1ec075e7b364e82a6/pymssql-2.3.4-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a54a018215cf0cffbaaa6edaa02215ef19fa9c9ff6a2c172e8fa563f577e2e91", size = 4079413, upload-time = "2025-04-02T02:15:14.592Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f1/79866247539144dcc9e44e9f8ad700bdc78c286863f37d879d71bbfd2c94/pymssql-2.3.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:14f2474fda3c57bc95b9ba146552463571fe77c816cbfb2e64344528d9afb755", size = 4141187, upload-time = "2025-04-02T02:13:44.711Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2d/c187ebcaeb2832cc7ac85034897eb920b361fd63bf011a5d02b31fe2f840/pymssql-2.3.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:145dc2b73e4fe115e6176866245921ce95a216a8d6cb0d9420c2e05ee2a911a9", size = 4661965, upload-time = "2025-04-02T02:15:06.727Z" }, + { url = "https://files.pythonhosted.org/packages/77/59/aae5ba396d1c603325112bf7106705e1781e4604381faa45ad55161f2b0f/pymssql-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e08f1bc9e4a914c82816e3e5270b53bead13d3444435fc7bddfff9cb302b9982", size = 4903978, upload-time = "2025-04-02T02:13:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/25ea7056857aabbfd285c397084c571e4486f341ff8e8086b067bc2e2109/pymssql-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e31b507f4669671e8bbdeecf1c1c2ed9c092953a1decfae5af656200a74195d1", size = 1337662, upload-time = "2025-04-02T02:21:12.84Z" }, ] [[package]] name = "pymysql" version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/ce59b5e5ed4ce8512f879ff1fa5ab699d211ae2495f1adaa5fbba2a1eada/pymysql-1.1.1.tar.gz", hash = "sha256:e127611aaf2b417403c60bf4dc570124aeb4a57f5f37b8e95ae399a42f904cd0", size = 47678, upload-time = "2024-05-21T11:03:43.722Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972 }, + { url = "https://files.pythonhosted.org/packages/0c/94/e4181a1f6286f545507528c78016e00065ea913276888db2262507693ce5/PyMySQL-1.1.1-py3-none-any.whl", hash = "sha256:4de15da4c61dc132f4fb9ab763063e693d521a80fd0e87943b9a453dd4c19d6c", size = 44972, upload-time = "2024-05-21T11:03:41.216Z" }, ] [[package]] @@ -3678,17 +3678,17 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854, upload-time = "2022-01-07T22:05:41.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 }, - { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 }, - { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 }, - { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 }, - { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 }, - { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 }, - { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 }, - { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 }, - { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 }, + { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920, upload-time = "2022-01-07T22:05:49.156Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722, upload-time = "2022-01-07T22:05:50.989Z" }, + { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087, upload-time = "2022-01-07T22:05:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678, upload-time = "2022-01-07T22:05:54.251Z" }, + { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660, upload-time = "2022-01-07T22:05:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824, upload-time = "2022-01-07T22:05:57.434Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912, upload-time = "2022-01-07T22:05:58.665Z" }, + { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624, upload-time = "2022-01-07T22:06:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, ] [[package]] @@ -3699,56 +3699,56 @@ dependencies = [ { name = "cryptography" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/51/27a5ad5f939d08f690a326ef9582cda7140555180db71695f6fb747d6a36/pyopenssl-26.2.0.tar.gz", hash = "sha256:8c6fcecd1183a7fc897548dfe388b0cdb7f37e018200d8409cf33959dbe35387", size = 182195, upload-time = "2026-05-04T23:06:09.72Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823 }, + { url = "https://files.pythonhosted.org/packages/73/b8/a0e2790ae249d6f38c9f66de7a211621a7ab2650217bcd04e1262f578a56/pyopenssl-26.2.0-py3-none-any.whl", hash = "sha256:4f9d971bc5298b8bc1fab282803da04bf000c755d4ad9d99b52de2569ca19a70", size = 55823, upload-time = "2026-05-04T23:06:08.395Z" }, ] [[package]] name = "pyparsing" version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781 }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pypdf" version = "6.13.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063 } +sdist = { url = "https://files.pythonhosted.org/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7", size = 6484063, upload-time = "2026-06-17T15:22:00.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288 }, + { url = "https://files.pythonhosted.org/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224", size = 347288, upload-time = "2026-06-17T15:21:59.512Z" }, ] [[package]] name = "pypdfium2" version = "5.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/f6/42f5f1b9beb7e036f5532832b9c590fd107c52a78f704302c03bc6793954/pypdfium2-5.5.0.tar.gz", hash = "sha256:3283c61f54c3c546d140da201ef48a51c18b0ad54293091a010029ac13ece23a", size = 270502 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/c0/cdddce35108c118cc110c1c2ed16de82d74d7646b9bcf98eae2fa440966b/pypdfium2-5.5.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:414f0b4aef7413e04df7355043fb752f2efb6f9777e04fd880d302612dacf89f", size = 2760984 }, - { url = "https://files.pythonhosted.org/packages/d0/c7/23a6fbd6d23fd8dbe657696acd81fba858639ef221254ce05970152ad1d8/pypdfium2-5.5.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:126ff8b131d12f16ce96b3e85b7f413e5073212be06b571f157fe11ad221c274", size = 2303146 }, - { url = "https://files.pythonhosted.org/packages/bc/a9/379ec56c4481f39f0e37a7ce42f4844e6ddd7662571922e2b348105960ab/pypdfium2-5.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0770bd3f0be5c68443fc4017e43b1b1fe8f36877481cab70fd29b68b2c362e1b", size = 2815036 }, - { url = "https://files.pythonhosted.org/packages/91/a4/b0cc01aaae1fdf1ca4e080cc55bb432f5a2234f33209a602bc498a47850d/pypdfium2-5.5.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:5ab41a3b9953d9be44be35c36a2340f1d67c602db98a0d6f70006610871ae43a", size = 2948686 }, - { url = "https://files.pythonhosted.org/packages/26/99/25a0c71b551d100b505c618910afec0df402b230e087078c8078f8b1fcff/pypdfium2-5.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2492a22c3126a004cee2fa208ea4aa03ede2c7e205d05814934ab18f83d073e9", size = 2977311 }, - { url = "https://files.pythonhosted.org/packages/85/64/691e21539566f7a0521295948b5589d2fdfe3df5acab9c29ff410633a839/pypdfium2-5.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83ff93e08b1fadb00040564e2eccc99147fc1a632ba5daff745126b373d78446", size = 2762449 }, - { url = "https://files.pythonhosted.org/packages/74/b1/9af288557291e2964bf5ffd460b7ed1090fcb8c54addfd6c7c5deb9ba7c7/pypdfium2-5.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7e85de3332bedf8e5f157c248063b4eaf968660e1e490353b6e581d9f96a4c6", size = 3074851 }, - { url = "https://files.pythonhosted.org/packages/a4/1e/c61fddbdea5ea1ba478dc7ecc9d68069d17b858e5fed04e4e071811f0858/pypdfium2-5.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e258365f34b6e334bb415e44dd9b1ee78a6e525bf854a1e74af67af7ede7555b", size = 3423003 }, - { url = "https://files.pythonhosted.org/packages/36/5f/d2eb58c54abba3a6c3bc4c297b3a11348dd4b4deb073f1aa8a872a298278/pypdfium2-5.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec21d833404ca771f02fa5cefb0b73e2148f05cbdb3b5b9989bdd51d9b5cbac", size = 3002104 }, - { url = "https://files.pythonhosted.org/packages/1c/33/87423eec4f5d4287d5a1726dbb9f06fb1f1aebc38ff75dcff817c492769d/pypdfium2-5.5.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1dd6ccbe1b5e2e778e8b021e47f9485b4fd42eaa6c9bdda2631641724e1fcc04", size = 3097209 }, - { url = "https://files.pythonhosted.org/packages/97/0a/a3fd71f00838bba7922691107219bee67f50fbda6d12df330ef485a97848/pypdfium2-5.5.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da3eada345570cec5e34872d1472d4ac542f0e650ccdb6c2eac08ae1a5f07c82", size = 2965027 }, - { url = "https://files.pythonhosted.org/packages/75/4a/2181260bd8a0b1b30ac50b7fd6ee3366e04f3a9f1c29351d882652da7fa7/pypdfium2-5.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a087fb4088c7433fd3d78833dbe42cfb66df3d5ac98e3edf66110520fb33c0f0", size = 4131431 }, - { url = "https://files.pythonhosted.org/packages/15/bb/3ccf481191346eda11c0c208bd4e46f8de019ae7d9e9c1b660633f0bb3f4/pypdfium2-5.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6418cdc500ef85a90319f9bc7f1c54fc133460379f509429403225d8a4c157f", size = 3747468 }, - { url = "https://files.pythonhosted.org/packages/15/51/17e50ec72cf2235ac18d9cbe907859501c769d3e964818fefac6a3e10727/pypdfium2-5.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f7b66eedfac26eb2df4b00936e081b0a1c76fb8ee1c12639d85c2e73b0769ef", size = 4337579 }, - { url = "https://files.pythonhosted.org/packages/c6/e4/f9bdf06f4d3f1e56eff9d997392a00a4b66cbc9c20f33934c4edc2a7943f/pypdfium2-5.5.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:faea3246591ce2ea6218cd06679071275e3c65f11c3f5c9091eb7fb07610af6a", size = 4376104 }, - { url = "https://files.pythonhosted.org/packages/8c/20/06baf1f5d494e035f50fc895fa1da5ed652d03ecc59aeb3aabb0daa5adfc/pypdfium2-5.5.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:aba26d404b51a9de3d3e80c867a95c71abf1c79552001ae22707451e59186b3d", size = 3929824 }, - { url = "https://files.pythonhosted.org/packages/3a/01/28940e54e6936674e9a05eb58ccce7c54d8e2ac81cd84ec0b76e7d32a010/pypdfium2-5.5.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e0fa8f81679e6e71f26806f4db853571ee6435dc3bde7a46acdd182ef886a5b9", size = 4270200 }, - { url = "https://files.pythonhosted.org/packages/cb/d4/1f36c505a3770aad9a88c895a46d61fd4c0535f79548f02c93b97ff89604/pypdfium2-5.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ee22df3376d350eeb64d2002a1071e3a02c0d874c557a3cd8229a8fc572cdaac", size = 4180794 }, - { url = "https://files.pythonhosted.org/packages/ac/38/f77e7792b4fba37f0e3d78db52fb7288d41db3c46ed28906fb940bc3e325/pypdfium2-5.5.0-py3-none-win32.whl", hash = "sha256:ec62a00223d1222d2f35c0866dd79cdc24da070738544cdf51b17d332d4a7389", size = 3001772 }, - { url = "https://files.pythonhosted.org/packages/3e/c5/0d7ba53148262f78d8eee528a504764f78ae7bebf434a53714294b1fd973/pypdfium2-5.5.0-py3-none-win_amd64.whl", hash = "sha256:15c32fbeebb5198afa785dd03e98906ebb4eded9ef8862e10f833c37b4a18786", size = 3107710 }, - { url = "https://files.pythonhosted.org/packages/29/ad/fae449d2ed7b3088c6ab088f53fc6a9e9af26ccc9e0477d4182e373c4dd8/pypdfium2-5.5.0-py3-none-win_arm64.whl", hash = "sha256:f618af0884c16c768539c44933a255039131dbbf39d68eded020da4f14958d73", size = 2938315 }, +sdist = { url = "https://files.pythonhosted.org/packages/fb/f6/42f5f1b9beb7e036f5532832b9c590fd107c52a78f704302c03bc6793954/pypdfium2-5.5.0.tar.gz", hash = "sha256:3283c61f54c3c546d140da201ef48a51c18b0ad54293091a010029ac13ece23a", size = 270502, upload-time = "2026-02-18T23:22:37.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c0/cdddce35108c118cc110c1c2ed16de82d74d7646b9bcf98eae2fa440966b/pypdfium2-5.5.0-py3-none-android_23_arm64_v8a.whl", hash = "sha256:414f0b4aef7413e04df7355043fb752f2efb6f9777e04fd880d302612dacf89f", size = 2760984, upload-time = "2026-02-18T23:21:56.668Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/23a6fbd6d23fd8dbe657696acd81fba858639ef221254ce05970152ad1d8/pypdfium2-5.5.0-py3-none-android_23_armeabi_v7a.whl", hash = "sha256:126ff8b131d12f16ce96b3e85b7f413e5073212be06b571f157fe11ad221c274", size = 2303146, upload-time = "2026-02-18T23:21:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a9/379ec56c4481f39f0e37a7ce42f4844e6ddd7662571922e2b348105960ab/pypdfium2-5.5.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0770bd3f0be5c68443fc4017e43b1b1fe8f36877481cab70fd29b68b2c362e1b", size = 2815036, upload-time = "2026-02-18T23:22:00.288Z" }, + { url = "https://files.pythonhosted.org/packages/91/a4/b0cc01aaae1fdf1ca4e080cc55bb432f5a2234f33209a602bc498a47850d/pypdfium2-5.5.0-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:5ab41a3b9953d9be44be35c36a2340f1d67c602db98a0d6f70006610871ae43a", size = 2948686, upload-time = "2026-02-18T23:22:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/26/99/25a0c71b551d100b505c618910afec0df402b230e087078c8078f8b1fcff/pypdfium2-5.5.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2492a22c3126a004cee2fa208ea4aa03ede2c7e205d05814934ab18f83d073e9", size = 2977311, upload-time = "2026-02-18T23:22:03.603Z" }, + { url = "https://files.pythonhosted.org/packages/85/64/691e21539566f7a0521295948b5589d2fdfe3df5acab9c29ff410633a839/pypdfium2-5.5.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83ff93e08b1fadb00040564e2eccc99147fc1a632ba5daff745126b373d78446", size = 2762449, upload-time = "2026-02-18T23:22:05.044Z" }, + { url = "https://files.pythonhosted.org/packages/74/b1/9af288557291e2964bf5ffd460b7ed1090fcb8c54addfd6c7c5deb9ba7c7/pypdfium2-5.5.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7e85de3332bedf8e5f157c248063b4eaf968660e1e490353b6e581d9f96a4c6", size = 3074851, upload-time = "2026-02-18T23:22:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/c61fddbdea5ea1ba478dc7ecc9d68069d17b858e5fed04e4e071811f0858/pypdfium2-5.5.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e258365f34b6e334bb415e44dd9b1ee78a6e525bf854a1e74af67af7ede7555b", size = 3423003, upload-time = "2026-02-18T23:22:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/d2eb58c54abba3a6c3bc4c297b3a11348dd4b4deb073f1aa8a872a298278/pypdfium2-5.5.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec21d833404ca771f02fa5cefb0b73e2148f05cbdb3b5b9989bdd51d9b5cbac", size = 3002104, upload-time = "2026-02-18T23:22:12.035Z" }, + { url = "https://files.pythonhosted.org/packages/1c/33/87423eec4f5d4287d5a1726dbb9f06fb1f1aebc38ff75dcff817c492769d/pypdfium2-5.5.0-py3-none-manylinux_2_27_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1dd6ccbe1b5e2e778e8b021e47f9485b4fd42eaa6c9bdda2631641724e1fcc04", size = 3097209, upload-time = "2026-02-18T23:22:13.809Z" }, + { url = "https://files.pythonhosted.org/packages/97/0a/a3fd71f00838bba7922691107219bee67f50fbda6d12df330ef485a97848/pypdfium2-5.5.0-py3-none-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da3eada345570cec5e34872d1472d4ac542f0e650ccdb6c2eac08ae1a5f07c82", size = 2965027, upload-time = "2026-02-18T23:22:16.324Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/2181260bd8a0b1b30ac50b7fd6ee3366e04f3a9f1c29351d882652da7fa7/pypdfium2-5.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a087fb4088c7433fd3d78833dbe42cfb66df3d5ac98e3edf66110520fb33c0f0", size = 4131431, upload-time = "2026-02-18T23:22:18.469Z" }, + { url = "https://files.pythonhosted.org/packages/15/bb/3ccf481191346eda11c0c208bd4e46f8de019ae7d9e9c1b660633f0bb3f4/pypdfium2-5.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e6418cdc500ef85a90319f9bc7f1c54fc133460379f509429403225d8a4c157f", size = 3747468, upload-time = "2026-02-18T23:22:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/51/17e50ec72cf2235ac18d9cbe907859501c769d3e964818fefac6a3e10727/pypdfium2-5.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8f7b66eedfac26eb2df4b00936e081b0a1c76fb8ee1c12639d85c2e73b0769ef", size = 4337579, upload-time = "2026-02-18T23:22:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/f9bdf06f4d3f1e56eff9d997392a00a4b66cbc9c20f33934c4edc2a7943f/pypdfium2-5.5.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:faea3246591ce2ea6218cd06679071275e3c65f11c3f5c9091eb7fb07610af6a", size = 4376104, upload-time = "2026-02-18T23:22:25.337Z" }, + { url = "https://files.pythonhosted.org/packages/8c/20/06baf1f5d494e035f50fc895fa1da5ed652d03ecc59aeb3aabb0daa5adfc/pypdfium2-5.5.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:aba26d404b51a9de3d3e80c867a95c71abf1c79552001ae22707451e59186b3d", size = 3929824, upload-time = "2026-02-18T23:22:26.889Z" }, + { url = "https://files.pythonhosted.org/packages/3a/01/28940e54e6936674e9a05eb58ccce7c54d8e2ac81cd84ec0b76e7d32a010/pypdfium2-5.5.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e0fa8f81679e6e71f26806f4db853571ee6435dc3bde7a46acdd182ef886a5b9", size = 4270200, upload-time = "2026-02-18T23:22:28.668Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d4/1f36c505a3770aad9a88c895a46d61fd4c0535f79548f02c93b97ff89604/pypdfium2-5.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ee22df3376d350eeb64d2002a1071e3a02c0d874c557a3cd8229a8fc572cdaac", size = 4180794, upload-time = "2026-02-18T23:22:30.11Z" }, + { url = "https://files.pythonhosted.org/packages/ac/38/f77e7792b4fba37f0e3d78db52fb7288d41db3c46ed28906fb940bc3e325/pypdfium2-5.5.0-py3-none-win32.whl", hash = "sha256:ec62a00223d1222d2f35c0866dd79cdc24da070738544cdf51b17d332d4a7389", size = 3001772, upload-time = "2026-02-18T23:22:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c5/0d7ba53148262f78d8eee528a504764f78ae7bebf434a53714294b1fd973/pypdfium2-5.5.0-py3-none-win_amd64.whl", hash = "sha256:15c32fbeebb5198afa785dd03e98906ebb4eded9ef8862e10f833c37b4a18786", size = 3107710, upload-time = "2026-02-18T23:22:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/29/ad/fae449d2ed7b3088c6ab088f53fc6a9e9af26ccc9e0477d4182e373c4dd8/pypdfium2-5.5.0-py3-none-win_arm64.whl", hash = "sha256:f618af0884c16c768539c44933a255039131dbbf39d68eded020da4f14958d73", size = 2938315, upload-time = "2026-02-18T23:22:35.907Z" }, ] [[package]] @@ -3762,9 +3762,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165 } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249 }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -3775,9 +3775,9 @@ dependencies = [ { name = "pytest" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, ] [[package]] @@ -3789,9 +3789,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424 }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] @@ -3801,9 +3801,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -3813,9 +3813,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 }, + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] [[package]] @@ -3825,18 +3825,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] @@ -3846,18 +3846,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "simple-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348 } +sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847 }, + { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, ] [[package]] name = "python-magic" version = "0.4.27" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677 } +sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840 }, + { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, ] [[package]] @@ -3868,47 +3868,47 @@ dependencies = [ { name = "bidict" }, { name = "python-engineio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508 } +sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054 }, + { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, ] [[package]] name = "pytokens" version = "0.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663 }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626 }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779 }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076 }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552 }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720 }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204 }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423 }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859 }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520 }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821 }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263 }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071 }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716 }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539 }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474 }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473 }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485 }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698 }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287 }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729 }, +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] @@ -3916,61 +3916,61 @@ name = "pywin32" version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] [[package]] @@ -3986,18 +3986,18 @@ dependencies = [ { name = "pydantic" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112, upload-time = "2025-12-12T10:58:30.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186 }, + { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186, upload-time = "2025-12-12T10:58:29.282Z" }, ] [[package]] name = "redis" version = "5.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355 } +sdist = { url = "https://files.pythonhosted.org/packages/47/da/d283a37303a995cd36f8b92db85135153dc4f7a8e4441aa827721b442cfb/redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f", size = 4608355, upload-time = "2024-12-06T09:50:41.956Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4", size = 261502, upload-time = "2024-12-06T09:50:39.656Z" }, ] [[package]] @@ -4009,97 +4009,97 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, + { 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" }, ] [[package]] name = "regex" version = "2026.2.28" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574 }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426 }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200 }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765 }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093 }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455 }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037 }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113 }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194 }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846 }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516 }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278 }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068 }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416 }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297 }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408 }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311 }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285 }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051 }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842 }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083 }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412 }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311 }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876 }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632 }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320 }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152 }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398 }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282 }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382 }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541 }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984 }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509 }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429 }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422 }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175 }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044 }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056 }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743 }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633 }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862 }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788 }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184 }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137 }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682 }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735 }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497 }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295 }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275 }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176 }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528 }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373 }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859 }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813 }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705 }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734 }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871 }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825 }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548 }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444 }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546 }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986 }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518 }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464 }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553 }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289 }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156 }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215 }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925 }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701 }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899 }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727 }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366 }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936 }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779 }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010 }, +sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, + { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, + { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, + { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, + { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, + { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, + { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, + { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, + { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, + { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, + { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, + { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, + { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, + { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, + { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, + { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, + { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, + { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, + { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, + { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, + { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, + { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, + { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, + { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, + { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, + { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, + { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, + { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, + { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, ] [[package]] @@ -4112,9 +4112,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232 } +sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017 }, + { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] [[package]] @@ -4125,9 +4125,9 @@ dependencies = [ { name = "oauthlib" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] [[package]] @@ -4137,9 +4137,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481 }, + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, ] [[package]] @@ -4151,9 +4151,9 @@ dependencies = [ { name = "requests" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099 }, + { url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" }, ] [[package]] @@ -4164,90 +4164,90 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458 }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, +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/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { 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]] @@ -4257,9 +4257,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] @@ -4271,9 +4271,9 @@ dependencies = [ { name = "aiohttp" }, { name = "fsspec" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/75/65/4b4c868cff76c036d11dc75dd91e5696dbf16ce626514166f35d5f4a930f/s3fs-2024.10.0.tar.gz", hash = "sha256:58b8c3650f8b99dbedf361543da3533aac8707035a104db5d80b094617ad4a3f", size = 75916 } +sdist = { url = "https://files.pythonhosted.org/packages/75/65/4b4c868cff76c036d11dc75dd91e5696dbf16ce626514166f35d5f4a930f/s3fs-2024.10.0.tar.gz", hash = "sha256:58b8c3650f8b99dbedf361543da3533aac8707035a104db5d80b094617ad4a3f", size = 75916, upload-time = "2024-10-21T01:45:49.967Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/44/bb9ff095ae7b1b6908480f683b6ca6b71c2105d343a5e5cb25334b01f5fa/s3fs-2024.10.0-py3-none-any.whl", hash = "sha256:7a2025d60d5b1a6025726b3a5e292a8e5aa713abc3b16fd1f81735181f7bb282", size = 29855 }, + { url = "https://files.pythonhosted.org/packages/99/44/bb9ff095ae7b1b6908480f683b6ca6b71c2105d343a5e5cb25334b01f5fa/s3fs-2024.10.0-py3-none-any.whl", hash = "sha256:7a2025d60d5b1a6025726b3a5e292a8e5aa713abc3b16fd1f81735181f7bb282", size = 29855, upload-time = "2024-10-21T01:45:47.905Z" }, ] [package.optional-dependencies] @@ -4288,27 +4288,27 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/0a/1cdbabf9edd0ea7747efdf6c9ab4e7061b085aa7f9bfc36bb1601563b069/s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7", size = 145287, upload-time = "2024-11-20T21:06:05.981Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175 }, + { url = "https://files.pythonhosted.org/packages/66/05/7957af15543b8c9799209506df4660cba7afc4cf94bfb60513827e96bed6/s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e", size = 83175, upload-time = "2024-11-20T21:06:03.961Z" }, ] [[package]] name = "setuptools" version = "82.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893 } +sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468 }, + { url = "https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0", size = 1003468, upload-time = "2026-02-08T15:08:38.723Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] @@ -4318,33 +4318,33 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wsproto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300 } +sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842 }, + { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, ] [[package]] name = "singleton-decorator" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/33/98/a8b5c919bee1152a9a1afd82014431f8db5882699754de50d1b3aba4d136/singleton-decorator-1.0.0.tar.gz", hash = "sha256:1a90ad8a8a738be591c9c167fdd677c5d4a43d1bc6b1c128227be1c5e03bee07", size = 2791 } +sdist = { url = "https://files.pythonhosted.org/packages/33/98/a8b5c919bee1152a9a1afd82014431f8db5882699754de50d1b3aba4d136/singleton-decorator-1.0.0.tar.gz", hash = "sha256:1a90ad8a8a738be591c9c167fdd677c5d4a43d1bc6b1c128227be1c5e03bee07", size = 2791, upload-time = "2017-08-10T19:52:45.903Z" } [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "sniffio" version = "1.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -4370,23 +4370,23 @@ dependencies = [ { name = "tomlkit" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/49/4d/7e6a9088381386b4cfae4c5d1d23ea0c3618ca694bc2290118737af59f36/snowflake_connector_python-4.6.0.tar.gz", hash = "sha256:06e2dba02703da6fd60e07bb0574506f810a85e5831d3461247753ecce4b8335", size = 937999 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/4e/a839eddf87df7fe91fd8086e6a43e10e6afddf7c6b718ef036643f032867/snowflake_connector_python-4.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a7701b702dbeb348769c5d1248231e18544c4ff1fb4118ad73d48e8f801cfb6e", size = 1167890 }, - { url = "https://files.pythonhosted.org/packages/7d/81/632b4ca9459cd801abfaa5396a60d9e60b9e2f051d015a577af0493782d3/snowflake_connector_python-4.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:00abbcfe958f60da18297191f3499b1e61802e64622521a2e8da1c059c14e1c0", size = 1181169 }, - { url = "https://files.pythonhosted.org/packages/c9/31/79871d7eea206c60a7891a8d4349fdd8933822101af87204231162a5c3e8/snowflake_connector_python-4.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:72aaee21a70e00fbe4dadcc60b9b1012b6411dddc90f94804d5efe5706fb9621", size = 2878875 }, - { url = "https://files.pythonhosted.org/packages/e5/ff/ea43b9f87cf632bd9735f4da18d7982572fb67073fd55c67841091a20f1a/snowflake_connector_python-4.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d3f6120edeb0d6edd208831d006cc3e769ec51bc346727f22d7aeaecbf20f77", size = 2910491 }, - { url = "https://files.pythonhosted.org/packages/52/b1/80bc142ce5afee2e9b0520e4444bcdf1a02627c1066653705e4c36b475ab/snowflake_connector_python-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f15e2493a316ce79ab3d7fb16add10252bb2401723e5cfbc7a2ebc44d89a7b2b", size = 5388193 }, - { url = "https://files.pythonhosted.org/packages/cd/7b/29af48b122f5df4e2c23a1733bd5ed28193f24734a7cf48e345e5c7c3012/snowflake_connector_python-4.6.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0ca5a035b1afa690fb36a767ba59c8db85ef6295b88c2bbc2040449e99992ad", size = 1166660 }, - { url = "https://files.pythonhosted.org/packages/20/af/9c5f1551278a309bbda06662e842b34fc17a60916032e5402033482c0367/snowflake_connector_python-4.6.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:1894504c69a76ac4a205d01fbb3e18c6a6e974e6ad26dad263edd06343bea501", size = 1179744 }, - { url = "https://files.pythonhosted.org/packages/4e/ef/fdaf6150dacf80edd4dac948fd9a08930944d2ad2e978fe33aca598aa0a5/snowflake_connector_python-4.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ed40d1e9d867253596860b9d5240280489ff4692b7a3fa21e2d45d63b4b61d36", size = 2844736 }, - { url = "https://files.pythonhosted.org/packages/da/a1/25fdb592dfed3150b429f1bbb22b495c2590e5a5007153be9d1b798c72c9/snowflake_connector_python-4.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c8476781cfef961fc5f6f75a5238e668d3e0ca5ebf1d055661b2fcf2831c254", size = 2878174 }, - { url = "https://files.pythonhosted.org/packages/29/1f/081d2fb06fca926bb2e9af81533516af4f86ca13abe2b7cbb16ee4938339/snowflake_connector_python-4.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8ccbf8b5e12177a86bd3ab8292cc5a99e9ac97d7645ef4a3ed0f767b4ec6594", size = 5388257 }, - { url = "https://files.pythonhosted.org/packages/31/db/4de9e9c82082441c09abad9d7fe30170c8101ecdfb012affab0383401fe2/snowflake_connector_python-4.6.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1fe93d88278a0b7e0efde6140890bc298a49fbf1e04968a35aa22c801131cced", size = 1167356 }, - { url = "https://files.pythonhosted.org/packages/3f/05/3a946e69712c178b0de355971c49e1b2afd259b8dc3992c5f8898214f9fd/snowflake_connector_python-4.6.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0829d57467bf1bb5af411f6e7723058cb2218fb7df07cf15d912e3b1a2c126eb", size = 1179787 }, - { url = "https://files.pythonhosted.org/packages/4d/c6/2b367aa04fb6f6d8e6da22908dd8f61f49ba613306648a2b35b61fb70cd4/snowflake_connector_python-4.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:676162cd45df744aa966483960d34bf204cdcae87cecad77fba970f1c2fd570d", size = 2845398 }, - { url = "https://files.pythonhosted.org/packages/b7/94/ec61dfbad2d70131c46605a52487733bc98e2d7b26ddd32334f1c4db104d/snowflake_connector_python-4.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eab420406a38ebc059100bb1faa55d7d6306bb224cefadb739ec3cafeff65384", size = 2874335 }, - { url = "https://files.pythonhosted.org/packages/af/de/0a816b9877948f60071a5852b1b97a4605475fce4704f04f89d7ca9f43f2/snowflake_connector_python-4.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:9dd8689123a7e7b873db0846f2d92745a02062b16665d20634fbaf34a9c88e7a", size = 5446341 }, +sdist = { url = "https://files.pythonhosted.org/packages/49/4d/7e6a9088381386b4cfae4c5d1d23ea0c3618ca694bc2290118737af59f36/snowflake_connector_python-4.6.0.tar.gz", hash = "sha256:06e2dba02703da6fd60e07bb0574506f810a85e5831d3461247753ecce4b8335", size = 937999, upload-time = "2026-05-28T13:01:48.582Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/4e/a839eddf87df7fe91fd8086e6a43e10e6afddf7c6b718ef036643f032867/snowflake_connector_python-4.6.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a7701b702dbeb348769c5d1248231e18544c4ff1fb4118ad73d48e8f801cfb6e", size = 1167890, upload-time = "2026-05-28T13:01:56.567Z" }, + { url = "https://files.pythonhosted.org/packages/7d/81/632b4ca9459cd801abfaa5396a60d9e60b9e2f051d015a577af0493782d3/snowflake_connector_python-4.6.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:00abbcfe958f60da18297191f3499b1e61802e64622521a2e8da1c059c14e1c0", size = 1181169, upload-time = "2026-05-28T13:01:58.16Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/79871d7eea206c60a7891a8d4349fdd8933822101af87204231162a5c3e8/snowflake_connector_python-4.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:72aaee21a70e00fbe4dadcc60b9b1012b6411dddc90f94804d5efe5706fb9621", size = 2878875, upload-time = "2026-05-28T13:01:36.26Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ff/ea43b9f87cf632bd9735f4da18d7982572fb67073fd55c67841091a20f1a/snowflake_connector_python-4.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d3f6120edeb0d6edd208831d006cc3e769ec51bc346727f22d7aeaecbf20f77", size = 2910491, upload-time = "2026-05-28T13:01:37.957Z" }, + { url = "https://files.pythonhosted.org/packages/52/b1/80bc142ce5afee2e9b0520e4444bcdf1a02627c1066653705e4c36b475ab/snowflake_connector_python-4.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:f15e2493a316ce79ab3d7fb16add10252bb2401723e5cfbc7a2ebc44d89a7b2b", size = 5388193, upload-time = "2026-05-28T13:02:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/29af48b122f5df4e2c23a1733bd5ed28193f24734a7cf48e345e5c7c3012/snowflake_connector_python-4.6.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0ca5a035b1afa690fb36a767ba59c8db85ef6295b88c2bbc2040449e99992ad", size = 1166660, upload-time = "2026-05-28T13:01:59.64Z" }, + { url = "https://files.pythonhosted.org/packages/20/af/9c5f1551278a309bbda06662e842b34fc17a60916032e5402033482c0367/snowflake_connector_python-4.6.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:1894504c69a76ac4a205d01fbb3e18c6a6e974e6ad26dad263edd06343bea501", size = 1179744, upload-time = "2026-05-28T13:02:01.254Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/fdaf6150dacf80edd4dac948fd9a08930944d2ad2e978fe33aca598aa0a5/snowflake_connector_python-4.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ed40d1e9d867253596860b9d5240280489ff4692b7a3fa21e2d45d63b4b61d36", size = 2844736, upload-time = "2026-05-28T13:01:40.001Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/25fdb592dfed3150b429f1bbb22b495c2590e5a5007153be9d1b798c72c9/snowflake_connector_python-4.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1c8476781cfef961fc5f6f75a5238e668d3e0ca5ebf1d055661b2fcf2831c254", size = 2878174, upload-time = "2026-05-28T13:01:42.448Z" }, + { url = "https://files.pythonhosted.org/packages/29/1f/081d2fb06fca926bb2e9af81533516af4f86ca13abe2b7cbb16ee4938339/snowflake_connector_python-4.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8ccbf8b5e12177a86bd3ab8292cc5a99e9ac97d7645ef4a3ed0f767b4ec6594", size = 5388257, upload-time = "2026-05-28T13:02:13.073Z" }, + { url = "https://files.pythonhosted.org/packages/31/db/4de9e9c82082441c09abad9d7fe30170c8101ecdfb012affab0383401fe2/snowflake_connector_python-4.6.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1fe93d88278a0b7e0efde6140890bc298a49fbf1e04968a35aa22c801131cced", size = 1167356, upload-time = "2026-05-28T13:02:02.706Z" }, + { url = "https://files.pythonhosted.org/packages/3f/05/3a946e69712c178b0de355971c49e1b2afd259b8dc3992c5f8898214f9fd/snowflake_connector_python-4.6.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0829d57467bf1bb5af411f6e7723058cb2218fb7df07cf15d912e3b1a2c126eb", size = 1179787, upload-time = "2026-05-28T13:02:04.387Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c6/2b367aa04fb6f6d8e6da22908dd8f61f49ba613306648a2b35b61fb70cd4/snowflake_connector_python-4.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:676162cd45df744aa966483960d34bf204cdcae87cecad77fba970f1c2fd570d", size = 2845398, upload-time = "2026-05-28T13:01:44.538Z" }, + { url = "https://files.pythonhosted.org/packages/b7/94/ec61dfbad2d70131c46605a52487733bc98e2d7b26ddd32334f1c4db104d/snowflake_connector_python-4.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:eab420406a38ebc059100bb1faa55d7d6306bb224cefadb739ec3cafeff65384", size = 2874335, upload-time = "2026-05-28T13:01:46.742Z" }, + { url = "https://files.pythonhosted.org/packages/af/de/0a816b9877948f60071a5852b1b97a4605475fce4704f04f89d7ca9f43f2/snowflake_connector_python-4.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:9dd8689123a7e7b873db0846f2d92745a02062b16665d20634fbaf34a9c88e7a", size = 5446341, upload-time = "2026-05-28T13:02:16.985Z" }, ] [package.optional-dependencies] @@ -4399,18 +4399,18 @@ pandas = [ name = "sortedcontainers" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, ] [[package]] name = "soupsieve" version = "2.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016 }, + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, ] [[package]] @@ -4421,42 +4421,42 @@ dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687 }, - { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978 }, - { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939 }, - { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648 }, - { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695 }, - { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502 }, - { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435 }, - { url = "https://files.pythonhosted.org/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143", size = 2152551 }, - { url = "https://files.pythonhosted.org/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6", size = 3278782 }, - { url = "https://files.pythonhosted.org/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5", size = 3295155 }, - { url = "https://files.pythonhosted.org/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092", size = 3226834 }, - { url = "https://files.pythonhosted.org/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01", size = 3265001 }, - { url = "https://files.pythonhosted.org/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476", size = 2113647 }, - { url = "https://files.pythonhosted.org/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66", size = 2139425 }, - { url = "https://files.pythonhosted.org/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003", size = 3558809 }, - { url = "https://files.pythonhosted.org/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487", size = 3525480 }, - { url = "https://files.pythonhosted.org/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb", size = 3466569 }, - { url = "https://files.pythonhosted.org/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e", size = 3475770 }, - { url = "https://files.pythonhosted.org/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc", size = 2141300 }, - { url = "https://files.pythonhosted.org/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42", size = 2173053 }, - { url = "https://files.pythonhosted.org/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6", size = 2154355 }, - { url = "https://files.pythonhosted.org/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44", size = 3274486 }, - { url = "https://files.pythonhosted.org/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6", size = 3279481 }, - { url = "https://files.pythonhosted.org/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f", size = 3224269 }, - { url = "https://files.pythonhosted.org/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f", size = 3246262 }, - { url = "https://files.pythonhosted.org/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db", size = 2116528 }, - { url = "https://files.pythonhosted.org/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb", size = 2142181 }, - { url = "https://files.pythonhosted.org/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64", size = 3563477 }, - { url = "https://files.pythonhosted.org/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172", size = 3528568 }, - { url = "https://files.pythonhosted.org/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4", size = 3472284 }, - { url = "https://files.pythonhosted.org/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706", size = 3478410 }, - { url = "https://files.pythonhosted.org/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff", size = 2147164 }, - { url = "https://files.pythonhosted.org/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7", size = 2182154 }, - { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159 }, +sdist = { url = "https://files.pythonhosted.org/packages/cd/4b/1e00561093fe2cd8eef09d406da003c8a118ff02d6548498c1ae677d68d9/sqlalchemy-2.0.47.tar.gz", hash = "sha256:e3e7feb57b267fe897e492b9721ae46d5c7de6f9e8dee58aacf105dc4e154f3d", size = 9886323, upload-time = "2026-02-24T16:34:27.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/88/74eb470223ff88ea6572a132c0b8de8c1d8ed7b843d3b44a8a3c77f31d39/sqlalchemy-2.0.47-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fa91b19d6b9821c04cc8f7aa2476429cc8887b9687c762815aa629f5c0edec1", size = 2155687, upload-time = "2026-02-24T17:05:46.451Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ba/1447d3d558971b036cb93b557595cb5dcdfe728f1c7ac4dec16505ef5756/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c5bbbd14eff577c8c79cbfe39a0771eecd20f430f3678533476f0087138f356", size = 3336978, upload-time = "2026-02-24T17:18:04.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/07/b47472d2ffd0776826f17ccf0b4d01b224c99fbd1904aeb103dffbb4b1cc/sqlalchemy-2.0.47-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a6c555da8d4280a3c4c78c5b7a3f990cee2b2884e5f934f87a226191682ff7", size = 3349939, upload-time = "2026-02-24T17:27:18.937Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/95fa32b79b57769da3e16f054cf658d90940317b5ca0ec20eac84aa19c4f/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ed48a1701d24dff3bb49a5bce94d6bc84cbe33d98af2aa2d3cdcce3dea1709ec", size = 3279648, upload-time = "2026-02-24T17:18:07.038Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c8/3d07e7c73928dc59a0bed40961ca4e313e797bce650b088e8d5fdd3ad939/sqlalchemy-2.0.47-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f3178c920ad98158f0b6309382194df04b14808fa6052ae07099fdde29d5602", size = 3314695, upload-time = "2026-02-24T17:27:20.93Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d2/ed32b1611c1e19fdb028eee1adc5a9aa138c2952d09ae11f1670170f80ae/sqlalchemy-2.0.47-cp312-cp312-win32.whl", hash = "sha256:b9c11ac9934dd59ece9619fe42780a08abe2faab7b0543bb00d5eabea4f421b9", size = 2115502, upload-time = "2026-02-24T17:22:52.546Z" }, + { url = "https://files.pythonhosted.org/packages/fd/52/9de590356a4dd8e9ef5a881dbba64b2bbc4cbc71bf02bc68e775fb9b1899/sqlalchemy-2.0.47-cp312-cp312-win_amd64.whl", hash = "sha256:db43b72cf8274a99e089755c9c1e0b947159b71adbc2c83c3de2e38d5d607acb", size = 2142435, upload-time = "2026-02-24T17:22:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/0af64ce7d8f60ec5328c10084e2f449e7912a9b8bdbefdcfb44454a25f49/sqlalchemy-2.0.47-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:456a135b790da5d3c6b53d0ef71ac7b7d280b7f41eb0c438986352bf03ca7143", size = 2152551, upload-time = "2026-02-24T17:05:47.675Z" }, + { url = "https://files.pythonhosted.org/packages/63/79/746b8d15f6940e2ac469ce22d7aa5b1124b1ab820bad9b046eb3000c88a6/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09a2f7698e44b3135433387da5d8846cf7cc7c10e5425af7c05fee609df978b6", size = 3278782, upload-time = "2026-02-24T17:18:10.012Z" }, + { url = "https://files.pythonhosted.org/packages/91/b1/bd793ddb34345d1ed43b13ab2d88c95d7d4eb2e28f5b5a99128b9cc2bca2/sqlalchemy-2.0.47-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bbc72e6a177c78d724f9106aaddc0d26a2ada89c6332b5935414eccf04cbd5", size = 3295155, upload-time = "2026-02-24T17:27:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/97/84/7213def33f94e5ca6f5718d259bc9f29de0363134648425aa218d4356b23/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:75460456b043b78b6006e41bdf5b86747ee42eafaf7fffa3b24a6e9a456a2092", size = 3226834, upload-time = "2026-02-24T17:18:11.465Z" }, + { url = "https://files.pythonhosted.org/packages/ef/06/456810204f4dc29b5f025b1b0a03b4bd6b600ebf3c1040aebd90a257fa33/sqlalchemy-2.0.47-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d9adaa616c3bc7d80f9ded57cd84b51d6617cad6a5456621d858c9f23aaee01", size = 3265001, upload-time = "2026-02-24T17:27:24.813Z" }, + { url = "https://files.pythonhosted.org/packages/fb/20/df3920a4b2217dbd7390a5bd277c1902e0393f42baaf49f49b3c935e7328/sqlalchemy-2.0.47-cp313-cp313-win32.whl", hash = "sha256:76e09f974382a496a5ed985db9343628b1cb1ac911f27342e4cc46a8bac10476", size = 2113647, upload-time = "2026-02-24T17:22:55.747Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/7873ddf69918efbfabd7211829f4bd8019739d0a719253112d305d3ba51d/sqlalchemy-2.0.47-cp313-cp313-win_amd64.whl", hash = "sha256:0664089b0bf6724a0bfb49a0cf4d4da24868a0a5c8e937cd7db356d5dcdf2c66", size = 2139425, upload-time = "2026-02-24T17:22:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/54/fa/61ad9731370c90ac7ea5bf8f5eaa12c48bb4beec41c0fa0360becf4ac10d/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed0c967c701ae13da98eb220f9ddab3044ab63504c1ba24ad6a59b26826ad003", size = 3558809, upload-time = "2026-02-24T17:12:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/33/d5/221fac96f0529391fe374875633804c866f2b21a9c6d3a6ca57d9c12cfd7/sqlalchemy-2.0.47-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3537943a61fd25b241e976426a0c6814434b93cf9b09d39e8e78f3c9eb9a487", size = 3525480, upload-time = "2026-02-24T17:27:59.602Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/8247d53998c3673e4a8d1958eba75c6f5cc3b39082029d400bb1f2a911ae/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:57f7e336a64a0dba686c66392d46b9bc7af2c57d55ce6dc1697b4ef32b043ceb", size = 3466569, upload-time = "2026-02-24T17:12:16.94Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/c1f0eea1bac6790845f71420a7fe2f2a0566203aa57543117d4af3b77d1c/sqlalchemy-2.0.47-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dff735a621858680217cb5142b779bad40ef7322ddbb7c12062190db6879772e", size = 3475770, upload-time = "2026-02-24T17:28:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ed/2f43f92474ea0c43c204657dc47d9d002cd738b96ca2af8e6d29a9b5e42d/sqlalchemy-2.0.47-cp313-cp313t-win32.whl", hash = "sha256:3893dc096bb3cca9608ea3487372ffcea3ae9b162f40e4d3c51dd49db1d1b2dc", size = 2141300, upload-time = "2026-02-24T17:14:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a9/8b73f9f1695b6e92f7aaf1711135a1e3bbeb78bca9eded35cb79180d3c6d/sqlalchemy-2.0.47-cp313-cp313t-win_amd64.whl", hash = "sha256:b5103427466f4b3e61f04833ae01f9a914b1280a2a8bcde3a9d7ab11f3755b42", size = 2173053, upload-time = "2026-02-24T17:14:38.688Z" }, + { url = "https://files.pythonhosted.org/packages/c1/30/98243209aae58ed80e090ea988d5182244ca7ab3ff59e6d850c3dfc7651e/sqlalchemy-2.0.47-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b03010a5a5dfe71676bc83f2473ebe082478e32d77e6f082c8fe15a31c3b42a6", size = 2154355, upload-time = "2026-02-24T17:05:48.959Z" }, + { url = "https://files.pythonhosted.org/packages/ab/62/12ca6ea92055fe486d6558a2a4efe93e194ff597463849c01f88e5adb99d/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8e3371aa9024520883a415a09cc20c33cfd3eeccf9e0f4f4c367f940b9cbd44", size = 3274486, upload-time = "2026-02-24T17:18:13.659Z" }, + { url = "https://files.pythonhosted.org/packages/97/88/7dfbdeaa8d42b1584e65d6cc713e9d33b6fa563e0d546d5cb87e545bb0e5/sqlalchemy-2.0.47-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9449f747e50d518c6e1b40cc379e48bfc796453c47b15e627ea901c201e48a6", size = 3279481, upload-time = "2026-02-24T17:27:26.491Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b7/75e1c1970616a9dd64a8a6fd788248da2ddaf81c95f4875f2a1e8aee4128/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:21410f60d5cac1d6bfe360e05bd91b179be4fa0aa6eea6be46054971d277608f", size = 3224269, upload-time = "2026-02-24T17:18:15.078Z" }, + { url = "https://files.pythonhosted.org/packages/31/ac/eec1a13b891df9a8bc203334caf6e6aac60b02f61b018ef3b4124b8c4120/sqlalchemy-2.0.47-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:819841dd5bb4324c284c09e2874cf96fe6338bfb57a64548d9b81a4e39c9871f", size = 3246262, upload-time = "2026-02-24T17:27:27.986Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b0/661b0245b06421058610da39f8ceb34abcc90b49f90f256380968d761dbe/sqlalchemy-2.0.47-cp314-cp314-win32.whl", hash = "sha256:e255ee44821a7ef45649c43064cf94e74f81f61b4df70547304b97a351e9b7db", size = 2116528, upload-time = "2026-02-24T17:22:59.363Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ef/1035a90d899e61810791c052004958be622a2cf3eb3df71c3fe20778c5d0/sqlalchemy-2.0.47-cp314-cp314-win_amd64.whl", hash = "sha256:209467ff73ea1518fe1a5aaed9ba75bb9e33b2666e2553af9ccd13387bf192cb", size = 2142181, upload-time = "2026-02-24T17:23:01.001Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/17a1dd09cbba91258218ceb582225f14b5364d2683f9f5a274f72f2d764f/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e78fd9186946afaa287f8a1fe147ead06e5d566b08c0afcb601226e9c7322a64", size = 3563477, upload-time = "2026-02-24T17:12:18.46Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/1a03d24c40cc321ef2f2231f05420d140bb06a84f7047eaa7eaa21d230ba/sqlalchemy-2.0.47-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5740e2f31b5987ed9619d6912ae5b750c03637f2078850da3002934c9532f172", size = 3528568, upload-time = "2026-02-24T17:28:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/d56a213055d6b038a5384f0db5ece7343334aca230ff3f0fa1561106f22c/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb9ac00d03de93acb210e8ec7243fefe3e012515bf5fd2f0898c8dff38bc77a4", size = 3472284, upload-time = "2026-02-24T17:12:20.319Z" }, + { url = "https://files.pythonhosted.org/packages/ff/19/c235d81b9cfdd6130bf63143b7bade0dc4afa46c4b634d5d6b2a96bea233/sqlalchemy-2.0.47-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c72a0b9eb2672d70d112cb149fbaf172d466bc691014c496aaac594f1988e706", size = 3478410, upload-time = "2026-02-24T17:28:05.892Z" }, + { url = "https://files.pythonhosted.org/packages/0e/db/cafdeca5ecdaa3bb0811ba5449501da677ce0d83be8d05c5822da72d2e86/sqlalchemy-2.0.47-cp314-cp314t-win32.whl", hash = "sha256:c200db1128d72a71dc3c31c24b42eb9fd85b2b3e5a3c9ba1e751c11ac31250ff", size = 2147164, upload-time = "2026-02-24T17:14:40.783Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/ff41a010e9e0f76418b02ad352060a4341bb15f0af66cedc924ab376c7c6/sqlalchemy-2.0.47-cp314-cp314t-win_amd64.whl", hash = "sha256:669837759b84e575407355dcff912835892058aea9b80bd1cb76d6a151cf37f7", size = 2182154, upload-time = "2026-02-24T17:14:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/15/9f/7c378406b592fcf1fc157248607b495a40e3202ba4a6f1372a2ba6447717/sqlalchemy-2.0.47-py3-none-any.whl", hash = "sha256:e2647043599297a1ef10e720cf310846b7f31b6c841fee093d2b09d81215eb93", size = 1940159, upload-time = "2026-02-24T17:15:07.158Z" }, ] [package.optional-dependencies] @@ -4472,27 +4472,27 @@ dependencies = [ { name = "ply" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/6f/ef25bbc1aefeb9c905d527f1d3cd3f41f22f40566d33001b8bb14ae0cdaf/stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50", size = 190888 } +sdist = { url = "https://files.pythonhosted.org/packages/99/6f/ef25bbc1aefeb9c905d527f1d3cd3f41f22f40566d33001b8bb14ae0cdaf/stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50", size = 190888, upload-time = "2022-01-25T21:32:16.729Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/92/d0c83f63d3518e5f0b8a311937c31347349ec9a47b209ddc17f7566f58fc/stone-3.3.1-py3-none-any.whl", hash = "sha256:e15866fad249c11a963cce3bdbed37758f2e88c8ff4898616bc0caeb1e216047", size = 162257 }, + { url = "https://files.pythonhosted.org/packages/5c/92/d0c83f63d3518e5f0b8a311937c31347349ec9a47b209ddc17f7566f58fc/stone-3.3.1-py3-none-any.whl", hash = "sha256:e15866fad249c11a963cce3bdbed37758f2e88c8ff4898616bc0caeb1e216047", size = 162257, upload-time = "2022-01-25T21:32:15.155Z" }, ] [[package]] name = "striprtf" version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/20/3d419008265346452d09e5dadfd5d045b64b40d8fc31af40588e6c76997a/striprtf-0.0.26.tar.gz", hash = "sha256:fdb2bba7ac440072d1c41eab50d8d74ae88f60a8b6575c6e2c7805dc462093aa", size = 6258 } +sdist = { url = "https://files.pythonhosted.org/packages/25/20/3d419008265346452d09e5dadfd5d045b64b40d8fc31af40588e6c76997a/striprtf-0.0.26.tar.gz", hash = "sha256:fdb2bba7ac440072d1c41eab50d8d74ae88f60a8b6575c6e2c7805dc462093aa", size = 6258, upload-time = "2023-07-20T14:30:36.29Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/cf/0fea4f4ba3fc2772ac2419278aa9f6964124d4302117d61bc055758e000c/striprtf-0.0.26-py3-none-any.whl", hash = "sha256:8c8f9d32083cdc2e8bfb149455aa1cc5a4e0a035893bedc75db8b73becb3a1bb", size = 6914 }, + { url = "https://files.pythonhosted.org/packages/a3/cf/0fea4f4ba3fc2772ac2419278aa9f6964124d4302117d61bc055758e000c/striprtf-0.0.26-py3-none-any.whl", hash = "sha256:8c8f9d32083cdc2e8bfb149455aa1cc5a4e0a035893bedc75db8b73becb3a1bb", size = 6914, upload-time = "2023-07-20T14:30:35.338Z" }, ] [[package]] name = "tenacity" version = "9.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413 } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926 }, + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, ] [[package]] @@ -4503,52 +4503,52 @@ dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728 }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049 }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008 }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665 }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230 }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688 }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694 }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802 }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995 }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948 }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986 }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222 }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097 }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117 }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309 }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712 }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725 }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875 }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451 }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794 }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777 }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188 }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978 }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271 }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216 }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860 }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567 }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067 }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473 }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855 }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022 }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736 }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908 }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706 }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667 }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, ] [[package]] name = "tinytag" version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/59/8a8cb2331e2602b53e4dc06960f57d1387a2b18e7efd24e5f9cb60ea4925/tinytag-2.2.1.tar.gz", hash = "sha256:e6d06610ebe7cd66fd07be2d3b9495914ab32654a5e47657bb8cd44c2484523c", size = 38214 } +sdist = { url = "https://files.pythonhosted.org/packages/96/59/8a8cb2331e2602b53e4dc06960f57d1387a2b18e7efd24e5f9cb60ea4925/tinytag-2.2.1.tar.gz", hash = "sha256:e6d06610ebe7cd66fd07be2d3b9495914ab32654a5e47657bb8cd44c2484523c", size = 38214, upload-time = "2026-03-15T18:48:01.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/34/d50e338631baaf65ec5396e70085e5de0b52b24b28db1ffbc1c6e82190dc/tinytag-2.2.1-py3-none-any.whl", hash = "sha256:ed8b1e6d25367937e3321e054f4974f9abfde1a3e0a538824c87da377130c2b6", size = 32927 }, + { url = "https://files.pythonhosted.org/packages/ce/34/d50e338631baaf65ec5396e70085e5de0b52b24b28db1ffbc1c6e82190dc/tinytag-2.2.1-py3-none-any.whl", hash = "sha256:ed8b1e6d25367937e3321e054f4974f9abfde1a3e0a538824c87da377130c2b6", size = 32927, upload-time = "2026-03-15T18:47:59.613Z" }, ] [[package]] @@ -4558,32 +4558,32 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115 } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275 }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472 }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736 }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835 }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673 }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818 }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195 }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982 }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245 }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069 }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263 }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429 }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363 }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786 }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133 }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] name = "tomlkit" version = "0.14.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310 }, + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, ] [[package]] @@ -4593,9 +4593,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598 } +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374 }, + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, ] [[package]] @@ -4608,18 +4608,18 @@ dependencies = [ { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047 } +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813 }, + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] @@ -4630,9 +4630,9 @@ dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825 } +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827 }, + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, ] [[package]] @@ -4642,74 +4642,79 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "ujson" version = "5.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/40/dbb8e2fe6ee33769602fba203dacaa3963b6599f0d0aefdf2b8811af5f70/ujson-5.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10f44bd08ae52ee23ca6e8b472692e5da1768af2d53ff1bad6f40b532e0bc7ee", size = 57951 }, - { url = "https://files.pythonhosted.org/packages/8d/db/627472e6b4ac34148ea52e6d3d15f6f366fc21c72fe7d6c7d3729d4b3ac5/ujson-5.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cc6ea753b7303fa5629fa9ac9257ea4b001c4d72583b2bb36ff1855a07db49f", size = 55562 }, - { url = "https://files.pythonhosted.org/packages/be/59/1248c966da197ae7d2673542444a2d9a1ff7c46e3ec2a302c3caf902b922/ujson-5.12.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289f13095764d03734adfa10107da9b530ceb64dc1b02a5f507588d978d5b7df", size = 59448 }, - { url = "https://files.pythonhosted.org/packages/d5/d7/60c1ca71a09c0654c3edca1192a18fc55e6cc06107be86d7d3f2b39fb29b/ujson-5.12.1-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:427893168d074e59214b0ee058337c57f5bb80175cdd5b4799a9c931aae22022", size = 61608 }, - { url = "https://files.pythonhosted.org/packages/d5/0a/c619525576219bfc50084100117481b1a732a16716a3878355570995de4e/ujson-5.12.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a81724d5d90a2da7155d15d8b156ce57eaed7cdd622df813f36a8e612fd4c8", size = 59113 }, - { url = "https://files.pythonhosted.org/packages/18/4d/79c1674036085e8dfdb77f8d87c1fd2896e97e6affd117c5e8ecc40f0ae4/ujson-5.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a6efff7dc6515416366819de4a1bc449b77107c5b48508b101fd40f7f8bec08", size = 1038914 }, - { url = "https://files.pythonhosted.org/packages/94/b1/9409bba17189ee282b6314cdf0ecdcc72e3d38cd565c870c0227d0494569/ujson-5.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77a71fe53427a0cf49d56eafd801d9f7e203b784b7f99cc717783fd6f6f7b732", size = 1198408 }, - { url = "https://files.pythonhosted.org/packages/4b/ad/fafbce7ac59f1a10a83892d0a34add23cc06492308e1330493aab707dc20/ujson-5.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ea3bed53d2ea8e5642e814a9e41f3e29420a8067874ba03ace8c0462e160490c", size = 1091451 }, - { url = "https://files.pythonhosted.org/packages/5a/1f/76fc9d5b1dcb9eb73ed45fd56e5114391bd30808eb1cea7f8bc5c9a64324/ujson-5.12.1-cp312-cp312-win32.whl", hash = "sha256:758e5c8fbe4e6d483041e03b307b01fb5d2f2dd4452d4d4b927ab902e188939e", size = 41049 }, - { url = "https://files.pythonhosted.org/packages/35/2a/7ce3b6fda10d05b79a245db03405734b521ba3da6c377f173b018dce6d4e/ujson-5.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:f6074d3d3267ba1914c624b6e1fa3d8152648ff36b0ab77ddf83b92db488c30d", size = 45330 }, - { url = "https://files.pythonhosted.org/packages/d7/66/5a37bba7a2e2ab36ae467521c4511e6593ad74c869f62ec4ba6330f3f71e/ujson-5.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:7642a41520ac1b2bc25ea282b66b8da522cc43424442e6fb5e039be4d4f96530", size = 39828 }, - { url = "https://files.pythonhosted.org/packages/b9/f0/985b351771ebf095e2c1aaad18f4d251831226a767a32593310e4f181f19/ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c", size = 57959 }, - { url = "https://files.pythonhosted.org/packages/61/73/03c7473372e1a538206fc655e474fa15f8bf9c46bb7c73c5fec9a544e429/ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242", size = 55564 }, - { url = "https://files.pythonhosted.org/packages/04/e6/104ebc35fa8dbaca66bf027c53c0c9c572271c2984576f4fd7d349d1a2e4/ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b", size = 59448 }, - { url = "https://files.pythonhosted.org/packages/11/d2/55274e80fe1806cdb5cb97483be16cd6163337ab11c3bd7e28ff8a8aad26/ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106", size = 61611 }, - { url = "https://files.pythonhosted.org/packages/6c/15/ec46b1757c8f7770d8c101b8a463bec67c19e89c46c608d01e4b193cc64a/ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25", size = 59120 }, - { url = "https://files.pythonhosted.org/packages/b5/27/ec73bc8908c33eb1f5be29d696084e531cbcfbd5c7b89ce54c025f66c682/ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe", size = 1038913 }, - { url = "https://files.pythonhosted.org/packages/6d/30/907e47569bed5f5eb258fef5e587c6759a7a062048796e40024497137e28/ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81", size = 1198409 }, - { url = "https://files.pythonhosted.org/packages/46/aa/f135f4b741baf14d5350be5511076408e7540353d3d850a430cb89d585a6/ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39", size = 1091456 }, - { url = "https://files.pythonhosted.org/packages/6e/81/5e6ef1115c0f700a74a150857c66cb22245f0e43f79667af9bf2b88f9452/ujson-5.12.1-cp313-cp313-win32.whl", hash = "sha256:ee83fbac03a0896faf190177c938f94eb610b798d495a19d50997242c4eca685", size = 41055 }, - { url = "https://files.pythonhosted.org/packages/98/76/8b423bc72a02f3fcf90f911a16382f360442c1a8887955c023d517f5d4ba/ujson-5.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:e08d9e096c416ddc34519241f97c201258b42639f2012d9547d8ae32921800dd", size = 45331 }, - { url = "https://files.pythonhosted.org/packages/5f/f2/c839a923da49384d4a319ddd5ce666e50e45a5c8417cec742c65667a1864/ujson-5.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:963287e4b1bc463735c4056968a2dfa59bb831b6daba68bddd14f451191fe9e5", size = 39828 }, - { url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099 }, - { url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631 }, - { url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469 }, - { url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611 }, - { url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122 }, - { url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938 }, - { url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416 }, - { url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425 }, - { url = "https://files.pythonhosted.org/packages/4e/d2/28938574b766980f873b68962abb4c68a944d939446768982934ad3bcd93/ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9", size = 42334 }, - { url = "https://files.pythonhosted.org/packages/49/b0/0af30bf65d96b73c28054b344ebbe24bc96780ae8a7f2973f5dad979510a/ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba", size = 46586 }, - { url = "https://files.pythonhosted.org/packages/4e/3b/0ee2555823724e60cc847c715c299f5792aa444bdde69c51d4aa42d885c2/ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232", size = 41178 }, - { url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565 }, - { url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157 }, - { url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288 }, - { url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302 }, - { url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784 }, - { url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759 }, - { url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121 }, - { url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085 }, - { url = "https://files.pythonhosted.org/packages/ae/52/4d4a6e78290a5eef3f576f6d281e6355535db903a08483fd1bb393bf8cb9/ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40", size = 43243 }, - { url = "https://files.pythonhosted.org/packages/3d/c8/849366785de52b513e5fc89d7aea0b531e71bb5641407cbdfdf47a99ede8/ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e", size = 47662 }, - { url = "https://files.pythonhosted.org/packages/8a/46/36a67f5a531a15308124786f3e2b7b96414b9d23dbcdc2a182dd3ffa2e1d/ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc", size = 41680 }, - { url = "https://files.pythonhosted.org/packages/6d/26/c9d0479236b3f5690d6a8bb45f708aabc2c91ca80d275eba24b1e9e464ab/ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c419bf42ae40963fc27f70c59e24e9a97f5cf168dbce2c572f3c0ce3595912", size = 56153 }, - { url = "https://files.pythonhosted.org/packages/ee/c8/785f4e132500aff2f1fd2bd4a4b86fe396a5519f830a098358c90ebb92ee/ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0be2b4f2f547b9f0f3d902640e410e5a2fc851576cbe033c88445a23e3e7aef1", size = 57352 }, - { url = "https://files.pythonhosted.org/packages/8f/13/b688a905653871b10b4ff0403c2ff562c17a0bd50be0d44324f3c85ca48f/ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a", size = 45988 }, -] +sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538, upload-time = "2026-05-05T22:05:01.354Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/40/dbb8e2fe6ee33769602fba203dacaa3963b6599f0d0aefdf2b8811af5f70/ujson-5.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:10f44bd08ae52ee23ca6e8b472692e5da1768af2d53ff1bad6f40b532e0bc7ee", size = 57951, upload-time = "2026-05-05T22:03:31.606Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/627472e6b4ac34148ea52e6d3d15f6f366fc21c72fe7d6c7d3729d4b3ac5/ujson-5.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cc6ea753b7303fa5629fa9ac9257ea4b001c4d72583b2bb36ff1855a07db49f", size = 55562, upload-time = "2026-05-05T22:03:32.853Z" }, + { url = "https://files.pythonhosted.org/packages/be/59/1248c966da197ae7d2673542444a2d9a1ff7c46e3ec2a302c3caf902b922/ujson-5.12.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:289f13095764d03734adfa10107da9b530ceb64dc1b02a5f507588d978d5b7df", size = 59448, upload-time = "2026-05-05T22:03:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d7/60c1ca71a09c0654c3edca1192a18fc55e6cc06107be86d7d3f2b39fb29b/ujson-5.12.1-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:427893168d074e59214b0ee058337c57f5bb80175cdd5b4799a9c931aae22022", size = 61608, upload-time = "2026-05-05T22:03:35.386Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0a/c619525576219bfc50084100117481b1a732a16716a3878355570995de4e/ujson-5.12.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7a81724d5d90a2da7155d15d8b156ce57eaed7cdd622df813f36a8e612fd4c8", size = 59113, upload-time = "2026-05-05T22:03:37.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/4d/79c1674036085e8dfdb77f8d87c1fd2896e97e6affd117c5e8ecc40f0ae4/ujson-5.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a6efff7dc6515416366819de4a1bc449b77107c5b48508b101fd40f7f8bec08", size = 1038914, upload-time = "2026-05-05T22:03:38.954Z" }, + { url = "https://files.pythonhosted.org/packages/94/b1/9409bba17189ee282b6314cdf0ecdcc72e3d38cd565c870c0227d0494569/ujson-5.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77a71fe53427a0cf49d56eafd801d9f7e203b784b7f99cc717783fd6f6f7b732", size = 1198408, upload-time = "2026-05-05T22:03:40.943Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ad/fafbce7ac59f1a10a83892d0a34add23cc06492308e1330493aab707dc20/ujson-5.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ea3bed53d2ea8e5642e814a9e41f3e29420a8067874ba03ace8c0462e160490c", size = 1091451, upload-time = "2026-05-05T22:03:42.739Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1f/76fc9d5b1dcb9eb73ed45fd56e5114391bd30808eb1cea7f8bc5c9a64324/ujson-5.12.1-cp312-cp312-win32.whl", hash = "sha256:758e5c8fbe4e6d483041e03b307b01fb5d2f2dd4452d4d4b927ab902e188939e", size = 41049, upload-time = "2026-05-05T22:03:44.341Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/7ce3b6fda10d05b79a245db03405734b521ba3da6c377f173b018dce6d4e/ujson-5.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:f6074d3d3267ba1914c624b6e1fa3d8152648ff36b0ab77ddf83b92db488c30d", size = 45330, upload-time = "2026-05-05T22:03:45.828Z" }, + { url = "https://files.pythonhosted.org/packages/d7/66/5a37bba7a2e2ab36ae467521c4511e6593ad74c869f62ec4ba6330f3f71e/ujson-5.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:7642a41520ac1b2bc25ea282b66b8da522cc43424442e6fb5e039be4d4f96530", size = 39828, upload-time = "2026-05-05T22:03:47.123Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/985b351771ebf095e2c1aaad18f4d251831226a767a32593310e4f181f19/ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c", size = 57959, upload-time = "2026-05-05T22:03:48.348Z" }, + { url = "https://files.pythonhosted.org/packages/61/73/03c7473372e1a538206fc655e474fa15f8bf9c46bb7c73c5fec9a544e429/ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242", size = 55564, upload-time = "2026-05-05T22:03:49.869Z" }, + { url = "https://files.pythonhosted.org/packages/04/e6/104ebc35fa8dbaca66bf027c53c0c9c572271c2984576f4fd7d349d1a2e4/ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b", size = 59448, upload-time = "2026-05-05T22:03:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/11/d2/55274e80fe1806cdb5cb97483be16cd6163337ab11c3bd7e28ff8a8aad26/ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106", size = 61611, upload-time = "2026-05-05T22:03:52.731Z" }, + { url = "https://files.pythonhosted.org/packages/6c/15/ec46b1757c8f7770d8c101b8a463bec67c19e89c46c608d01e4b193cc64a/ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25", size = 59120, upload-time = "2026-05-05T22:03:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/ec73bc8908c33eb1f5be29d696084e531cbcfbd5c7b89ce54c025f66c682/ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe", size = 1038913, upload-time = "2026-05-05T22:03:55.792Z" }, + { url = "https://files.pythonhosted.org/packages/6d/30/907e47569bed5f5eb258fef5e587c6759a7a062048796e40024497137e28/ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81", size = 1198409, upload-time = "2026-05-05T22:03:57.947Z" }, + { url = "https://files.pythonhosted.org/packages/46/aa/f135f4b741baf14d5350be5511076408e7540353d3d850a430cb89d585a6/ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39", size = 1091456, upload-time = "2026-05-05T22:04:00.355Z" }, + { url = "https://files.pythonhosted.org/packages/6e/81/5e6ef1115c0f700a74a150857c66cb22245f0e43f79667af9bf2b88f9452/ujson-5.12.1-cp313-cp313-win32.whl", hash = "sha256:ee83fbac03a0896faf190177c938f94eb610b798d495a19d50997242c4eca685", size = 41055, upload-time = "2026-05-05T22:04:02.372Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/8b423bc72a02f3fcf90f911a16382f360442c1a8887955c023d517f5d4ba/ujson-5.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:e08d9e096c416ddc34519241f97c201258b42639f2012d9547d8ae32921800dd", size = 45331, upload-time = "2026-05-05T22:04:03.946Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f2/c839a923da49384d4a319ddd5ce666e50e45a5c8417cec742c65667a1864/ujson-5.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:963287e4b1bc463735c4056968a2dfa59bb831b6daba68bddd14f451191fe9e5", size = 39828, upload-time = "2026-05-05T22:04:05.52Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099, upload-time = "2026-05-05T22:04:06.688Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631, upload-time = "2026-05-05T22:04:07.925Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469, upload-time = "2026-05-05T22:04:09.208Z" }, + { url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611, upload-time = "2026-05-05T22:04:10.836Z" }, + { url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122, upload-time = "2026-05-05T22:04:12.137Z" }, + { url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938, upload-time = "2026-05-05T22:04:13.548Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416, upload-time = "2026-05-05T22:04:15.609Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425, upload-time = "2026-05-05T22:04:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d2/28938574b766980f873b68962abb4c68a944d939446768982934ad3bcd93/ujson-5.12.1-cp314-cp314-win32.whl", hash = "sha256:c8e626b6bc9bdd2e8f7393b7d99f3daa2ca4022e6203662e70de7bb3604b21b9", size = 42334, upload-time = "2026-05-05T22:04:19.85Z" }, + { url = "https://files.pythonhosted.org/packages/49/b0/0af30bf65d96b73c28054b344ebbe24bc96780ae8a7f2973f5dad979510a/ujson-5.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:c6d3bdd020333688ee60559437021ed68a98a28fdd609b5af16de5dd58f90cba", size = 46586, upload-time = "2026-05-05T22:04:21.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3b/0ee2555823724e60cc847c715c299f5792aa444bdde69c51d4aa42d885c2/ujson-5.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:e3c9c894971f4ada3ded16a804ed4640e1f2b3e5239beaeec7c48296f39f4232", size = 41178, upload-time = "2026-05-05T22:04:22.597Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565, upload-time = "2026-05-05T22:04:23.75Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157, upload-time = "2026-05-05T22:04:25.028Z" }, + { url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288, upload-time = "2026-05-05T22:04:26.273Z" }, + { url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302, upload-time = "2026-05-05T22:04:27.516Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784, upload-time = "2026-05-05T22:04:28.776Z" }, + { url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759, upload-time = "2026-05-05T22:04:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121, upload-time = "2026-05-05T22:04:31.961Z" }, + { url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085, upload-time = "2026-05-05T22:04:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/ae/52/4d4a6e78290a5eef3f576f6d281e6355535db903a08483fd1bb393bf8cb9/ujson-5.12.1-cp314-cp314t-win32.whl", hash = "sha256:4e12ca368b397aed7fa1eec534ea1ba8d94977b376f9df3e93ae1acfd004ec40", size = 43243, upload-time = "2026-05-05T22:04:35.486Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c8/849366785de52b513e5fc89d7aea0b531e71bb5641407cbdfdf47a99ede8/ujson-5.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:cec6b9b539539affc1f01a795c99574592a635ce22331b64f2b42e0af570659e", size = 47662, upload-time = "2026-05-05T22:04:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/8a/46/36a67f5a531a15308124786f3e2b7b96414b9d23dbcdc2a182dd3ffa2e1d/ujson-5.12.1-cp314-cp314t-win_arm64.whl", hash = "sha256:696224d4cfb8883fa5c0285dff31e5ce924704dd9ccd38e9ea8b5bf4a42b12fc", size = 41680, upload-time = "2026-05-05T22:04:39.083Z" }, + { url = "https://files.pythonhosted.org/packages/6d/26/c9d0479236b3f5690d6a8bb45f708aabc2c91ca80d275eba24b1e9e464ab/ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c419bf42ae40963fc27f70c59e24e9a97f5cf168dbce2c572f3c0ce3595912", size = 56153, upload-time = "2026-05-05T22:04:40.326Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c8/785f4e132500aff2f1fd2bd4a4b86fe396a5519f830a098358c90ebb92ee/ujson-5.12.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0be2b4f2f547b9f0f3d902640e410e5a2fc851576cbe033c88445a23e3e7aef1", size = 57352, upload-time = "2026-05-05T22:04:42.005Z" }, + { url = "https://files.pythonhosted.org/packages/8f/13/b688a905653871b10b4ff0403c2ff562c17a0bd50be0d44324f3c85ca48f/ujson-5.12.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:4ea0c490c702c20495e97345acfcf0c2f3153e658ef537ff111929c48b89e10a", size = 45988, upload-time = "2026-05-05T22:04:43.36Z" }, +] + +[[package]] +name = "unstract-agent-kv-schema" +version = "0.1.0" +source = { editable = "../unstract/agent-kv-schema" } [[package]] name = "unstract-connectors" @@ -4958,6 +4963,7 @@ dependencies = [ { name = "python-socketio" }, { name = "redis" }, { name = "requests" }, + { name = "unstract-agent-kv-schema" }, { name = "unstract-connectors" }, { name = "unstract-core" }, { name = "unstract-filesystem" }, @@ -5009,6 +5015,7 @@ requires-dist = [ { name = "python-socketio", specifier = ">=5.9.0" }, { name = "redis", specifier = ">=4.5.0,<6.0.0" }, { name = "requests", specifier = ">=2.33.0,<3.0.0" }, + { name = "unstract-agent-kv-schema", editable = "../unstract/agent-kv-schema" }, { name = "unstract-connectors", editable = "../unstract/connectors" }, { name = "unstract-core", editable = "../unstract/core" }, { name = "unstract-filesystem", editable = "../unstract/filesystem" }, @@ -5074,45 +5081,45 @@ requires-dist = [ name = "uritemplate" version = "4.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267 } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488 }, + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, ] [[package]] name = "urllib3" version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "validators" version = "0.35.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399 } +sdist = { url = "https://files.pythonhosted.org/packages/53/66/a435d9ae49850b2f071f7ebd8119dd4e84872b01630d6736761e6e7fd847/validators-0.35.0.tar.gz", hash = "sha256:992d6c48a4e77c81f1b4daba10d16c3a9bb0dbb79b3a19ea847ff0928e70497a", size = 73399, upload-time = "2025-05-01T05:42:06.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712 }, + { url = "https://files.pythonhosted.org/packages/fa/6e/3e955517e22cbdd565f2f8b2e73d52528b14b8bcfdb04f62466b071de847/validators-0.35.0-py3-none-any.whl", hash = "sha256:e8c947097eae7892cb3d26868d637f79f47b4a0554bc6b80065dfe5aac3705dd", size = 44712, upload-time = "2025-05-01T05:42:04.203Z" }, ] [[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 } +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 }, + { 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 = "wcwidth" version = "0.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684 } +sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189 }, + { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, ] [[package]] @@ -5128,67 +5135,67 @@ dependencies = [ { name = "pydantic" }, { name = "validators" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6a/0e/450732a620dec30e5c13d0e5ba6ea81cd2b168ffb5a1e6cfa034da2fd988/weaviate_client-4.20.1.tar.gz", hash = "sha256:7d3c4835292b17d54757c3f62921e87975c01f26869db787250cebef2a17bd80", size = 807802 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0e/450732a620dec30e5c13d0e5ba6ea81cd2b168ffb5a1e6cfa034da2fd988/weaviate_client-4.20.1.tar.gz", hash = "sha256:7d3c4835292b17d54757c3f62921e87975c01f26869db787250cebef2a17bd80", size = 807802, upload-time = "2026-02-25T13:39:56.125Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3a/a2c74bfbdc5f8acbab8025205520448dcece82942206a9861ec48ccad03f/weaviate_client-4.20.1-py3-none-any.whl", hash = "sha256:6ca36bb8752c39589bfc856c1232cbcb627f69141e7a741b592e2516888f59d8", size = 618735 }, + { url = "https://files.pythonhosted.org/packages/de/3a/a2c74bfbdc5f8acbab8025205520448dcece82942206a9861ec48ccad03f/weaviate_client-4.20.1-py3-none-any.whl", hash = "sha256:6ca36bb8752c39589bfc856c1232cbcb627f69141e7a741b592e2516888f59d8", size = 618735, upload-time = "2026-02-25T13:39:54.476Z" }, ] [[package]] name = "websocket-client" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616 }, + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, ] [[package]] name = "wrapt" version = "1.17.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998 }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020 }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098 }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036 }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156 }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102 }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732 }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705 }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877 }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885 }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003 }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025 }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108 }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072 }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214 }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105 }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766 }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711 }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885 }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896 }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132 }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091 }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172 }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163 }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963 }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945 }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857 }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178 }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310 }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266 }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544 }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283 }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366 }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571 }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094 }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659 }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946 }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717 }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334 }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471 }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591 }, +sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, + { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, + { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, + { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, + { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, + { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, + { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, + { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, + { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, + { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, + { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, + { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, + { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, + { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, + { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, + { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] [[package]] @@ -5198,9 +5205,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405 }, + { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] [[package]] @@ -5212,115 +5219,115 @@ dependencies = [ { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737 }, - { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029 }, - { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310 }, - { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587 }, - { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528 }, - { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339 }, - { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061 }, - { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132 }, - { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289 }, - { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950 }, - { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960 }, - { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703 }, - { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325 }, - { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067 }, - { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285 }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359 }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674 }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879 }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796 }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547 }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854 }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351 }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711 }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014 }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557 }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559 }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502 }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027 }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369 }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565 }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813 }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632 }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895 }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356 }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515 }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785 }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719 }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690 }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851 }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874 }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710 }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033 }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817 }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482 }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949 }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839 }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696 }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865 }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234 }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295 }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784 }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313 }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932 }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786 }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455 }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752 }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291 }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026 }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355 }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417 }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422 }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915 }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690 }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750 }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685 }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009 }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033 }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483 }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175 }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871 }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093 }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384 }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019 }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894 }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979 }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943 }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786 }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307 }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904 }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728 }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964 }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882 }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797 }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023 }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227 }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302 }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202 }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558 }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610 }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041 }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288 }, +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, ] [[package]] name = "zipp" version = "3.23.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] [[package]] name = "zipstream-ng" version = "1.9.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/f2/690a35762cf8366ce6f3b644805de970bd6a897ca44ce74184c7b2bc94e7/zipstream_ng-1.9.0.tar.gz", hash = "sha256:a0d94030822d137efbf80dfdc680603c42f804696f41147bb3db895df667daea", size = 37963 } +sdist = { url = "https://files.pythonhosted.org/packages/11/f2/690a35762cf8366ce6f3b644805de970bd6a897ca44ce74184c7b2bc94e7/zipstream_ng-1.9.0.tar.gz", hash = "sha256:a0d94030822d137efbf80dfdc680603c42f804696f41147bb3db895df667daea", size = 37963, upload-time = "2025-08-29T01:03:36.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/62/c2da1c495291a52e561257d017585e08906d288035d025ccf636f6b9a266/zipstream_ng-1.9.0-py3-none-any.whl", hash = "sha256:31dc2cf617abdbf28d44f2e08c0d14c8eee2ea0ec26507a7e4d5d5f97c564b7a", size = 24852 }, + { url = "https://files.pythonhosted.org/packages/de/62/c2da1c495291a52e561257d017585e08906d288035d025ccf636f6b9a266/zipstream_ng-1.9.0-py3-none-any.whl", hash = "sha256:31dc2cf617abdbf28d44f2e08c0d14c8eee2ea0ec26507a7e4d5d5f97c564b7a", size = 24852, upload-time = "2025-08-29T01:03:35.046Z" }, ] From d6dc9e4b9092a7596c679a7172b357f530945aec Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 12:19:41 +0530 Subject: [PATCH 02/18] UN-4232 [FIX] Lock the cross-repo stage name; correct the API docs Task 5a and Task 6. THE STAGE NAME `TABLE_STAGE_NAMES[0]` (here) is the stage name the status endpoint will SHOW. `STAGE_TABLE_EXTRACTION` (cloud `agentic_table/src/api_binding.py`) is the name the executor actually SENDS. Nothing enforced their equality: StageReportView persists whatever arrives, and `_status_document` then filters a job's recorded stages through the list here. On drift the job completes normally, bills normally, and every status response returns an empty `stages` array. No test on either side could catch it -- each repo is internally consistent on its own -- and from a client's seat it reads as "the API is broken". Until now the only thing tying the two was a pair of comments. Both halves now assert the literal and name the other's file. A literal, not an import: the repos are separate checkouts that meet only in the merged tree, so an import would pass vacuously exactly where drift gets introduced. DOCS `docs/agent-kv-api.md` said the `name` field takes "`kv` or `table`". It now says `table` is the only supported value, with a note explaining why a `kv` submit returns 400 -- otherwise that refusal reads as a bug -- and that the wire format does not change when `kv` ships. The `/validate` section is removed along with its route, and every reference to it, and sections 7-13 are renumbered to 6-12 with their anchors and cross-links. Section 2 (the `kv` schema language) is kept but now opens by saying the extractor it describes is not routable here: the compiler ships and the language is frozen, so an integration can still be written against it. Co-Authored-By: Claude Opus 5 --- .../tests/test_cross_repo_stage_name.py | 43 +++++++++ docs/agent-kv-api.md | 90 ++++++++----------- 2 files changed, 80 insertions(+), 53 deletions(-) create mode 100644 backend/agent_kv/tests/test_cross_repo_stage_name.py diff --git a/backend/agent_kv/tests/test_cross_repo_stage_name.py b/backend/agent_kv/tests/test_cross_repo_stage_name.py new file mode 100644 index 0000000000..00255a7169 --- /dev/null +++ b/backend/agent_kv/tests/test_cross_repo_stage_name.py @@ -0,0 +1,43 @@ +"""The one constant that drifts silently across the two repos. + +``TABLE_STAGE_NAMES[0]`` here is the stage name ``_status_document`` will +*show*. ``STAGE_TABLE_EXTRACTION`` in the cloud repo's +``workers/plugins/agentic_table/src/api_binding.py`` is the name the executor +actually *sends*. They must be equal, and nothing enforces it: +``StageReportView`` persists whatever name arrives, and ``_status_document`` +then filters a job's recorded stages through the list below. + +So on drift the job completes normally, bills normally, and every status +response reports an empty ``stages`` array. No test on either side can catch +that alone -- each repo is internally consistent -- and from a client's seat it +reads as "the API is broken". This file is one half of the lock; the cloud half +is ``workers/plugins/agentic_table/tests/test_cross_repo_stage_name.py``. + +A literal, not an import: the two repos are separate checkouts and only meet in +the merged tree, so an import would make this test pass vacuously wherever the +other side is absent -- exactly the environments where drift is introduced. +""" + +from agent_kv.constants import TABLE_EXTRACTOR_NAME, TABLE_STAGE_NAMES + +#: Must equal ``STAGE_TABLE_EXTRACTION`` in the cloud repo's +#: ``workers/plugins/agentic_table/src/api_binding.py``. Change one, change +#: both, in the same PR. +CLOUD_STAGE_TABLE_EXTRACTION = "table_extraction" + + +def test_the_status_filter_matches_the_name_the_executor_sends(): + assert TABLE_STAGE_NAMES[0] == CLOUD_STAGE_TABLE_EXTRACTION, ( + "TABLE_STAGE_NAMES[0] no longer equals STAGE_TABLE_EXTRACTION in the " + "cloud repo's workers/plugins/agentic_table/src/api_binding.py. Every " + "table job will complete normally while reporting an empty `stages` " + "array. Update both sides, and the cloud twin of this test, together." + ) + + +def test_the_table_extractor_reports_exactly_one_stage(): + """The table engine exposes no node-level progress hooks, so a second name + here would describe progress the executor cannot substantiate -- it would + be recorded as missing rather than pending, for every job.""" + assert TABLE_STAGE_NAMES == [CLOUD_STAGE_TABLE_EXTRACTION] + assert TABLE_EXTRACTOR_NAME == "table" diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md index a3d17308af..493e13083e 100644 --- a/docs/agent-kv-api.md +++ b/docs/agent-kv-api.md @@ -9,7 +9,7 @@ This document describes the API **as built in this repo** — the OSS scaffold ( validation, dispatch, job lifecycle, key management). The document, `keys.json` schema, and OCR/LLM extraction pipeline itself are described only insofar as they shape the contract; the extraction engine ships as a cloud executor plugin (see -[§11, 501 behavior](#11-when-the-engine-is-unavailable-501-behavior) below). +[§6, 501 behavior](#10-when-the-engine-is-unavailable-501-behavior) below). Design background: `docs/superpowers/specs/2026-08-28-agent-kv-api-design.md` (referred to as "the spec" throughout this document; section numbers below are spec section @@ -23,7 +23,7 @@ There are two independent auth schemes on two different URL prefixes: | Surface | Prefix | Auth | |---|---|---| -| Public execution API (submit/status/result/cancel/validate) | `/agent-kv/…` (top-level, not under the tenant subfolder) | `Authorization: Bearer ` — a dedicated per-organization key, validated by the view itself | +| Public execution API (submit/status/result/cancel) | `/agent-kv/…` (top-level, not under the tenant subfolder) | `Authorization: Bearer ` — a dedicated per-organization key, validated by the view itself | | Key management (create/list/rotate/revoke) | `{PATH_PREFIX}/unstract/…/agent-kv/keys/…` (tenant-scoped, alongside Platform API key management) | Session auth, `IsOrganizationAdmin` only | The execution prefix (`AGENT_KV_PATH_PREFIX`, default `agent-kv`) sits **outside** the @@ -68,10 +68,17 @@ not a v1 gap unique to this feature. ## 2. The extraction schema (`keys` field) +> **This section describes the `kv` extractor's schema language, which is not +> routable on this deployment** (see [§3](#3-submit--post-agent-kv-multipart)). +> It is documented here because the compiler ships and the language is frozen, +> so an integration can be written against it ahead of the extractor's release. +> The `table` extractor's `keys` is just `{"target_table": ""}` and needs +> none of this. + The `keys` submit field — spec-called "keys.json" — is a JSON object describing what to extract. It compiles through `unstract.agent_kv_schema.compile_schema` (`unstract/agent-kv-schema/src/unstract/agent_kv_schema/`), the **single schema compiler -in the system**: the OSS API validates with it at submit time and at `/validate`, and +in the system**: the OSS API validates with it at submit time, and the cloud engine imports the same package to execute — the schema language cannot drift between validation and execution by construction (spec §5.1). @@ -165,11 +172,10 @@ Enforced by `compile_schema` regardless of the byte-size cap below (`SchemaCaps` These caps are hardcoded in the compiler (not env-configurable); what **is** env-configurable is the raw byte size of the whole `keys` document (`AGENT_KV_MAX_SCHEMA_BYTES`) and of `calculations` -(`AGENT_KV_MAX_CALCULATIONS_BYTES`) — see [§13 env reference](#13-environment-reference). +(`AGENT_KV_MAX_CALCULATIONS_BYTES`) — see [§13 env reference](#12-environment-reference). A schema that fails to compile is rejected with a `400` at submit time — **nothing is -billed** — or, at `/validate`, with a `200` carrying `{"valid": false, "error": ""}` -(compile errors are not 400s there; only a missing `keys` body key is). +billed**. ## 3. Submit — `POST /agent-kv/` (multipart) @@ -196,10 +202,17 @@ The request is **extractor-scoped**: a per-extractor schema and its knobs live i | Field | Required | Notes | |---|---|---| -| `name` | yes | Which extractor: `kv` or `table`; anything else ⇒ 400. More than one entry ⇒ 400 — the format is in place for multi-extractor jobs, the execution is not built yet. | -| `keys` | yes | This extractor's own schema. For `kv`, its `keys.json` (see [§2](#2-the-extraction-schema-keys-field)); for `table`, `{"target_table": ""}` — the engine's one required extraction parameter. | +| `name` | yes | Which extractor. **`table` is the only supported value on this deployment**; anything else, `kv` included, ⇒ 400 (see the note below). More than one entry ⇒ 400 — the format is in place for multi-extractor jobs, the execution is not built yet. | +| `keys` | yes | This extractor's own request. For `table`, `{"target_table": ""}` — the engine's one required extraction parameter. | | `options` | no | This extractor's own knobs (below). An unrecognised option ⇒ 400, so a knob aimed at the wrong extractor fails loudly instead of being silently dropped. | +> **Why `kv` returns 400 here.** This deployment ships the table extractor only. +> The `kv` extractor's engine runs on its own release track and nothing on this +> deployment consumes its queue, so the API refuses a `kv` submit outright +> rather than accepting it with a `202` for a job that would never run. The +> wire format is unchanged: when `kv` ships, it becomes a second accepted value +> of `name` and nothing else about this document changes. + **`kv` options:** | Option | Notes | @@ -265,9 +278,9 @@ Not exposed (D6): model choice, challenger model, `parallel_pages`, thinking bud [result payload](#5-result--get-agent-kvjob_idresult), same shape as the result endpoint. -`501`: [engine unavailable](#11-when-the-engine-is-unavailable-501-behavior). +`501`: [engine unavailable](#10-when-the-engine-is-unavailable-501-behavior). `429`: rate-limited (per-key request rate, or per-org concurrent-job limit — see -[§13](#13-environment-reference)). +[§6](#12-environment-reference)). `402`: the organization's subscription does not permit the request — its trial has expired, or the subscription is inactive. Nothing is dispatched and nothing is billed. This is the same gate, the same policy and the same response bodies an @@ -509,36 +522,7 @@ and `parse_failures` appears only when some page failed to parse. There is **no > two settings of one option: a client that sets the flag must read `tables.rows` > instead of iterating `tables`. -## 6. Validate — `POST /agent-kv/validate` - -Compile-only check; **free of charge but not free of auth** — authenticated (valid key) -and rate-limited the same as submit (spec §6.1: "an open compile endpoint would be a -probing/DoS surface"). No job is created; nothing is billed either way. - -```bash -curl -X POST https://api.unstract.example/agent-kv/validate \ - -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" \ - -H "Content-Type: application/json" \ - -d '{"keys": {"invoice_number": {"description": "Invoice #", "required": true}}}' -``` - -Success (`200`): - -```json -{"valid": true, "leaves": 1, "arrays": 0, "constraints": 0} -``` - -Invalid schema — still `200`, not `400` (schema-compile rejection is a *response*, not -a request error): - -```json -{"valid": false, "error": "Leaf at invoice_number is missing a 'description'"} -``` - -Missing `keys` key in the body is the one case that *is* a `400`: -`{"detail": "body must include 'keys'"}`. - -## 7. Cancel — `POST /agent-kv/{job_id}/cancel` +## 6. Cancel — `POST /agent-kv/{job_id}/cancel` Defined precisely per spec §7.4: no task-revocation machinery exists anywhere in the platform, so cancel flips the job row to `CANCELLED` via the same guarded @@ -558,7 +542,7 @@ simply has its result discarded on arrival. - Won the race (job was non-terminal): `200` `{"status": "cancelled"}`. - Already terminal: `409` `{"status": ""}` — **note this one is the raw uppercase enum value** (e.g. `"COMPLETED"`), unlike every lowercased `status` value - elsewhere in this API (status doc, result 409, validate). This is the code as shipped + elsewhere in this API (status doc, result 409). This is the code as shipped (`execution_views.py::JobCancelView`, confirmed by `test_job_views.py::test_cancel_on_completed_is_409_and_result_untouched`), not a typo in this document. @@ -568,7 +552,7 @@ curl -X POST https://api.unstract.example/agent-kv/5b6e9b0a-.../cancel \ -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" ``` -## 8. Delete — `DELETE /agent-kv/{job_id}` +## 7. Delete — `DELETE /agent-kv/{job_id}` Deletes the result and any residual staged input immediately, and blanks both refs (`204`, no body). The job row itself is retained (audit trail); a later `result` fetch @@ -579,7 +563,7 @@ curl -X DELETE https://api.unstract.example/agent-kv/5b6e9b0a-.../ \ -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" ``` -## 9. Webhook delivery +## 8. Webhook delivery `webhook_url` (if given at submit) receives exactly one terminal-state POST: `{"job_id": "...", "status": "completed"|"failed"|"cancelled"}` — a fixed payload @@ -621,7 +605,7 @@ ide-callback worker waives both guards (http scheme and non-public host) so the e2e lane can deliver to a receiver on the compose host. Never set it in production. -## 10. Retention and TTL +## 9. Retention and TTL - **Input deletion is completion-triggered, not TTL-based** (spec D10: "uploaded document deleted on job completion"). `FinalizeView.post` @@ -641,7 +625,7 @@ production. `DELETE /agent-kv/{job_id}`. - **Result retention**: `AGENT_KV_RESULT_TTL_DAYS` (default **7** days, D10's engineering default) — results are re-readable until then, then swept by the - internal `ttl-cleanup` endpoint ([§12](#12-deploy-checklist)), which blanks + internal `ttl-cleanup` endpoint ([§6](#11-deploy-checklist)), which blanks `result_ref` (and `input_ref` too, covering the cancelled-job case above, or defensively for any input that somehow outlives completion). The job row itself is never deleted (audit trail persists past TTL, only the object-store payloads are @@ -654,7 +638,7 @@ production. under the full 7 days. Documented here as shipped behavior, not silently reconciled — flagged for anyone tightening D10 later. -## 11. When the engine is unavailable (501 behavior) +## 10. When the engine is unavailable (501 behavior) The Django backend cannot see the executor plugin registry (that lives in the workers process). Dispatching to a queue no worker consumes would hang, not error — so gating @@ -674,7 +658,7 @@ plugin is a cloud deliverable, spec §5.1/§11: "OSS scaffold merges dark"). Eve {"detail": "agent-kv engine not available on this deployment"} ``` -with HTTP `501`. `/validate`, key management, status/result/cancel/delete on +with HTTP `501`. Key management and status/result/cancel/delete on *already-existing* jobs are unaffected by this gate — only `SubmitView` probes it, since only submit needs the engine. @@ -704,21 +688,21 @@ On `noop: true`, the executor **must**: Step 2 is not optional. `FinalizeView`'s terminal-state guard (spec §5.4) correctly no-ops the status write for an already-terminal job — but its concurrency-slot release (`AgentKVConcurrencyLimiter.release`) lives in a `finally`, unconditional on -whether the guard actually won ([§10](#10-retention-and-ttl) covers the input-deletion +whether the guard actually won ([§6](#9-retention-and-ttl) covers the input-deletion side of this same guard). An executor that treats `noop: true` as "nothing left to do here" and skips the finalize call entirely leaks that job's concurrency slot for the rest of the limiter's TTL (6 hours) instead of releasing it immediately. This is the entire mechanism behind a pre-pickup cancel actually being observed and cleaned up promptly — there is no other signal. -## 12. Deploy checklist +## 11. Deploy checklist Everything below is required (or worth checking) to run this feature for real, beyond `docker compose up`: 1. **Cloud plugin**: install/enable the `agent_kv` backend capability plugin (probed via `plugins.get_plugin("agent_kv")`) — without it, submit always 501s - ([§11](#11-when-the-engine-is-unavailable-501-behavior)). This repo ships gated + ([§6](#10-when-the-engine-is-unavailable-501-behavior)). This repo ships gated dark by design; nothing to do for an OSS-only deployment except accept the 501. The marker the probe keys off is the mere presence of `backend/plugins/agent_kv/__init__.py` — a cloud image ships it (via the @@ -800,7 +784,7 @@ beyond `docker compose up`: ttl-cleanup additionally needs `storage` for `AGENT_KV_FILE_STORAGE_CREDENTIALS` (see item 6 below) — both already on the `backend` deployment's config list. 5. **Env vars**: every `AGENT_KV_*` setting plus `AGENT_KV_FILE_STORAGE_CREDENTIALS` — - see [§13](#13-environment-reference) and `docker/sample.env`. The executor-side vars + see [§6](#12-environment-reference) and `docker/sample.env`. The executor-side vars (read by the cloud `agentic_kv` plugin, not by the backend settings in §13) are a separate chart group: `global.sharedConfigs.agentKv` (`charts/unstract-platform/values.yaml`, cloud repo) — @@ -981,7 +965,7 @@ beyond `docker compose up`: `runtimeClass: gvisor` sandbox. Consequently the sandbox pod's default-deny egress and no-secrets posture are load-bearing and must not be relaxed. -## 13. Environment reference +## 12. Environment reference All `AGENT_KV_*` settings (`backend/backend/settings/base.py`), each `os.environ.get` with the default shown: @@ -993,7 +977,7 @@ with the default shown: | `AGENT_KV_MAX_PAGES` | `100` | Pre-OCR page cap for PDFs (§6.1). Excel has no pre-OCR page concept; images are not accepted. | | `AGENT_KV_MAX_CALCULATIONS_BYTES` | `20000` | Byte cap on the optional `calculations` field. | | `AGENT_KV_MAX_SCHEMA_BYTES` | `262144` | Byte cap on the raw `keys` JSON document (256 KiB). | -| `AGENT_KV_RESULT_TTL_DAYS` | `7` | Result retention window (and a cancelled job's input, which rides the same TTL — a completed/failed job's input is deleted immediately at finalize instead), stamped at submit time (see [§10](#10-retention-and-ttl)). | +| `AGENT_KV_RESULT_TTL_DAYS` | `7` | Result retention window (and a cancelled job's input, which rides the same TTL — a completed/failed job's input is deleted immediately at finalize instead), stamped at submit time (see [§6](#9-retention-and-ttl)). | | `AGENT_KV_MAX_TIMEOUT_SECONDS` | `300` | Upper bound on the submit `timeout` (synchronous-wait) field. | | `AGENT_KV_CONCURRENT_LIMIT` | `5` | Per-organization concurrent in-flight job cap (own Redis namespace, fails open on Redis errors). | | `AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE` | `60` | Per-key request rate limit (submit + validate), fails open on Redis errors. | From 7067790f5a038dfd4a96bc7406a138550f0527cc Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 13:36:23 +0530 Subject: [PATCH 03/18] UN-4232 [MISC] Clear the six SonarCloud cognitive-complexity findings All six were `python:S3776` on code this PR introduces to `main`. Every one is a pure extraction -- no behaviour change, and the existing suites are the proof (193 backend, 42 schema, unchanged and green throughout). execution_views.py SubmitView.post 16 -> 8 internal_views.py FinalizeView.post 23 -> ~7 kv_schema.py _walk 25 -> ~9 compile.py compile_schema 20 -> ~4 constraints.py _aggregate 18 -> ~9 constraints.py _truth 18 -> ~6 The extractions follow the shape each function already had: * `SubmitView.post` -- `_subscription_denial`, `_dispatch_or_fail` (which folds the two identical except-branches into one exit) and `_sync_wait_response`. * `FinalizeView.post` -- `_complete`, `_fail` and `_drop_staged_input`, which flattens a four-deep nest into a dispatch and one guard. * `_walk` -- `_walk_array` and `_append_leaf`, one per node kind; the function now only does the depth/shape guards and dispatches. * `compile_schema` -- `_enforce_shape_caps`, `_enforce_leaf_caps` and `_validated_constraints`, split along the three things it was checking. * `_aggregate` -- `_parse_agg_call` takes the fail-closed call validation and the path split. * `_truth` -- `_compare_chain` takes the chained-comparison loop. Verified under flake8-cognitive-complexity at `--max-cognitive-complexity=15`: clean across `backend/agent_kv/` and `unstract/agent-kv-schema/src/`. The two functions that still measure 14-15 on that checker were analysed by Sonar in this same PR and not flagged, so they score at or under the threshold on its algorithm; they are untouched rather than churned for no gain. Also drops an orphan missed in the e2e re-point: `_CALC_ENV_GATE` and `_skip_unless_calculations_declared` outlived the two calculation scenarios they gated, and referenced a sandbox worker this PR does not ship. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 139 ++++++++++-------- backend/agent_kv/internal_views.py | 104 +++++++------ tests/e2e/agent_kv/test_agent_kv_e2e.py | 25 ---- .../src/unstract/agent_kv_schema/compile.py | 23 ++- .../unstract/agent_kv_schema/constraints.py | 78 ++++++---- .../src/unstract/agent_kv_schema/kv_schema.py | 97 +++++++----- 6 files changed, 270 insertions(+), 196 deletions(-) diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index f2fbb324fc..2853a6dafb 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -104,6 +104,75 @@ def _fail_job_response(job, org_id: str, message: str, *, job_saved: bool) -> Re ) +def _subscription_denial(plugin, agent_kv_key, request) -> Response | None: + """Subscription admission (§6.6), or ``None`` when the org may proceed. + + A submit dispatches paid work, so it is gated exactly as an API deployment + execute is -- same policy, same 402 bodies -- via the cloud plugin's gate, + which calls the very ``SubscriptionHelper`` that cloud's + ``SubscriptionMiddleware`` calls. + + Why here and not in that middleware: it resolves the org from the URL + (``/deployment/api/{org_name}/...``). Agent-KV's URL carries no org segment + -- the org is inside the Bearer key -- so the middleware's + ``get_organization_id`` returns None for these requests, finds no + subscription row, and admits every one of them. The view is the first point + where the org is actually known. + + ``organization.organization_id`` is the org SLUG -- the CharField + ``Subscription.organization_id`` is keyed on, and what the deployment URL + supplies as ``org_name``. NOT ``agent_kv_key.organization_id``, which is the + Organization FK primary key: that matches no row, and the shared policy + reads "no row" as "nothing to enforce", so the gate would silently admit + everything while looking correctly wired. + """ + gate_factory = plugin.get("service_class") + if not gate_factory: # absent on a cloud build predating the gate + return None + return gate_factory().check(agent_kv_key.organization.organization_id, request) + + +def _dispatch_or_fail(job, org_id: str, entry: dict, options: dict) -> Response | None: + """Dispatch the job, or return the failure response. ``None`` means sent. + + Both failure paths terminalize the job and release the concurrency slot. + The bare ``except`` is belt-and-braces: ``dispatch_job`` wraps its own + internal failures as ``DispatchError``, but nothing here may rely on that + alone -- any other exception must still terminalize and release rather than + escape as an unhandled 500 with the slot still held. + """ + try: + dispatch_job(job, extractor=entry["name"], schema=entry["keys"], options=options) + except DispatchError: + logger.exception("agent-kv dispatch failed for job %s", job.id) + except Exception: + logger.exception("agent-kv dispatch raised unexpectedly for job %s", job.id) + else: + return None + return _fail_job_response( + job, org_id, "Job could not be dispatched; nothing was billed.", job_saved=True + ) + + +def _sync_wait_response(job, wait: float) -> Response | None: + """Poll until the job is terminal or ``wait`` elapses (§7.1 sync mode). + + Returns the full result payload if it terminalized in time, else ``None`` + so the caller falls through to the normal 202 handshake. + """ + if not wait: + return None + deadline = time.monotonic() + wait + while time.monotonic() < deadline: + job.refresh_from_db() + if job.status in AgentKVJob.TERMINAL: + from agent_kv.execution_views_result import result_payload + + return Response(result_payload(job), status=200) + time.sleep(1) + return None + + class SubmitView(APIView): authentication_classes: list = [] permission_classes: list = [] @@ -116,31 +185,9 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): if not check_key_rate(str(agent_kv_key.id)): raise RateLimited() - # Subscription admission (§6.6). A submit dispatches paid work, so it is - # gated exactly as an API deployment execute is -- same policy, same 402 - # bodies -- via the cloud plugin's gate, which calls the very - # `SubscriptionHelper` that cloud's `SubscriptionMiddleware` calls. - # - # Why here and not in that middleware: it resolves the org from the URL - # (`/deployment/api/{org_name}/...`). Agent-KV's URL carries no org - # segment -- the org is inside the Bearer key -- so the middleware's - # `get_organization_id` returns None for these requests, finds no - # subscription row, and admits every one of them. This is the first - # point where the org is actually known. - # - # `organization.organization_id` is the org SLUG -- the CharField - # `Subscription.organization_id` is keyed on, and what the deployment - # URL supplies as `org_name`. NOT `agent_kv_key.organization_id`, which - # is the Organization FK primary key: that matches no row, and the - # shared policy reads "no row" as "nothing to enforce", so the gate - # would silently admit everything while looking correctly wired. - gate_factory = plugin.get("service_class") - if gate_factory: # absent on a cloud build predating the gate - denied = gate_factory().check( - agent_kv_key.organization.organization_id, request - ) - if denied is not None: - return denied + denied = _subscription_denial(plugin, agent_kv_key, request) + if denied is not None: + return denied data = request.data.copy() part = data.get("extractors") @@ -188,44 +235,14 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): # range drives the shared OCR pass, so it cannot be per-extractor). options["page_start"] = v["page_start"] options["page_end"] = v["page_end"] - try: - dispatch_job( - job, extractor=entry["name"], schema=entry["keys"], options=options - ) - except DispatchError: - logger.exception("agent-kv dispatch failed for job %s", job.id) - return _fail_job_response( - job, - org_id, - "Job could not be dispatched; nothing was billed.", - job_saved=True, - ) - except Exception: - # Belt-and-braces: dispatch_job wraps its own internal failures - # as DispatchError, but nothing here may rely on that alone — - # any other exception must still terminalize the job and - # release the slot rather than escape as an unhandled 500. - logger.exception( - "agent-kv dispatch raised unexpectedly for job %s", - job.id, - ) - return _fail_job_response( - job, - org_id, - "Job could not be dispatched; nothing was billed.", - job_saved=True, - ) - wait = v["timeout"] - if wait: - deadline = time.monotonic() + wait - while time.monotonic() < deadline: - job.refresh_from_db() - if job.status in AgentKVJob.TERMINAL: - from agent_kv.execution_views_result import result_payload + failed = _dispatch_or_fail(job, org_id, entry, options) + if failed is not None: + return failed - return Response(result_payload(job), status=200) - time.sleep(1) + inline = _sync_wait_response(job, v["timeout"]) + if inline is not None: + return inline return Response( { diff --git a/backend/agent_kv/internal_views.py b/backend/agent_kv/internal_views.py index 744a571bf9..606b0af63a 100644 --- a/backend/agent_kv/internal_views.py +++ b/backend/agent_kv/internal_views.py @@ -150,6 +150,63 @@ class FinalizeView(APIView): authentication_classes: list = [] permission_classes: list = [] + @staticmethod + def _complete(job, job_id, org_id: str, body) -> bool: + """Persist the result and terminalize as COMPLETED. True if the guard won. + + ``write_result`` unavoidably runs *before* the ``mark_terminal`` guard + (the result has to exist to get a ``result_ref`` to pass in), so a + guard-loss right after the write -- a concurrent cancel or duplicate + finalize won instead -- leaves a result file with no ref anywhere + pointing at it. That orphan is deleted here rather than left to leak + past even TTL cleanup, which only ever acts on a job's *stored* + ``result_ref``. Best-effort; never blocks the response. + """ + result_ref = write_result(org_id, str(job_id), body.get("result") or {}) + finalized = AgentKVJob.mark_terminal( + job_id, + org_id, + JobStatus.COMPLETED, + result_ref=result_ref, + usage_summary=body.get("usage_summary"), + ) + if finalized: + job.status = JobStatus.COMPLETED + else: + delete_result_file(result_ref) + return finalized + + @staticmethod + def _fail(job, job_id, org_id: str, body) -> bool: + """Terminalize as FAILED. True if the guard won.""" + finalized = AgentKVJob.mark_terminal( + job_id, org_id, JobStatus.FAILED, error=body.get("error") or "" + ) + if finalized: + job.status = JobStatus.FAILED + return finalized + + @staticmethod + def _drop_staged_input(job, job_id, org_id: str) -> None: + """Delete the staged input now the run is over (spec D10). + + Only ever called when ``mark_terminal`` actually won, so it never runs + on the duplicate/guard-lost path -- there, either another writer owns + cleanup or there is nothing new to terminalize. A CANCELLED job never + reaches here either: cancel goes through JobCancelView/mark_terminal + directly, and a late finalize against an already-CANCELLED job loses + the terminal guard, so a cancelled job's input rides the normal TTL + sweep instead. + + The ref is blanked only when the file is confirmed gone -- otherwise it + is the only handle TTL cleanup could retry from, and blanking it would + orphan the uploaded document. + """ + if delete_input(job): + AgentKVJob.objects.filter(id=job_id, organization_id=org_id).update( + input_ref="" + ) + def post(self, request, *args, job_id=None, **kwargs): body = request.data org_id = body.get("org_id") @@ -166,51 +223,10 @@ def post(self, request, *args, job_id=None, **kwargs): finalized = False try: if job is not None and job.status not in AgentKVJob.TERMINAL: - if success: - result_ref = write_result( - org_id, str(job_id), body.get("result") or {} - ) - finalized = AgentKVJob.mark_terminal( - job_id, - org_id, - JobStatus.COMPLETED, - result_ref=result_ref, - usage_summary=body.get("usage_summary"), - ) - if finalized: - job.status = JobStatus.COMPLETED - else: - # Guard lost after the write above (e.g. a cancel - # raced in between the read and mark_terminal's - # guarded UPDATE): the result we just wrote has no - # ref anywhere -- unreachable forever unless cleaned - # up here. Best-effort; never blocks the response. - delete_result_file(result_ref) - else: - finalized = AgentKVJob.mark_terminal( - job_id, - org_id, - JobStatus.FAILED, - error=body.get("error") or "", - ) - if finalized: - job.status = JobStatus.FAILED - + handler = self._complete if success else self._fail + finalized = handler(job, job_id, org_id, body) if finalized: - # A CANCELLED job never reaches here: cancel goes - # through JobCancelView/mark_terminal directly, not - # this finalize path, and a late finalize call against - # an already-CANCELLED job loses the terminal guard - # above -- so a cancelled job's input intentionally - # rides the normal TTL sweep instead of being deleted - # here. - # Blank the ref only when the file is confirmed gone -- - # otherwise it is the only handle TTL cleanup can retry - # from, and blanking it orphans the uploaded document. - if delete_input(job): - AgentKVJob.objects.filter( - id=job_id, organization_id=org_id - ).update(input_ref="") + self._drop_staged_input(job, job_id, org_id) finally: AgentKVConcurrencyLimiter.release(org_id, str(job_id)) diff --git a/tests/e2e/agent_kv/test_agent_kv_e2e.py b/tests/e2e/agent_kv/test_agent_kv_e2e.py index b04578cea6..edfff5dbf0 100644 --- a/tests/e2e/agent_kv/test_agent_kv_e2e.py +++ b/tests/e2e/agent_kv/test_agent_kv_e2e.py @@ -618,31 +618,6 @@ def log_message(self, *args): # keep pytest output pristine thread.join(timeout=5) -# --------------------------------------------------------------------------- -# 9e. Calculations (sandbox worker, operator-gated) -# --------------------------------------------------------------------------- - -_CALC_ENV_GATE = "AGENT_KV_E2E_CALCULATIONS" - - -def _skip_unless_calculations_declared() -> None: - """Calculations need a stack running with ``AGENT_KV_CALCULATIONS_ENABLED - =true`` on the backend AND the sandbox codegen worker fleet actually up - and consuming ``sandbox_codegen`` (docs/agent-kv-api.md §11) -- neither - of which this test process can see or arrange for itself. So, like the - bad-LLM-key scenario above, it needs an explicit operator declaration - that the stack under test is configured that way; not every stack that - runs the rest of this lane has the sandbox worker running. - """ - if os.environ.get(_CALC_ENV_GATE) != "1": - pytest.skip( - "calculation e2e scenarios need a stack running with " - "AGENT_KV_CALCULATIONS_ENABLED=true and the sandbox codegen " - f"worker fleet up (docs/agent-kv-api.md §11); set {_CALC_ENV_GATE}=1 " - "to declare that this stack is configured that way" - ) - - def test_concurrency_limit_returns_429(agent_kv_key: AgentKVAuth) -> None: """Saturate the org's concurrency limiter; every other submit-based scenario in this module runs before this one, and ``tests/groups.yaml`` diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py index 6b28c3b07f..5eb2c9bb1e 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py @@ -226,6 +226,17 @@ def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema except ValueError as e: raise SchemaError(str(e)) from e + _enforce_shape_caps(key_specs, array_specs, caps) + _enforce_leaf_caps(key_specs, array_specs, caps) + constraints = _validated_constraints(spec, caps) + + return CompiledSchema( + key_specs=key_specs, array_specs=array_specs, constraints=list(constraints) + ) + + +def _enforce_shape_caps(key_specs, array_specs, caps: SchemaCaps) -> None: + """Bound how much structure the schema declares.""" if len(key_specs) > caps.max_leaves: raise SchemaError(f"schema exceeds max_leaves={caps.max_leaves}") if len(array_specs) > caps.max_arrays: @@ -236,6 +247,10 @@ def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema f"array '{aspec.path}' exceeds " f"max_columns_per_array={caps.max_columns_per_array}" ) + + +def _enforce_leaf_caps(key_specs, array_specs, caps: SchemaCaps) -> None: + """Bound every leaf's author-supplied text, across scalars and array columns.""" for kspec in key_specs + [s for a in array_specs for s in a.item_specs]: _reject_unsafe_regex(kspec.path, kspec.regex_pattern) if len(kspec.regex_pattern) > caps.max_regex_len: @@ -250,6 +265,9 @@ def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema f"max_description_len={caps.max_description_len}" ) + +def _validated_constraints(spec: dict, caps: SchemaCaps) -> list: + """Return the schema's `_constraints`, rejecting a malformed or oversized list.""" constraints = spec.get("_constraints", []) if not isinstance(constraints, list) or not all( isinstance(c, str) for c in constraints @@ -259,7 +277,4 @@ def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema raise SchemaError(f"schema exceeds max_constraints={caps.max_constraints}") for expr in constraints: _check_constraint_syntax(expr) - - return CompiledSchema( - key_specs=key_specs, array_specs=array_specs, constraints=list(constraints) - ) + return constraints diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py index 85efa58b2e..8c93096dba 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py @@ -108,6 +108,33 @@ def _coerce(raw: str): return n if n is not None else raw +def _parse_agg_call(node: ast.Call) -> tuple[str, str, str]: + """Validate `NAME('literal')` and split it into (fn, array_path, column). + + FAIL-CLOSED by construction: the only thing accepted is `NAME('literal')` + where NAME is in `_AGG`, the func is a bare `ast.Name` (no attribute access + -- blocks `os.system` / `x.__class__`), there is exactly one positional arg, + no keywords, and that arg is a string Constant. Any deviation raises `_Skip`, + so the constraint is skipped and never run. + """ + if not isinstance(node.func, ast.Name) or node.func.id not in _AGG: + raise _Skip() # not a whitelisted aggregate name + if node.keywords or len(node.args) != 1: + raise _Skip() # exactly one positional arg, no keywords + arg = node.args[0] + if not (isinstance(arg, ast.Constant) and isinstance(arg.value, str)): + raise _Skip() # arg must be a literal string path + ref = arg.value + # Split on the LAST dot: an ArraySpec.path may itself be dotted (e.g. + # 'invoice.lines'), while the column is always a row-LOCAL bare name. A bare + # ref (no dot) is a whole-array row count -> array_path=ref, column=''. + if "." in ref: + array_path, _, column = ref.rpartition(".") + else: + array_path, column = ref, "" + return node.func.id, array_path, column + + def _aggregate(node: ast.Call, arrays: dict[str, list[dict[str, str]]]): """Evaluate one of the five allowlisted aggregates over an array column. @@ -123,21 +150,7 @@ def _aggregate(node: ast.Call, arrays: dict[str, list[dict[str, str]]]): sku/description count too); `count('a')` is the row count. A sum/min/max/avg over zero usable cells raises `_Skip` (advisory) rather than guessing 0. """ - if not isinstance(node.func, ast.Name) or node.func.id not in _AGG: - raise _Skip() # not a whitelisted aggregate name - if node.keywords or len(node.args) != 1: - raise _Skip() # exactly one positional arg, no keywords - arg = node.args[0] - if not (isinstance(arg, ast.Constant) and isinstance(arg.value, str)): - raise _Skip() # arg must be a literal string path - fn, ref = node.func.id, arg.value - # Split on the LAST dot: an ArraySpec.path may itself be dotted (e.g. 'invoice.lines'), - # while the column is always a row-LOCAL bare name. A bare ref (no dot) is a whole-array - # row count -> array_path=ref, column=''. - if "." in ref: - array_path, _, column = ref.rpartition(".") - else: - array_path, column = ref, "" + fn, array_path, column = _parse_agg_call(node) rows = arrays.get(array_path) if rows is None: # no such array available raise _Skip() @@ -207,21 +220,32 @@ def _truth(node, values: dict[str, str], arrays: dict[str, list[dict[str, str]]] if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): return not _truth(node.operand, values, arrays) if isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators): - left = _operand(node.left, values, arrays) - for op, comp in zip(node.ops, node.comparators, strict=False): - if type(op) not in _CMP: - raise _Skip() - right = _operand(comp, values, arrays) - # only compare like-typed operands (number<->number, str<->str); else skip - if isinstance(left, (int, float)) != isinstance(right, (int, float)): - raise _Skip() - if not _CMP[type(op)](left, right): - return False - left = right - return True + return _compare_chain(node, values, arrays) raise _Skip() +def _compare_chain( + node: ast.Compare, values: dict[str, str], arrays: dict[str, list[dict[str, str]]] +) -> bool: + """Evaluate a (possibly chained) comparison, Python-style: `a < b < c`. + + Each link is checked against the previous operand, so the chain short- + circuits on the first False exactly as the language does. + """ + left = _operand(node.left, values, arrays) + for op, comp in zip(node.ops, node.comparators, strict=False): + if type(op) not in _CMP: + raise _Skip() + right = _operand(comp, values, arrays) + # only compare like-typed operands (number<->number, str<->str); else skip + if isinstance(left, (int, float)) != isinstance(right, (int, float)): + raise _Skip() + if not _CMP[type(op)](left, right): + return False + left = right + return True + + def _evaluate_one( expr: str, values: dict[str, str], arrays: dict[str, list[dict[str, str]]] ): diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py index 93cedeb508..04235763d2 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/kv_schema.py @@ -75,40 +75,7 @@ def _walk( ) if "_array" in node: - extra = set(node.keys()) - RESERVED_NODE - if extra: - raise ValueError( - f"Array node at {'.'.join(path_parts) or ''} has unexpected keys {sorted(extra)}; " - f"allowed: {sorted(RESERVED_NODE)}" - ) - item_schema = node["_array"] - if not isinstance(item_schema, dict) or not item_schema: - raise ValueError( - f"Array item schema at {'.'.join(path_parts)} must be a non-empty object" - ) - item_specs: list[KeySpec] = [] - for col_name, col_node in item_schema.items(): - if isinstance(col_node, dict) and "_array" in col_node: - raise ValueError( - f"Nested array at {'.'.join(path_parts)}.{col_name} is P8b (not supported in P8a)" - ) - _walk( - col_node, - [col_name], - item_specs, - [], # row-LOCAL paths; nested arrays sink to [] (rejected above) - max_depth=max_depth, - depth=depth + 1, - ) - arrays.append( - ArraySpec( - path=".".join(path_parts), - description=str(node.get("description", "")), - item_specs=item_specs, - key_column=str(node.get("_key", "")), - dedup_rows=_parse_dedup(node.get("_dedup", True), ".".join(path_parts)), - ) - ) + _walk_array(node, path_parts, arrays, max_depth=max_depth, depth=depth) return object_values = [v for v in node.values() if isinstance(v, dict)] @@ -135,7 +102,67 @@ def _walk( ) return - # Leaf node: scalar attributes only. + _append_leaf(node, path_parts, scalar_keys, out) + + +def _walk_array( + node: dict[str, Any], + path_parts: list[str], + arrays: list[ArraySpec], + *, + max_depth: int, + depth: int, +) -> None: + """Compile an `_array` node into one ArraySpec. + + Column paths are row-LOCAL: each column walks with its own name as the + whole path and an empty `arrays` sink, so a nested array would have nowhere + to land -- which is why it is rejected explicitly first rather than left to + disappear into that sink. + """ + extra = set(node.keys()) - RESERVED_NODE + if extra: + raise ValueError( + f"Array node at {'.'.join(path_parts) or ''} has unexpected keys {sorted(extra)}; " + f"allowed: {sorted(RESERVED_NODE)}" + ) + item_schema = node["_array"] + if not isinstance(item_schema, dict) or not item_schema: + raise ValueError( + f"Array item schema at {'.'.join(path_parts)} must be a non-empty object" + ) + item_specs: list[KeySpec] = [] + for col_name, col_node in item_schema.items(): + if isinstance(col_node, dict) and "_array" in col_node: + raise ValueError( + f"Nested array at {'.'.join(path_parts)}.{col_name} is P8b (not supported in P8a)" + ) + _walk( + col_node, + [col_name], + item_specs, + [], + max_depth=max_depth, + depth=depth + 1, + ) + arrays.append( + ArraySpec( + path=".".join(path_parts), + description=str(node.get("description", "")), + item_specs=item_specs, + key_column=str(node.get("_key", "")), + dedup_rows=_parse_dedup(node.get("_dedup", True), ".".join(path_parts)), + ) + ) + + +def _append_leaf( + node: dict[str, Any], + path_parts: list[str], + scalar_keys: list[str], + out: list[KeySpec], +) -> None: + """Compile a leaf node (scalar attributes only) into one KeySpec.""" unknown = [k for k in scalar_keys if k not in RESERVED] if unknown: raise ValueError( From 2f44cc4530bbb6836ce9656d0f5af2a7fdf2fc70 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 13:46:01 +0530 Subject: [PATCH 04/18] UN-4232 [FIX] DELETE on a lost terminal race blanked the winner's result_ref Greptile P1 on PR #2317. Real, and it is data loss. The race: DELETE reads a RUNNING job whose `result_ref` is still "", the finalize callback wins the guarded UPDATE in between and writes COMPLETED plus a real `result_ref`, and `mark_terminal` here returns False. Execution then continued against the STALE in-memory job. `delete_job_files` reports an already-empty ref as "cleared" -- correctly, there was nothing to delete -- so the stale `result_ref: ""` came back in `cleared`, and `job.save(update_fields=cleared)` wrote "" over the ref the callback had just persisted. The outcome: a job that reports COMPLETED, a result that 404s (`JobResultView` reads COMPLETED-without-a-ref as swept), and the result object orphaned in the bucket with nothing pointing at it -- TTL cleanup selects on `result_ref > ""`, so a blanked row never comes back as a candidate. Fix: re-read the row when the terminal race is lost, before touching files. Deleting is still the caller's intent; it just has to act on the refs that actually exist rather than on a copy that predates the winner's write. Only on the lost-race path -- winning means nothing else wrote to the row, so the in-memory copy is current and the extra query would be waste on the common path. Both directions are asserted. Mutation-checked: replacing the refresh with `pass` fails `test_delete_refreshes_the_job_when_it_loses_the_terminal_race`. One existing test needed a stub: this suite touches no database, so the new read is mocked in `test_delete_does_not_release_the_slot_when_it_loses_the_terminal_race`. 195 passed. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 17 +++++ backend/agent_kv/tests/test_job_views.py | 88 +++++++++++++++++++++++- 2 files changed, 104 insertions(+), 1 deletion(-) diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index 2853a6dafb..d0ae1ee6c4 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -331,6 +331,23 @@ def delete(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): # the winner is still accounting for; release() is idempotent # (zrem), so the winner's own release stays correct. AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + else: + # Lost the race: a finalize or cancel terminalized this job + # between our read above and the guarded UPDATE. `job` is now + # STALE, and `result_ref` is the field that matters -- a winning + # finalize has just written one. + # + # Without this refresh the cleanup below runs against the stale + # copy, where `result_ref` is still "". `delete_job_files` + # reports an already-empty ref as "cleared" (nothing to delete), + # so the save then writes "" OVER the winner's real ref. The job + # stays COMPLETED, its result 404s (`JobResultView` treats + # COMPLETED-without-a-ref as swept), and the object is orphaned + # in the bucket with nothing left pointing at it -- TTL cleanup + # selects on `result_ref > ""`, so a blanked row never comes + # back. Deleting is still the caller's intent; it just has to + # act on the refs that actually exist now. + job.refresh_from_db() # Blank only the refs whose files are confirmed gone, so a ref whose # delete failed survives as the handle TTL cleanup retries from. 204 # either way: the job IS terminal and the caller's intent is recorded, diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index a070af8d76..fa2dded333 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -411,12 +411,15 @@ def test_delete_on_running_job_releases_the_concurrency_slot( # --------------------------------------------------------------------------- @mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") @mock.patch.object(AgentKVJob, "save") +# A lost race re-reads the row before cleanup (see 8i); this suite touches no +# database, so the read itself is stubbed out here. +@mock.patch.object(AgentKVJob, "refresh_from_db") @mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) @mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) @mock.patch.object(AgentKVJob, "objects") @mock.patch.object(AgentKVKey, "objects") def test_delete_does_not_release_the_slot_when_it_loses_the_terminal_race( - m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_refresh, m_save, m_release ): m_keys.get.return_value = kv_key() job = AgentKVJob(status=JobStatus.RUNNING) @@ -511,3 +514,86 @@ def test_all_job_views_401_without_key(): req = getattr(APIRequestFactory(), method)("/agent-kv/x") resp = view.as_view()(req, job_id=uuid.uuid4()) assert resp.status_code == 403 + + +# --------------------------------------------------------------------------- +# (8i) Losing the terminal race must RE-READ the job before touching its files. +# +# The race: DELETE reads a RUNNING job (result_ref ""), the finalize callback +# wins the guarded UPDATE in between and writes COMPLETED + a real result_ref, +# and `mark_terminal` here returns False. Without a refresh the cleanup below +# runs against the stale copy: `delete_job_files` reports an already-empty +# `result_ref` as "cleared" (there was nothing to delete), and the save then +# writes "" OVER the winner's real ref. The job stays COMPLETED, its result +# 404s, and the object is orphaned -- TTL cleanup selects on `result_ref > ""`, +# so a blanked row never comes back. +# +# Reported by Greptile on PR #2317. +# --------------------------------------------------------------------------- +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_refreshes_the_job_when_it_loses_the_terminal_race( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + # The winner's write, applied by refresh_from_db: COMPLETED + a real ref. + def _win_the_race(): + job.status = JobStatus.COMPLETED + job.result_ref = "org1/job/result.json" + + with mock.patch.object( + AgentKVJob, "refresh_from_db", side_effect=_win_the_race + ) as m_refresh: + # Capture what the cleanup actually SAW. `call_args` would be useless + # here: it records a reference to the same job object the view then + # blanks, so by assertion time it reads "" whether the fix works or not. + seen: list[str] = [] + + def _record(j): + seen.append(j.result_ref) + return ["input_ref", "result_ref"] if j.result_ref else ["input_ref"] + + m_delete_files.side_effect = _record + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert m_refresh.called, ( + "a lost terminal race must re-read the job; acting on the stale copy " + "blanks the winner's result_ref and orphans the result file" + ) + # The file helper saw the winner's ref, so the real object is deleted... + assert seen == ["org1/job/result.json"] + # ...and the blanking is of a ref that was genuinely cleared, not a stale "". + assert m_save.call_args.kwargs["update_fields"] == ["input_ref", "result_ref"] + + +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_does_not_refresh_when_it_wins_the_terminal_race( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release +): + """Winning means nothing else wrote to the row, so the in-memory copy is + current and the extra query would be waste on the common path. + """ + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + m_jobs.get.return_value = job + + with mock.patch.object(AgentKVJob, "refresh_from_db") as m_refresh: + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert not m_refresh.called From d01cb1785ee082f9687e8968059d2d9b13e4cc8c Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 14:26:34 +0530 Subject: [PATCH 05/18] UN-4232 [FIX] Four review findings: gate fail-open, unscheduled sweeps, page cap, cancel webhook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four traced and confirmed before changing anything. 1. SUBSCRIPTION GATE FAILED OPEN (`execution_views.py`) A plugin without `service_class` skipped the check and continued to dispatch. Reachable only on a cloud image predating the gate -- but the admitted request spends money, and this route's URL carries no org segment, so `SubscriptionMiddleware` cannot catch it downstream either: a mixed deploy ran unmetered paid work with nothing anywhere enforcing entitlement. Now refuses, via a new `SubscriptionGateUnavailable` (503). Deliberately not a 402 -- the subscription was never evaluated, and reporting it as denied would send an operator to the billing system for what is an image-pairing problem. `test_plugin_without_a_service_class_still_proceeds` is inverted to `..._is_refused`; it pinned the old behaviour as a contract, so it had to change rather than be deleted. 13 other submit tests now carry an admitting gate. 2. NOTHING SCHEDULED THE SWEEP OR THE TTL CLEANUP (migration 0004) Both tasks registered, both endpoints live, no `PgPeriodicTask` row anywhere -- the module docstring said an operator registers them by hand, which means they never ran. So an OOM-killed executor left its row non-terminal forever (`link_error` never fires for a killed process) holding a concurrency slot until the 6h Redis TTL, and `AGENT_KV_RESULT_TTL_DAYS` was advisory: every customer document retained indefinitely. Data migration following the dashboard_metrics precedent (0004-0006): same table, `update_or_create` so a re-run is idempotent, `pg_owned: False` so applying it does not itself start firing them. Sweep every 10 minutes (a stuck job holds a slot); TTL cleanup daily at 03:17, off the hour and off the metrics cleanups so two bulk deletes do not share a tick. The new test pins the migration's `task_name`s against the worker's registered wire names as literals -- an import would pass vacuously in the backend venv, which is exactly where the two sides drift. 3. PAGE CAP COUNTED THE DOCUMENT, NOT THE REQUEST (`execution_serializers.py`) Asking for pages 1-5 of a 400-page PDF was refused against a 100-page cap, despite requesting five pages of work. The cap now bounds the selected range. `pages_total` is unchanged -- it is what metering and the status document report -- and a new `pages_selected` carries what the cap compares. Also rejects a `page_start` past the end of the document, which previously selected nothing and billed for a job over zero pages. 4. CANCELLATION NEVER SENT THE DOCUMENTED WEBHOOK (`dispatch.py`, both cancel paths) Cancellation does not reach finalize, so no webhook fired; and a late executor callback could not send one either -- it loses the terminal guard, and `_maybe_webhook` correctly declines a non-fresh finalize. A caller who supplied `webhook_url` was simply never told, against docs §8. New `agent_kv_cancelled` worker task, routed on the existing callback queue, and enqueued by the backend from both `JobCancelView` and the DELETE-side cancel. Guarded on `won`, which is what makes the two paths mutually exclusive: a cancel that lost means a finalize won and will send. Exactly one path owns the notification. Best-effort enqueue -- the job is already cancelled and the caller already has their 200, so a lost notification logs rather than failing their request. The SSRF waiver moved into one `_send_webhook` helper so the cancel path cannot drift from the finalize path on what it permits. 211 backend tests pass (up from 195), plus ide_callback 18, registry 3, scheduler 7, task-imports 4, queue wiring 9. ruff clean. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/dispatch.py | 43 +++++++ backend/agent_kv/exceptions.py | 17 +++ backend/agent_kv/execution_serializers.py | 31 +++++- backend/agent_kv/execution_views.py | 42 ++++++- .../migrations/0004_pg_periodic_tasks.py | 97 ++++++++++++++++ backend/agent_kv/tests/test_job_views.py | 80 +++++++++++++ .../tests/test_periodic_tasks_migration.py | 105 ++++++++++++++++++ .../agent_kv/tests/test_submit_serializer.py | 45 ++++++++ backend/agent_kv/tests/test_submit_view.py | 61 +++++++--- workers/ide_callback/agent_kv_tasks.py | 65 +++++++++-- .../shared/infrastructure/config/registry.py | 4 + 11 files changed, 555 insertions(+), 35 deletions(-) create mode 100644 backend/agent_kv/migrations/0004_pg_periodic_tasks.py create mode 100644 backend/agent_kv/tests/test_periodic_tasks_migration.py diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py index 54166bc7ad..d2d8348686 100644 --- a/backend/agent_kv/dispatch.py +++ b/backend/agent_kv/dispatch.py @@ -20,6 +20,49 @@ class DispatchError(Exception): """Enqueue failed; the caller terminalizes the job (spec §5.3).""" +def dispatch_cancelled_webhook(job) -> None: + """Queue the terminal webhook for a job the API just cancelled. + + Cancellation never reaches finalize -- ``JobCancelView`` and DELETE + terminalize the row themselves -- so without this the caller who supplied + ``webhook_url`` is never told, and a late executor callback cannot tell them + either (it loses the terminal guard, and the callback declines a non-fresh + finalize rather than double-notifying). Docs §8 promises delivery on + terminal states; this is the cancel half of that promise. + + Call ONLY when the guarded cancel actually won. That is what makes the two + paths mutually exclusive: a cancel that lost means a finalize won and will + send, and a cancel that won means no fresh finalize can. + + Best-effort by design. The job is already cancelled and the caller already + has their 200; failing their request because a notification could not be + QUEUED would be the wrong trade, so this logs and returns rather than + raising. Delivery itself is the worker's problem. + """ + if not job.webhook_url: + return + try: + from pg_queue.producer import enqueue_task + + enqueue_task( + task_name="agent_kv_cancelled", + queue=CALLBACK_QUEUE, + kwargs={ + "callback_kwargs": { + "job_id": str(job.id), + "webhook_url": job.webhook_url, + } + }, + org_id=str(job.organization_id), + ) + except Exception: + logger.exception( + "agent-kv: could not queue the cancellation webhook for job %s; " + "the job IS cancelled, only the notification was lost", + job.id, + ) + + def _dispatcher(): # No `celery_app`: UN-4046 removed that parameter when the routing # dispatcher's Celery branch went with the pg_queue_enabled flag. Passing it diff --git a/backend/agent_kv/exceptions.py b/backend/agent_kv/exceptions.py index 459821d2ef..6c84a9dd19 100644 --- a/backend/agent_kv/exceptions.py +++ b/backend/agent_kv/exceptions.py @@ -14,3 +14,20 @@ class RateLimited(APIException): class JobNotFound(APIException): status_code = 404 default_detail = "Job not found" + + +class SubscriptionGateUnavailable(APIException): + """The engine plugin is installed but exposes no subscription gate. + + Deliberately NOT a 402: the org's subscription was never evaluated, so + claiming it was denied would be a lie. This is a deployment fault — a build + that can run billable work but cannot check entitlement — and it fails + CLOSED, because the alternative is admitting unmetered paid work on a route + whose URL carries no org segment for the middleware to fall back on. + """ + + status_code = 503 + default_detail = ( + "agent-kv cannot verify subscription entitlement on this deployment; " + "the engine plugin exposes no subscription gate" + ) diff --git a/backend/agent_kv/execution_serializers.py b/backend/agent_kv/execution_serializers.py index 38e845b497..974adaf186 100644 --- a/backend/agent_kv/execution_serializers.py +++ b/backend/agent_kv/execution_serializers.py @@ -239,7 +239,13 @@ class SubmitSerializer(serializers.Serializer): #: ``{extractor_name: compiled schema}`` -- populated during validation. compiled = None + #: Measured page count of the uploaded document. None for Excel, which has + #: no pre-OCR page concept. This is what metering and the status document + #: report, and it is NOT what the page cap is compared against. pages_total = None + #: How many pages the request actually asks to process, after `page_start` / + #: `page_end` are applied. This is what the cap bounds. None for Excel. + pages_selected = None def validate_file(self, f): name = (f.name or "").lower() @@ -311,11 +317,30 @@ def validate(self, data): raise serializers.ValidationError({"file": "Unreadable PDF"}) finally: f.seek(0) - if self.pages_total > settings.AGENT_KV_MAX_PAGES: + if start > self.pages_total: raise serializers.ValidationError( { - "file": f"Document has {self.pages_total} pages; " - f"max is {settings.AGENT_KV_MAX_PAGES} (§6.1)" + "page_start": f"page_start {start} is past the end of a " + f"{self.pages_total}-page document" + } + ) + # The cap bounds the work the job will DO, not the size of the file + # it was handed. A caller asking for pages 1-5 of a 400-page PDF is + # requesting five pages of OCR and extraction; refusing that against + # a 100-page cap rejected a request that was inside the documented + # limit. `pages_total` stays the measured document count -- it is + # what metering and the status document report -- and only the cap + # comparison moves to the selected range. + last_page = self.pages_total if end is None else min(end, self.pages_total) + self.pages_selected = last_page - start + 1 + if self.pages_selected > settings.AGENT_KV_MAX_PAGES: + raise serializers.ValidationError( + { + "file": f"Requested {self.pages_selected} pages " + f"(page_start={start}, page_end=" + f"{end if end is not None else self.pages_total}) of a " + f"{self.pages_total}-page document; max is " + f"{settings.AGENT_KV_MAX_PAGES} (§6.1)" } ) # Excel: no page concept pre-OCR (spec §6.1); pages_total stays None, diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index d0ae1ee6c4..b6b476af0c 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -9,8 +9,13 @@ from rest_framework.views import APIView from agent_kv.constants import STAGE_NAMES_BY_EXTRACTOR -from agent_kv.dispatch import DispatchError, dispatch_job -from agent_kv.exceptions import EngineUnavailable, JobNotFound, RateLimited +from agent_kv.dispatch import DispatchError, dispatch_cancelled_webhook, dispatch_job +from agent_kv.exceptions import ( + EngineUnavailable, + JobNotFound, + RateLimited, + SubscriptionGateUnavailable, +) from agent_kv.execution_serializers import SubmitSerializer from agent_kv.execution_views_result import result_payload from agent_kv.key_validator import AgentKVKeyValidator @@ -125,10 +130,31 @@ def _subscription_denial(plugin, agent_kv_key, request) -> Response | None: Organization FK primary key: that matches no row, and the shared policy reads "no row" as "nothing to enforce", so the gate would silently admit everything while looking correctly wired. + + **Fails closed when the plugin exposes no gate.** This used to return None + -- admit -- to tolerate a cloud build predating the gate. But the admitted + request dispatches billable LLM and OCR work, and this route's URL carries + no org segment, so ``SubscriptionMiddleware`` cannot catch it downstream + either: a mixed deploy (this backend against a pre-gate cloud image) would + run unmetered paid work with nothing anywhere enforcing entitlement. + + Refusing instead is the conservative read of a deployment that can spend + money but cannot check whether it may. It surfaces as a 503 naming the + cause, not a 402 -- the subscription was never evaluated, and reporting it + as denied would send an operator to the billing system for what is an + image-pairing problem. + + Raises: + SubscriptionGateUnavailable: the engine plugin is installed but exposes + no ``service_class``. """ gate_factory = plugin.get("service_class") - if not gate_factory: # absent on a cloud build predating the gate - return None + if not gate_factory: + logger.error( + "agent-kv: engine plugin exposes no subscription gate; refusing the " + "submit rather than dispatching billable work unmetered" + ) + raise SubscriptionGateUnavailable() return gate_factory().check(agent_kv_key.organization.organization_id, request) @@ -331,6 +357,9 @@ def delete(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): # the winner is still accounting for; release() is idempotent # (zrem), so the winner's own release stays correct. AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + # This request owns the terminal notification: no fresh finalize + # can follow a cancel that won, so nothing else will send it. + dispatch_cancelled_webhook(job) else: # Lost the race: a finalize or cancel terminalized this job # between our read above and the guarded UPDATE. `job` is now @@ -402,6 +431,11 @@ def post(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): # release() is idempotent (zrem), so a later finalize-callback # release on a cancel-mid-run is harmless. AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + # Docs §8 promises a terminal notification, and cancellation never + # reaches finalize -- so this path has to send it. Guarded on `won` + # so a cancel that LOST leaves the notification to the finalize that + # beat it, and the caller is told exactly once. + dispatch_cancelled_webhook(job) return Response({"status": "cancelled"}, status=200) # Lowercased for cross-endpoint consistency (spec §7.2). return Response({"status": job.status.lower()}, status=409) diff --git a/backend/agent_kv/migrations/0004_pg_periodic_tasks.py b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py new file mode 100644 index 0000000000..5a0cb7f8e7 --- /dev/null +++ b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py @@ -0,0 +1,97 @@ +"""Schedule the two Agent-KV maintenance periodics. + +Both tasks were registered in ``workers/scheduler/agent_kv_tasks.py`` and both +internal endpoints existed, but nothing anywhere created a row to fire them -- +no ``PgPeriodicTask``, no beat entry, no CronJob. The module's own docstring +said an operator must register them by hand, which in practice means they never +ran. + +What that costs, concretely: + +* ``run_sweep`` never runs, so an OOM-killed executor leaves its job row + non-terminal forever -- ``link_error`` never fires for a killed process -- and + the row holds its concurrency slot until the 6h Redis TTL. Enough of those and + the org's allowance is exhausted and new submits start getting 429s. +* ``run_ttl_cleanup`` never runs, so ``AGENT_KV_RESULT_TTL_DAYS`` is advisory: + every staged customer document and every result stays in the bucket + indefinitely. That is a retention-policy failure, not just a disk-usage one. + +Follows the ``dashboard_metrics`` precedent (migrations 0004-0006) rather than +inventing a mechanism: the same ``PgPeriodicTask`` table, the same +``update_or_create`` shape so a re-run is idempotent, and ``pg_owned: False`` so +applying this migration does not itself start firing them -- the rollout flag +decides that, exactly as it does for the metrics periodics. + +Cadences are deliberately offset from each other and off the hour: the sweep is +cheap and wants to be frequent (a stuck job holds a slot), the TTL cleanup is a +bulk delete that should not land on the same tick as anything else. +""" + +from django.db import migrations + +PG_PERIODIC_TASKS = [ + { + "name": "agent_kv_sweep", + # Wire name registered by workers/scheduler/agent_kv_tasks.py. + "task_name": "agent_kv.sweep", + "queue": "scheduler", + "task_args": [], + "task_kwargs": {}, + # Every 10 minutes: a stuck job holds a concurrency slot, and the + # handler is idempotent and batch-capped server-side, so a missed tick + # costs nothing and a frequent one is cheap. + "cron_string": "*/10 * * * *", + }, + { + "name": "agent_kv_ttl_cleanup", + "task_name": "agent_kv.ttl_cleanup", + "queue": "scheduler", + "task_args": [], + "task_kwargs": {}, + # Daily at 03:17 UTC. Off the hour and off the dashboard_metrics + # cleanups (02:00 / 03:00) so a bulk object-store delete does not land + # on the same tick as another bulk job. + "cron_string": "17 3 * * *", + }, +] + + +def create_pg_periodic_tasks(apps, schema_editor): + PgPeriodicTask = apps.get_model("pg_queue", "PgPeriodicTask") + for spec in PG_PERIODIC_TASKS: + PgPeriodicTask.objects.update_or_create( + name=spec["name"], + defaults={ + "task_name": spec["task_name"], + "queue": spec["queue"], + "task_args": spec["task_args"], + "task_kwargs": spec["task_kwargs"], + "cron_string": spec["cron_string"], + # Platform-wide, not per-org (spec §5.4): both internal + # endpoints sweep across every organization. + "org_id": "", + "enabled": True, + # Inert until the rollout flag decides otherwise; never fired by + # applying this migration. Same posture as dashboard_metrics. + "pg_owned": False, + }, + ) + + +def remove_pg_periodic_tasks(apps, schema_editor): + PgPeriodicTask = apps.get_model("pg_queue", "PgPeriodicTask") + PgPeriodicTask.objects.filter( + name__in=[spec["name"] for spec in PG_PERIODIC_TASKS] + ).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("agent_kv", "0003_agentkvjob_cleanup_failed_at"), + # The table this seeds. + ("pg_queue", "0003_pgperiodictask"), + ] + + operations = [ + migrations.RunPython(create_pg_periodic_tasks, remove_pg_periodic_tasks), + ] diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index fa2dded333..dc1890111c 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -597,3 +597,83 @@ def test_delete_does_not_refresh_when_it_wins_the_terminal_race( assert resp.status_code == 204 assert not m_refresh.called + + +# --------------------------------------------------------------------------- +# (8j) Cancellation must deliver the documented terminal webhook. +# +# Cancellation never reaches finalize -- JobCancelView and DELETE terminalize +# the row themselves -- so nothing in the finalize path ever fired a webhook for +# a cancelled job, and a late executor callback cannot either: it loses the +# terminal guard, and `_maybe_webhook` correctly declines a non-fresh finalize. +# A caller who supplied `webhook_url` was simply never told, despite docs §8. +# +# Guarded on `won`, which is what keeps exactly one path owning the +# notification: a cancel that LOST means a finalize won and will send. +# +# Reported by Greptile on PR #2317. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_that_wins_queues_the_terminal_webhook( + m_keys, m_jobs, m_mark_terminal, m_release, m_hook +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="https://hook.example/x") + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert m_hook.called, "a won cancel must notify; nothing else will" + assert m_hook.call_args.args[0] is job + + +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=False) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_that_loses_does_not_queue_a_webhook( + m_keys, m_jobs, m_mark_terminal, m_release, m_hook +): + """The finalize that beat it owns the notification. Sending here too would + double-notify the caller for one job. + """ + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="https://hook.example/x") + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 409 + assert not m_hook.called + + +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_that_cancels_a_running_job_also_queues_the_webhook( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release, m_hook +): + """DELETE on a non-terminal job cancels it first; that cancellation is just + as terminal, and just as invisible to the caller without this. + """ + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING, webhook_url="https://hook.example/x") + job.organization_id = "org1" + m_jobs.get.return_value = job + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert m_hook.called diff --git a/backend/agent_kv/tests/test_periodic_tasks_migration.py b/backend/agent_kv/tests/test_periodic_tasks_migration.py new file mode 100644 index 0000000000..662db83338 --- /dev/null +++ b/backend/agent_kv/tests/test_periodic_tasks_migration.py @@ -0,0 +1,105 @@ +"""The two maintenance periodics must be scheduled, not merely registered. + +Both tasks were registered in ``workers/scheduler/agent_kv_tasks.py`` and both +internal endpoints existed, but nothing anywhere created a row to fire them. +"Wired but unscheduled" is an invisible failure: the code looks complete, the +endpoints respond to a manual call, and the only symptoms are that stuck jobs +keep their concurrency slots until a 6h Redis TTL and that customer documents +are retained forever despite a documented ``AGENT_KV_RESULT_TTL_DAYS``. + +These assertions pin the migration's declared rows against the wire names the +worker actually registers, so renaming one side without the other fails here +rather than silently stopping the schedule. + +Reported by Greptile on PR #2317, and as 2.3 in the branch review. +""" + +import importlib + +import pytest + +# The module name starts with a digit, so it cannot be a plain `import`. +MIGRATION = importlib.import_module("agent_kv.migrations.0004_pg_periodic_tasks") + +#: Wire names registered by `@worker_task(name=...)` in +#: `workers/scheduler/agent_kv_tasks.py`. Duplicated as literals on purpose: +#: the workers package is not importable from the backend's test venv, and an +#: import would make this pass vacuously wherever it is absent -- which is +#: exactly the environment where the two sides drift apart. +WORKER_TASK_NAMES = {"agent_kv.sweep", "agent_kv.ttl_cleanup"} + + +def _specs() -> dict: + return {spec["name"]: spec for spec in MIGRATION.PG_PERIODIC_TASKS} + + +def test_both_maintenance_tasks_are_scheduled(): + assert set(_specs()) == {"agent_kv_sweep", "agent_kv_ttl_cleanup"} + + +def test_every_scheduled_row_points_at_a_task_the_worker_registers(): + """A row naming a task nothing registers is a schedule that fires into + nothing -- the same invisible failure, one layer along. + """ + declared = {spec["task_name"] for spec in MIGRATION.PG_PERIODIC_TASKS} + + assert declared == WORKER_TASK_NAMES, ( + f"migration schedules {sorted(declared)} but workers/scheduler/" + f"agent_kv_tasks.py registers {sorted(WORKER_TASK_NAMES)}" + ) + + +@pytest.mark.parametrize("name", ["agent_kv_sweep", "agent_kv_ttl_cleanup"]) +def test_rows_are_scheduled_onto_the_scheduler_queue(name): + assert _specs()[name]["queue"] == "scheduler" + + +def test_rows_are_platform_wide_and_enabled(): + """Both internal endpoints sweep across every organization (spec §5.4), so a + non-empty `org_id` would scope them to one tenant and silently leave every + other org unswept. + + Asserted on the writer rather than the spec table: `org_id`/`enabled` are + set once in `defaults`, not per row. + """ + import inspect + + source = inspect.getsource(MIGRATION.create_pg_periodic_tasks) + + assert '"org_id": ""' in source + assert '"enabled": True' in source + + +@pytest.mark.parametrize("name", ["agent_kv_sweep", "agent_kv_ttl_cleanup"]) +def test_cron_strings_are_five_field_and_parseable(name): + """A malformed cron string is accepted by the column and then never fires.""" + fields = _specs()[name]["cron_string"].split() + + assert len(fields) == 5, _specs()[name]["cron_string"] + + +def test_the_sweep_runs_far_more_often_than_the_ttl_cleanup(): + """Not cosmetic: a stuck job holds a concurrency slot, so the sweep has to + be frequent, while the TTL cleanup is a bulk object-store delete that should + not run on a tight loop. If these ever converge, one of them is wrong. + """ + sweep = _specs()["agent_kv_sweep"]["cron_string"] + ttl = _specs()["agent_kv_ttl_cleanup"]["cron_string"] + + assert sweep.startswith("*/"), f"sweep should be interval-based, got {sweep!r}" + assert not ttl.startswith("*/"), f"ttl cleanup should be daily, got {ttl!r}" + + +def test_rows_are_created_inert(): + """`pg_owned: False` means applying the migration does not itself start + firing them -- the rollout flag decides, same posture as the + dashboard_metrics periodics this follows. + """ + # The flag lives in the migration's writer, not its spec table, so assert on + # the source rather than re-running the RunPython against a live DB. + import inspect + + source = inspect.getsource(MIGRATION.create_pg_periodic_tasks) + + assert '"pg_owned": False' in source + assert "update_or_create" in source, "re-running the migration must be idempotent" diff --git a/backend/agent_kv/tests/test_submit_serializer.py b/backend/agent_kv/tests/test_submit_serializer.py index 179bd00916..b89eda14d4 100644 --- a/backend/agent_kv/tests/test_submit_serializer.py +++ b/backend/agent_kv/tests/test_submit_serializer.py @@ -159,6 +159,7 @@ def test_oversize_file_rejected(): def test_page_cap_rejected(): + """No range given, so the whole document is the selection.""" with mock.patch("agent_kv.execution_serializers.settings") as m: _defaults(m) m.AGENT_KV_MAX_PAGES = 1 @@ -167,6 +168,50 @@ def test_page_cap_rejected(): assert "pages" in str(s.errors).lower() +def test_page_cap_counts_the_selected_range_not_the_whole_document(): + """The cap bounds the work the job will DO, not the size of the file. + + A caller asking for one page of a two-page document is requesting one page + of OCR and extraction. Counting the whole document rejected requests that + were inside the documented limit. Reported by Greptile on #2317. + """ + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_MAX_PAGES = 1 + s = SubmitSerializer(data=_data(page_start=1, page_end=1)) + assert s.is_valid(), s.errors + assert s.pages_selected == 1 + # The measured document count is unchanged -- it is what metering and + # the status document report, and only the cap comparison moved. + assert s.pages_total == 2 + + +def test_an_open_ended_range_is_capped_at_the_last_page(): + """`page_end` past the end selects to the end, it does not inflate the count.""" + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_MAX_PAGES = 2 + s = SubmitSerializer(data=_data(page_start=2, page_end=999)) + assert s.is_valid(), s.errors + assert s.pages_selected == 1 + + +def test_a_selected_range_over_the_cap_is_still_rejected(): + with mock.patch("agent_kv.execution_serializers.settings") as m: + _defaults(m) + m.AGENT_KV_MAX_PAGES = 1 + s = SubmitSerializer(data=_data(page_start=1, page_end=2)) + assert not s.is_valid() + assert "Requested 2 pages" in _errs(s) + + +def test_page_start_past_the_end_of_the_document_is_rejected(): + """Otherwise the selection is empty and the job runs over nothing, billed.""" + s = SubmitSerializer(data=_data(page_start=5)) + assert not s.is_valid() + assert "past the end" in _errs(s) + + def test_extractors_not_json_rejected(): s = SubmitSerializer(data=_data(extractors="{not json")) assert not s.is_valid() diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index 3e5b821114..565662fce2 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -22,6 +22,19 @@ def _post(data=None): return APIRequestFactory().post("/agent-kv/", data or {}, format="multipart") +def _plugin_with_gate(denial=None): + """A plugin dict shaped like the cloud build's, with a gate that admits. + + Every submit test needs one now: a plugin WITHOUT `service_class` means the + deployment cannot check entitlement, and the view refuses rather than + dispatching billable work unmetered (see + `test_plugin_without_a_service_class_is_refused`). + """ + gate = mock.Mock() + gate.check.return_value = denial + return {"module": object(), "service_class": lambda: gate} + + def _authed_post(data=None): req = _post(data) req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" @@ -106,7 +119,7 @@ def test_absent_plugin_501s_before_anything(m_keys, m_plugin): # 429: per-key rate limit refusal. # --------------------------------------------------------------------------- @mock.patch.object(ev, "check_key_rate", return_value=False) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_key_rate_limited_429s(m_keys, m_plugin, m_rate): m_keys.get.return_value = kv_key() @@ -124,7 +137,7 @@ def test_key_rate_limited_429s(m_keys, m_plugin, m_rate): @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_concurrency_limited_429s_with_no_job_row( m_keys, m_plugin, m_rate, m_serializer_cls, m_limiter, m_stage, m_save @@ -148,7 +161,7 @@ def test_concurrency_limited_429s_with_no_job_row( @mock.patch.object(ev, "stage_input") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_stage_input_failure_releases_slot_and_500s_with_safe_body( m_keys, m_plugin, m_rate, m_serializer_cls, m_stage, m_limiter @@ -186,7 +199,7 @@ def test_stage_input_failure_releases_slot_and_500s_with_safe_body( @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_happy_path_returns_202_with_job_id_status_and_status_url( m_keys, @@ -239,7 +252,7 @@ def _side_effect(job, *, extractor, schema, options): @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_dispatch_job_called_with_expected_options_and_schema( m_keys, @@ -298,7 +311,7 @@ def test_dispatch_job_called_with_expected_options_and_schema( @mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_dispatch_failure_marks_job_failed_releases_slot_and_500s( m_keys, @@ -346,7 +359,7 @@ def test_dispatch_failure_marks_job_failed_releases_slot_and_500s( @mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_dispatch_job_raising_non_dispatch_error_is_still_caught_and_cleaned_up( m_keys, @@ -388,7 +401,7 @@ def test_dispatch_job_raising_non_dispatch_error_is_still_caught_and_cleaned_up( @mock.patch.object(ev, "stage_input", return_value="org/o/agent_kv/j/input.pdf") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_platform_key_lookup_failure_inside_real_dispatch_is_caught_end_to_end( m_keys, @@ -432,7 +445,7 @@ def test_platform_key_lookup_failure_inside_real_dispatch_is_caught_end_to_end( @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_timeout_zero_returns_immediately_without_polling( m_keys, @@ -474,7 +487,7 @@ def test_timeout_zero_returns_immediately_without_polling( @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "SubmitSerializer") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_sync_wait_returns_200_with_failure_body_when_job_fails_mid_wait( m_keys, @@ -519,6 +532,8 @@ def _refresh_side_effect(job, *args, **kwargs): # resolves org_id=None for these requests and lets every one of them through. # The gate is therefore invoked here, after key validation. # --------------------------------------------------------------------------- + + def _key_with_org(slug="acme-slug", pk=42): """A key whose FK pk and org slug are deliberately DIFFERENT values. @@ -604,13 +619,21 @@ def test_subscription_gate_is_passed_the_org_slug_not_the_fk_pk( @mock.patch.object(ev, "check_key_rate", return_value=True) @mock.patch.object(ev, "get_plugin", return_value={"module": object()}) @mock.patch.object(AgentKVKey, "objects") -def test_plugin_without_a_service_class_still_proceeds( +def test_plugin_without_a_service_class_is_refused( m_keys, m_plugin, m_rate, m_serializer_cls, m_limiter ): - """A cloud build predating the gate exposes no `service_class`. The view - must degrade to today's behaviour rather than 500 -- the capability probe - above already 501s an engine-less deployment, so this only covers the - version skew between an older plugin and a newer backend. + """A plugin that exposes no `service_class` cannot check entitlement, so + the submit is REFUSED rather than admitted. + + This used to degrade to "proceed", to tolerate a cloud image predating the + gate. But the admitted request dispatches billable LLM and OCR work, and + this route's URL carries no org segment, so `SubscriptionMiddleware` cannot + catch it downstream either -- a mixed deploy would run unmetered paid work + with nothing anywhere enforcing entitlement. Reported by Greptile on #2317. + + 503, not 402: the subscription was never evaluated, so reporting it as + denied would send an operator to the billing system for what is an + image-pairing problem. """ m_keys.get.return_value = _key_with_org() _mock_serializer(m_serializer_cls) @@ -618,7 +641,9 @@ def test_plugin_without_a_service_class_still_proceeds( resp = ev.SubmitView.as_view()(_authed_post()) - assert resp.status_code == 429 # reached the concurrency limiter, not a 500 + assert resp.status_code == 503, resp.data + # Refused BEFORE a slot was taken or anything was staged. + assert not m_limiter.check_and_acquire.called # --------------------------------------------------------------------------- @@ -655,7 +680,7 @@ def _real_multipart_post(extractors, **job_level): @mock.patch.object(ev, "stage_input") @mock.patch.object(ev, "AgentKVConcurrencyLimiter") @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_real_serializer_through_real_view_reaches_dispatch_intact_for_table( m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch @@ -701,7 +726,7 @@ def test_real_serializer_through_real_view_reaches_dispatch_intact_for_table( @mock.patch.object(ev, "check_key_rate", return_value=True) -@mock.patch.object(ev, "get_plugin", return_value={"module": object()}) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") def test_real_serializer_rejects_the_old_flat_shape_with_400(m_keys, m_plugin, m_rate): """End-to-end proof of the hard switch: a caller on the pre-§7.0 format gets diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index b74cb1ed31..0a87e16fed 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -156,6 +156,56 @@ def agent_kv_error( return None +@worker_task(name="agent_kv_cancelled") +def agent_kv_cancelled( + callback_kwargs: dict[str, Any] | None = None, +) -> dict[str, Any] | None: + """Fire the terminal webhook for a job cancelled by the API. + + Cancellation does not go through finalize: ``JobCancelView`` (and DELETE on + a non-terminal job) terminalizes the row directly, releases the slot and + returns. So nothing in the finalize path ever fired a webhook for it, and a + late executor callback cannot either -- it loses the terminal guard and + ``_maybe_webhook`` correctly declines a non-fresh finalize. The caller who + supplied ``webhook_url`` was simply never told, despite docs §8 promising + delivery on terminal states. + + Enqueued by the backend ONLY when its guarded cancel actually won, which is + what keeps this from double-notifying: if the cancel lost to a finalize, the + backend does not enqueue and the finalize callback sends instead. Exactly + one of the two paths owns the notification for any given job. + """ + cb = callback_kwargs or {} + job_id = cb.get("job_id", "") + url = cb.get("webhook_url", "") + if not url: + return None + try: + _send_webhook(url, job_id, "cancelled") + except Exception: + logger.exception("agent_kv_cancelled webhook failed: job_id=%s", job_id) + raise + return {"job_id": job_id, "status": "cancelled"} + + +def _send_webhook(url: str, job_id: str, status: str) -> None: + """Deliver one terminal notification. + + Single place the SSRF waiver is read, so the cancel path cannot drift from + the finalize path on what it permits. + """ + # Test/dev stacks only (e2e lane): waive the SSRF guards so a receiver on + # the compose host is reachable. Unset/false in production. + allow_insecure = os.environ.get( + "AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE", "" + ).lower() in ("1", "true", "yes") + send_webhook( + url, + {"job_id": job_id, "status": status}, + allow_insecure=allow_insecure, + ) + + def _maybe_webhook(finalize_response: dict[str, Any], job_id: str) -> None: """Fire the completion webhook, but only for a fresh finalize. @@ -163,19 +213,14 @@ def _maybe_webhook(finalize_response: dict[str, Any], job_id: str) -> None: was already terminal) as well as for an unknown job -- either way the webhook already fired (or never should), so firing again here would double-notify the caller. + + A job cancelled through the API is the "already terminal" case, and its + notification is owned by ``agent_kv_cancelled``, enqueued by the backend at + the moment its guarded cancel won. """ if not finalize_response.get("finalized"): return url = finalize_response.get("webhook_url") or "" if not url: return - # Test/dev stacks only (e2e lane): waive the SSRF guards so a receiver on - # the compose host is reachable. Unset/false in production. - allow_insecure = os.environ.get( - "AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE", "" - ).lower() in ("1", "true", "yes") - send_webhook( - url, - {"job_id": job_id, "status": finalize_response.get("status", "")}, - allow_insecure=allow_insecure, - ) + _send_webhook(url, job_id, finalize_response.get("status", "")) diff --git a/workers/shared/infrastructure/config/registry.py b/workers/shared/infrastructure/config/registry.py index 01c39aee73..56fc84ff80 100644 --- a/workers/shared/infrastructure/config/registry.py +++ b/workers/shared/infrastructure/config/registry.py @@ -161,6 +161,10 @@ class WorkerRegistry: TaskRoute("ide_callback.tasks.*", QueueName.IDE_CALLBACK), TaskRoute("agent_kv_complete", QueueName.AGENT_KV_CALLBACK), TaskRoute("agent_kv_error", QueueName.AGENT_KV_CALLBACK), + # Cancellation does not go through finalize, so its terminal + # webhook has no callback to ride on; the backend enqueues this + # directly when its guarded cancel wins. + TaskRoute("agent_kv_cancelled", QueueName.AGENT_KV_CALLBACK), ], ), } From 66553a114be195593a7cda1f0fc18c305ddd89ac Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 14:38:19 +0530 Subject: [PATCH 06/18] =?UTF-8?q?UN-4232=20[FIX]=201.2=20=E2=80=94=20the?= =?UTF-8?q?=20table=20API=20operation=20had=20no=20billing=20backstop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `execute_extraction` logs when an op in `_LLM_BEARING_OPS` finishes successfully having emitted no usage records. That log line is the ONLY signal that a run's billing produced nothing: the cloud `flush()` returns an empty list rather than raising, so total loss of a job's usage rows is otherwise indistinguishable from a job that legitimately made no LLM calls. `table_extract_api` was absent from the set, so the entire Agent-KV table path shipped with no backstop at all — every table job drives two LLMs, and a run that lost all of its billing rows would have produced zero log lines. Adds the op, and a test file pinning the set against a declared list of paid operations in BOTH directions: a new paid op missing from the set fails, and an op added to the set without being declared fails too. One-directional would let the pair drift into a superset and quietly stop meaning anything. Also documents on the set itself that keeping a paid op out of it is a missing alarm rather than a missing log line — the next person adding an operation is the one who needs to know that. Note `kv_extract` is deliberately NOT added: `kv` is not routable on this deployment, so declaring it here would assert a backstop for a path that cannot run. It belongs with the branch that makes the extractor routable. Companion: the cloud half of 1.2 hardens `flush()` itself. Co-Authored-By: Claude Opus 5 --- workers/executor/tasks.py | 14 +++++++ workers/tests/test_llm_bearing_ops.py | 55 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 workers/tests/test_llm_bearing_ops.py diff --git a/workers/executor/tasks.py b/workers/executor/tasks.py index 16b97c2c33..f6545c2300 100644 --- a/workers/executor/tasks.py +++ b/workers/executor/tasks.py @@ -42,12 +42,26 @@ # ``executor/worker.py`` reaches it by importing this module. register_all() +#: Operations that are expected to emit usage records. An op in this set that +#: finishes successfully with NO records logs below -- it is the only signal +#: that billing silently produced nothing. +#: +#: Keeping a paid op out of this set is not a missing log line, it is a missing +#: alarm: the cloud `flush()` returns `[]` on a skipped LLM without raising, so +#: total loss of a run's billing rows is otherwise indistinguishable from a run +#: that legitimately made no LLM calls. Add every new LLM-bearing operation here +#: at the same time as the operation itself. _LLM_BEARING_OPS = frozenset( { "answer_prompt", "single_pass_extraction", "summarize", "structure_pipeline", + # The Agent-KV API's table extractor (cloud `agentic_table`'s blind-API + # operation). Every run drives two LLMs; a run of this op with no usage + # records means the billing chain broke, never that there was nothing + # to bill. + "table_extract_api", } ) diff --git a/workers/tests/test_llm_bearing_ops.py b/workers/tests/test_llm_bearing_ops.py new file mode 100644 index 0000000000..77a602e5cd --- /dev/null +++ b/workers/tests/test_llm_bearing_ops.py @@ -0,0 +1,55 @@ +"""Every paid operation must be in the no-usage-records backstop. + +`execute_extraction` logs when an op in `_LLM_BEARING_OPS` finishes +successfully having emitted no usage records. That log line is the ONLY signal +that a run's billing produced nothing: the cloud `flush()` returns an empty list +rather than raising, so total loss of a job's usage rows is otherwise +indistinguishable from a job that legitimately made no LLM calls. + +So leaving a paid op out of the set is not a missing log line, it is a missing +alarm — and the gap is invisible, because the symptom is silence. + +`table_extract_api` (the Agent-KV API's table extractor) was absent, which is +how the whole Agent-KV table path shipped with no billing backstop at all. +Reported as 1.2 in the branch review. +""" + +from executor.tasks import _LLM_BEARING_OPS + +#: Operations known to drive at least one LLM call on every successful run. +#: Add to this list and the set together; a new paid op that lands in neither is +#: exactly the failure this file exists to catch. +PAID_OPERATIONS = { + "answer_prompt", + "single_pass_extraction", + "summarize", + "structure_pipeline", + # Drives two LLMs (advanced + lite) on every run. + "table_extract_api", +} + + +def test_the_table_api_operation_is_covered(): + """The regression this file was added for.""" + assert "table_extract_api" in _LLM_BEARING_OPS + + +def test_every_known_paid_operation_is_covered(): + missing = PAID_OPERATIONS - _LLM_BEARING_OPS + assert not missing, ( + f"{sorted(missing)} drive LLM calls but are absent from " + f"_LLM_BEARING_OPS, so a run of one that emits zero usage records " + f"logs nothing and the lost billing is silent" + ) + + +def test_the_set_has_not_quietly_grown_past_what_is_declared_here(): + """The reverse direction, so the two lists stay a real pair rather than one + drifting into a superset of the other. + """ + unexpected = _LLM_BEARING_OPS - PAID_OPERATIONS + assert not unexpected, ( + f"{sorted(unexpected)} were added to _LLM_BEARING_OPS without being " + f"declared here; add them to PAID_OPERATIONS with a note on what they " + f"spend" + ) From 1280337452c3f293ba6aecd3c4c2ef15cdff37c1 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 14:56:17 +0530 Subject: [PATCH 07/18] =?UTF-8?q?UN-4232=20[FIX]=202.1,=202.2,=202.4=20?= =?UTF-8?q?=E2=80=94=20double-validated=20identity,=20an=20unrouted=20API,?= =?UTF-8?q?=20a=20swallowed=20finalize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.1 — EXTRACTOR IDENTITY WAS DECIDED TWICE, UNDER TWO DIFFERENT VALUES `validate_keys` branched on raw `initial_data["name"]`; `validate_name` saw the value DRF had already trimmed (`CharField.trim_whitespace` defaults True). So `{"name": " table "}` passed the name check as `table` and then took the **kv** branch for keys, never running `TableKeysSerializer`. That is not merely untidy. A kv-shaped payload like `{"target_table": {"description": "x"}}` compiles cleanly as a KV schema, so the submit returned **202** and dispatched to `agentic_table` with a `target_table` that is a dict rather than the string the binding requires -- the document staged, the job billed, and it failed at the executor. Keys validation moves into `validate()`, where `data["name"]` is trimmed and already checked against `SUPPORTED_EXTRACTORS` exactly once, behind a `_KEYS_SERIALIZERS` table mirroring `_OPTIONS_SERIALIZERS`. An extractor absent from it falls through to the schema compiler, which is `kv`'s actual contract. Mutation-checked by restoring the raw-name lookup. 2.2 — /agent-kv/ WAS NEVER ROUTED THROUGH THE COMPOSE STACK traefik's backend rule matched `/api/v1`, `/deployment` and `/public`; the frontend rule is the negation of exactly those three. `base_urls.py` mounts the API at `/agent-kv/`, so every request through the stack was served by the SPA's nginx and never reached Django. The e2e lane could not catch it: `conftest.py` talks to `UNSTRACT_BACKEND_URL` (port 8000) directly, bypassing traefik. So the API was unreachable exactly as a customer would reach it, and green. Both rules updated. Three guards added to `test_queue_consumer_wiring.py` -- the established home for "configured-looking but unreachable" wiring. The third asserts the RELATIONSHIP rather than a hardcoded prefix: every prefix routed to the backend must be excluded from the frontend, so the next mount point cannot be added to one side only. Mutation-checked. 2.4 — A FAILED FINALIZE WAS REPORTED AS A SUCCESSFUL CALLBACK `agent_kv_error` caught, logged and returned None. The consumer records that as SUCCESS, deletes the message and moves on: no retry, no dead letter, no failed-task record. Since this task is the SOLE terminalizer of a failed job, a momentary backend blip during finalize stranded the job in RUNNING until the sweep terminalized it with "Job timed out" -- overwriting the real executor error, which existed only in the log line above. Now `bind=True` with backoff retries, raising on exhaustion, matching its sibling `agent_kv_complete` and the `process_batch_callback_api` precedent in `workers/callback/tasks.py`. Verified first that the PG transport honours task-level autoretry -- `queue_backend/pg_queue/consumer.py` relies on it explicitly for the executor task. `test_finalize_raising_is_swallowed_and_logged` asserted `result is None`, pinning the silence as a contract. Inverted rather than deleted: that assertion is exactly what a regression would restore. Mutation-checked. 214 backend tests (up from 211), ide_callback 20, queue wiring 12. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_serializers.py | 52 ++++++++++---- .../agent_kv/tests/test_submit_serializer.py | 68 +++++++++++++++++++ docker/docker-compose.yaml | 4 +- workers/ide_callback/agent_kv_tasks.py | 41 +++++++++-- .../tests/test_agent_kv_callbacks.py | 39 +++++++++-- workers/tests/test_queue_consumer_wiring.py | 67 ++++++++++++++++++ 6 files changed, 249 insertions(+), 22 deletions(-) diff --git a/backend/agent_kv/execution_serializers.py b/backend/agent_kv/execution_serializers.py index 974adaf186..8d4f750b8d 100644 --- a/backend/agent_kv/execution_serializers.py +++ b/backend/agent_kv/execution_serializers.py @@ -154,6 +154,13 @@ def validate(self, data): TABLE_EXTRACTOR_NAME: TableOptionsSerializer, } +#: Each extractor's own `keys` validator. An extractor absent from this table +#: falls through to the schema compiler, which is `kv`'s contract: its `keys` IS +#: a compiled schema rather than a fixed set of fields. +_KEYS_SERIALIZERS = { + TABLE_EXTRACTOR_NAME: TableKeysSerializer, +} + class ExtractorSerializer(serializers.Serializer): """One entry of the submit's `extractors` array (spec §7.0/§7.1).""" @@ -171,24 +178,43 @@ def validate_name(self, v): ) return v - def validate_keys(self, spec): - name = (self.initial_data or {}).get("name") - if name == TABLE_EXTRACTOR_NAME: - keys = TableKeysSerializer(data=spec if isinstance(spec, dict) else {}) + def _validated_keys(self, name: str, spec): + """Validate `keys` for the extractor named by the VALIDATED `name`. + + Not a `validate_keys` field validator, deliberately. That ran before + object-level validation and branched on raw ``self.initial_data["name"]`` + -- while ``validate_name`` saw the value DRF had already trimmed + (``CharField.trim_whitespace`` defaults True). So the extractor's + identity was decided twice, under two different values: + ``{"name": " table ", ...}`` passed the name check as ``table`` and then + took the **kv** branch here, never running ``TableKeysSerializer``. A + kv-shaped payload like ``{"target_table": {"description": "x"}}`` + compiles cleanly as a KV schema, so the submit returned **202** and + dispatched to ``agentic_table`` with a `target_table` that is a dict + rather than the string the binding requires -- the job staged, billed + and then failed at the executor. + + Branching on `data["name"]` in ``validate()`` means the name is trimmed + and already checked against ``SUPPORTED_EXTRACTORS`` exactly once. + """ + keys_cls = _KEYS_SERIALIZERS.get(name) + if keys_cls is not None: + keys = keys_cls(data=spec if isinstance(spec, dict) else {}) keys.is_valid(raise_exception=True) return keys.validated_data - # kv: size is capped on the SERIALIZED form -- the cap exists to bound - # parse and compile cost, and `keys` arrives here already parsed out of - # the `extractors` JSON. - # ensure_ascii=False so this measures the SAME bytes the outer - # `extractors` cap measured. + + # kv (and any future extractor with no dedicated keys serializer): size + # is capped on the SERIALIZED form -- the cap exists to bound parse and + # compile cost, and `keys` arrives here already parsed out of the + # `extractors` JSON. ensure_ascii=False so this measures the SAME bytes + # the outer `extractors` cap measured. serialized = json.dumps(spec, ensure_ascii=False).encode("utf-8") if len(serialized) > settings.AGENT_KV_MAX_SCHEMA_BYTES: - raise serializers.ValidationError("keys schema too large") + raise serializers.ValidationError({"keys": "keys schema too large"}) try: self.compiled = compile_schema(spec) except SchemaError as e: - raise serializers.ValidationError(str(e)) + raise serializers.ValidationError({"keys": str(e)}) return spec def validate(self, data): @@ -202,7 +228,9 @@ def validate(self, data): raise serializers.ValidationError( f"unknown keys on extractor entry: {sorted(unknown)}" ) - opts_cls = _OPTIONS_SERIALIZERS[data["name"]] + name = data["name"] + data["keys"] = self._validated_keys(name, data.get("keys")) + opts_cls = _OPTIONS_SERIALIZERS[name] opts = opts_cls(data=data.get("options") or {}) opts.is_valid(raise_exception=True) data["options"] = opts.validated_data diff --git a/backend/agent_kv/tests/test_submit_serializer.py b/backend/agent_kv/tests/test_submit_serializer.py index b89eda14d4..027ee3f16c 100644 --- a/backend/agent_kv/tests/test_submit_serializer.py +++ b/backend/agent_kv/tests/test_submit_serializer.py @@ -408,3 +408,71 @@ def test_kv_unknown_option_is_rejected(): s = _kv_options(qaa=True) # typo for `qa` assert not s.is_valid() assert "unknown options for extractor 'kv'" in str(s.errors) + + +# --------------------------------------------------------------------------- +# Extractor identity must be decided ONCE. +# +# `validate_keys` used to branch on raw `initial_data["name"]` while +# `validate_name` saw the value DRF had already trimmed +# (`CharField.trim_whitespace` defaults True). So `" table "` passed the name +# check as `table` and then took the **kv** branch for keys, never running +# `TableKeysSerializer`. +# +# A kv-shaped payload compiles cleanly as a KV schema, so the submit returned +# 202 and dispatched to `agentic_table` with a `target_table` that is a dict +# rather than the string the binding requires -- staged, billed, then failed at +# the executor. +# +# Reported as 2.1 in the branch review. +# --------------------------------------------------------------------------- +def test_a_padded_extractor_name_still_validates_keys_as_that_extractor(): + s = SubmitSerializer( + data=_data( + extractors=json.dumps( + [ + { + # Trimmed to "table" by validate_name... + "name": " table ", + # ...but this is a KV-shaped schema, not a table one. It + # compiles fine as KV, which is how it used to reach 202. + "keys": {"target_table": {"description": "Grand total"}}, + } + ] + ) + ) + ) + + assert not s.is_valid(), ( + "a kv-shaped keys payload was accepted for the table extractor; the " + "job would stage, bill and then fail at the executor on target_table" + ) + assert "target_table" in _errs(s) + + +def test_a_padded_name_is_still_normalised_for_routing(): + """The trim itself is fine and worth keeping -- the bug was deciding + identity twice, not accepting whitespace. + """ + s = SubmitSerializer( + data=_data(extractors=json.dumps([{"name": " table ", "keys": TABLE_KEYS}])) + ) + + assert s.is_valid(), s.errors + assert s.validated_data["extractors"][0]["name"] == "table" + + +def test_table_keys_are_validated_through_the_keys_serializer(): + """Reaching TableKeysSerializer is what rejects unknown members; the kv + fallback would have compiled them as a schema instead. + """ + s = SubmitSerializer( + data=_data( + extractors=json.dumps( + [{"name": "table", "keys": {"target_table": "T", "bogus": 1}}] + ) + ) + ) + + assert not s.is_valid() + assert "unknown keys for extractor 'table'" in _errs(s) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 50c8d8fad4..eedfdf7a39 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -71,7 +71,7 @@ services: - PG_SCHEDULER_ADOPT_PERIODICS=true labels: - traefik.enable=true - - traefik.http.routers.backend.rule=Host(`frontend.unstract.localhost`) && (PathPrefix(`/api/v1`) || PathPrefix(`/deployment`) || PathPrefix(`/public`)) + - traefik.http.routers.backend.rule=Host(`frontend.unstract.localhost`) && (PathPrefix(`/api/v1`) || PathPrefix(`/deployment`) || PathPrefix(`/public`) || PathPrefix(`/agent-kv`)) - traefik.http.services.backend.loadbalancer.server.port=8000 # Celery Flower @@ -113,7 +113,7 @@ services: - UNSTRACT_APPS_VERSION=${VERSION} labels: - traefik.enable=true - - traefik.http.routers.frontend.rule=Host(`frontend.unstract.localhost`) && !PathPrefix(`/api/v1`) && !PathPrefix(`/deployment`) && !PathPrefix(`/public`) + - traefik.http.routers.frontend.rule=Host(`frontend.unstract.localhost`) && !PathPrefix(`/api/v1`) && !PathPrefix(`/deployment`) && !PathPrefix(`/public`) && !PathPrefix(`/agent-kv`) - traefik.http.services.frontend.loadbalancer.server.port=80 platform-service: diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index 0a87e16fed..615434bc79 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -130,12 +130,41 @@ def agent_kv_complete( raise -@worker_task(name="agent_kv_error") +@worker_task( + bind=True, + name="agent_kv_error", + autoretry_for=(Exception,), + max_retries=3, + retry_backoff=True, + retry_backoff_max=300, + retry_jitter=True, +) def agent_kv_error( + self, failed_task_id: str, callback_kwargs: dict[str, Any] | None = None, ) -> dict[str, Any] | None: - """Celery link_error callback when the agent-kv executor task raises.""" + """Celery link_error callback when the agent-kv executor task raises. + + This is the SOLE terminalizer of a failed job, so a finalize that does not + happen is a job stuck in RUNNING. It used to catch, log and ``return None`` + -- which the consumer records as SUCCESS, deletes the message, and moves on: + no retry, no dead letter, no failed-task record. A momentary backend blip + during finalize therefore stranded the job until the sweep eventually + terminalized it with "Job timed out", overwriting the real executor error, + which existed only in the log line above. + + Now retries with backoff and RAISES on exhaustion, matching its sibling + ``agent_kv_complete`` and the ``process_batch_callback_api`` precedent in + ``workers/callback/tasks.py``. The PG consumer relies on task-level + autoretry for exactly this (see its own comment in + ``queue_backend/pg_queue/consumer.py``). + + ``autoretry_for=(Exception,)`` rather than a narrow transport type: the + whole point is that this task must not quietly give up, and a programming + error that burns three retries and then surfaces loudly is a far better + outcome than one that returns None and reports success. + """ cb = callback_kwargs or {} job_id = cb.get("job_id", "") org_id = cb.get("org_id", "") @@ -151,9 +180,13 @@ def agent_kv_error( except Exception: logger.exception( - "agent_kv_error callback failed: job_id=%s org_id=%s", job_id, org_id + "agent_kv_error callback failed: job_id=%s org_id=%s (retry %s of %s)", + job_id, + org_id, + self.request.retries, + self.max_retries, ) - return None + raise @worker_task(name="agent_kv_cancelled") diff --git a/workers/ide_callback/tests/test_agent_kv_callbacks.py b/workers/ide_callback/tests/test_agent_kv_callbacks.py index 4b1e2f1ef1..07ad5bf24b 100644 --- a/workers/ide_callback/tests/test_agent_kv_callbacks.py +++ b/workers/ide_callback/tests/test_agent_kv_callbacks.py @@ -290,10 +290,22 @@ def test_pg_transport_empty_explicit_error_falls_back_to_async_result( @patch(_PATCH_SEND_WEBHOOK) @patch(_PATCH_GET_CLIENT) - def test_finalize_raising_is_swallowed_and_logged( + def test_finalize_raising_propagates_instead_of_reporting_success( self, mock_get_client, mock_send_webhook, cb_kwargs, caplog ): - """agent_kv_error mirrors ide_index_error: swallow, log, don't raise.""" + """A finalize that fails must NOT be reported as a completed callback. + + This test previously asserted `result is None` -- pinning the silence as + a contract. That silence was the bug: `agent_kv_error` is the sole + terminalizer of a failed job, and returning None makes the consumer + record SUCCESS, delete the message and move on. No retry, no dead + letter, no failed-task record; the job sat in RUNNING until the sweep + terminalized it with "Job timed out", overwriting the real executor + error. Inverted rather than deleted, because the old assertion is + exactly what a regression would restore. + + Reported as 2.4 in the branch review. + """ import logging api = MagicMock() @@ -303,13 +315,32 @@ def test_finalize_raising_is_swallowed_and_logged( with ( patch(_PATCH_ASYNC_RESULT, return_value=MagicMock(result=None)), caplog.at_level(logging.ERROR, logger="ide_callback.agent_kv_tasks"), + pytest.raises(Exception, match="backend unreachable"), ): - result = self._call("failed-task-6", cb_kwargs) + self._call("failed-task-6", cb_kwargs) - assert result is None assert "agent_kv_error callback failed" in caplog.text mock_send_webhook.assert_not_called() + def test_agent_kv_error_is_configured_to_retry_before_giving_up(self): + """The retry budget is what turns a momentary backend blip into a + delayed terminalization rather than a stranded job. + """ + from ide_callback.agent_kv_tasks import agent_kv_error + + assert agent_kv_error.max_retries == 3 + assert Exception in agent_kv_error.autoretry_for + + def test_agent_kv_error_matches_its_siblings_failure_posture(self): + """`agent_kv_complete` re-raises; these two are the success and failure + halves of one contract and must not disagree about what a failed + finalize means. + """ + from ide_callback.agent_kv_tasks import agent_kv_complete, agent_kv_error + + assert agent_kv_complete is not None + assert agent_kv_error.max_retries is not None + # --------------------------------------------------------------------------- # Webhook firing rules (shared by both callbacks) diff --git a/workers/tests/test_queue_consumer_wiring.py b/workers/tests/test_queue_consumer_wiring.py index 0c496bdd32..0868d6f35c 100644 --- a/workers/tests/test_queue_consumer_wiring.py +++ b/workers/tests/test_queue_consumer_wiring.py @@ -188,3 +188,70 @@ def test_run_worker_pg_ide_callback_role_drains_the_callback_queue(): match = re.search(r'\["\$PG_ROLE_IDE_CALLBACK"\]="ide_callback;([^"]+)"', text) assert match, "could not find the PG_ROLE_IDE_CALLBACK entry in run-worker.sh" assert AGENT_KV_CALLBACK_QUEUE in _queues(match.group(1)) + + +# -------------------------------------------------------------------------- +# Traefik routing. Same family of failure as an unconsumed queue: the service +# is up, the route looks configured, and the request lands somewhere that +# cannot serve it. +# +# `/agent-kv/` is mounted by `backend/backend/base_urls.py`, but traefik's +# backend rule matched only `/api/v1`, `/deployment` and `/public`, and the +# frontend rule is the negation of exactly those -- so every Agent-KV request +# through the compose stack was served by the SPA's nginx and never reached +# Django. The e2e lane did not catch it because it talks to +# `UNSTRACT_BACKEND_URL` (port 8000) directly, bypassing traefik entirely. +# +# Reported as 2.2 in the branch review. +# -------------------------------------------------------------------------- + +AGENT_KV_PREFIX = "/agent-kv" + + +def _router_rule(compose_path: Path, router: str) -> str: + svc_labels = [] + for svc in yaml.safe_load(compose_path.read_text())["services"].values(): + svc_labels.extend(svc.get("labels") or []) + prefix = f"traefik.http.routers.{router}.rule=" + for label in svc_labels: + if isinstance(label, str) and label.startswith(prefix): + return label[len(prefix) :] + pytest.fail(f"no traefik rule for router {router!r} in {compose_path.name}") + + +def test_traefik_routes_agent_kv_to_the_backend(): + rule = _router_rule(DEV_COMPOSE, "backend") + assert f"PathPrefix(`{AGENT_KV_PREFIX}`)" in rule, ( + "traefik does not route /agent-kv to the backend; the request is served " + "by the frontend SPA and never reaches Django" + ) + + +def test_traefik_excludes_agent_kv_from_the_frontend(): + """The frontend rule is a negation list, so a prefix missing from it is + claimed by the SPA even once the backend also matches -- whichever router + wins, one of them is wrong. + """ + rule = _router_rule(DEV_COMPOSE, "frontend") + assert f"!PathPrefix(`{AGENT_KV_PREFIX}`)" in rule, ( + "the frontend router still claims /agent-kv; it must be excluded " + "explicitly, exactly as /api/v1, /deployment and /public are" + ) + + +def test_every_backend_prefix_is_excluded_from_the_frontend(): + """The two rules are each other's complement by construction. Asserting the + relationship rather than one hardcoded prefix means the next mount point + cannot be added to one side only. + """ + backend_rule = _router_rule(DEV_COMPOSE, "backend") + frontend_rule = _router_rule(DEV_COMPOSE, "frontend") + + backend_prefixes = set(re.findall(r"PathPrefix\(`([^`]+)`\)", backend_rule)) + excluded = set(re.findall(r"!PathPrefix\(`([^`]+)`\)", frontend_rule)) + + missing = backend_prefixes - excluded + assert not missing, ( + f"{sorted(missing)} route to the backend but are not excluded from the " + f"frontend router; the SPA will claim them" + ) From 4a20ef714be12bdff2e8ffdbd0295b4176305674 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 15:06:30 +0530 Subject: [PATCH 08/18] =?UTF-8?q?UN-4232=20[FIX]=202.5,=202.9,=202.11,=202?= =?UTF-8?q?.12=20=E2=80=94=20an=20over-subscribed=20concurrency=20cap=20an?= =?UTF-8?q?d=20three=20silent=20failures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.5 — CANCEL RELEASED THE SLOT WITHOUT STOPPING THE EXECUTOR `mark_terminal` excludes only TERMINAL, so cancelling a DISPATCHED/RUNNING job returned won=True and the slot was released immediately. Nothing revokes a running executor -- `job.task_id` is written at dispatch and never read again -- so the engine kept running, kept calling LLMs and kept billing while its slot was handed to the next submit. Submit-then-cancel in a loop therefore ran arbitrarily many concurrent extractions against a ceiling of AGENT_KV_CONCURRENT_LIMIT, all paid for. This is a cost bug, not just a bookkeeping one. Release is now narrowed to jobs that never reached an executor, via a `_never_dispatched` helper applied at both cancel sites. Narrowing loses nothing: a mid-run cancel's slot is released by FinalizeView's `finally` when the callback lands, and release() is idempotent. `test_delete_on_running_job_releases_the_concurrency_slot` asserted the release -- it pinned the bug. Inverted rather than deleted, with the positive case (never-dispatched DOES release) added alongside, plus a unit test for the PENDING-but-dispatched edge: post-enqueue bookkeeping can fail after the task is queued, leaving a row that reads PENDING while an executor runs, and releasing that would over-subscribe exactly as before. 2.9 — maintenance.py HAD NO LOGGER AT ALL Not "logged too little": no `import logging`, no logger, in the module that can terminalize a thousand jobs as FAILED in one call. A backlog of stranded jobs was indistinguishable from a quiet, healthy system. Both entry points now report what they did, at WARNING when the counts are non-zero (jobs WERE stranded and slots WERE held) and INFO when there was nothing to do. The scheduler proxies log their result too -- counts alone cannot distinguish "ran and found nothing" from "never ran", which is precisely the state this task pair shipped in. Also pinned `retained` in the TTL return shape. The test asserted `{"cleaned": 2}` while the endpoint returns `{"cleaned": N, "retained": M}`, so the field that counts files it could NOT delete was outside the contract. 2.11 — `except Exception: pass` LOST THE REAL EXECUTOR ERROR An empty catch with no log, around the result-backend lookup. The common case is a kombu deserialization failure: the executor raised a cloud-plugin exception class not importable in the OSS ide_callback image, so the error exists and this process cannot read it. The job was then finalized with "Executor failed without an error message" -- the exact useless error the lookup exists to avoid -- and the cause was unrecoverable even from logs. 2.12 — AN UNKNOWN JOB AND A DUPLICATE RETURNED THE SAME 200 FinalizeView and StageReportView both answered a byte-identical 200 no-op whether the job was already terminal (ordinary) or no row matched the job/org pair at all (never ordinary). The module imported `logger` and never called it. That second case is what an org-slug-vs-FK-pk mix-up looks like, and dispatch.py documents that exact confusion shipping once already. If it recurs, every finalize is a silent no-op, every job stays non-terminal and is reaped as "Job timed out": a 100% failure rate presenting as timeouts, with nothing anywhere. Both views now log the unknown-job case and return a `reason` field so the worker can tell them apart. Three finalize tests asserted the exact response dict and gained the new key -- an additive contract change, not a loosened assertion. 221 backend tests (from 218), ide_callback 20, scheduler 7, queue wiring 12. Every fix mutation-checked by reintroducing the original behaviour. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 80 ++++++++++---- backend/agent_kv/internal_views.py | 41 ++++++- backend/agent_kv/maintenance.py | 30 +++++ backend/agent_kv/tests/test_internal_views.py | 78 ++++++++++++- backend/agent_kv/tests/test_job_views.py | 103 +++++++++++++++++- workers/ide_callback/agent_kv_tasks.py | 14 ++- workers/scheduler/agent_kv_tasks.py | 12 +- .../tests/test_agent_kv_scheduler_tasks.py | 11 +- 8 files changed, 338 insertions(+), 31 deletions(-) diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index b6b476af0c..d3a4821412 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -89,6 +89,18 @@ def _status_document(job) -> dict: return doc +def _never_dispatched(job) -> bool: + """True when this job never reached an executor, so no callback will come. + + Read from the row as it was BEFORE this request terminalized it. Both + conditions are deliberate: `dispatched_at` is stamped by post-enqueue + bookkeeping that can legitimately fail, and `status` is what the dispatcher + advances -- a job with either marker set has, or may have, a running + executor whose finalize callback owns the slot. + """ + return job.dispatched_at is None and job.status == JobStatus.PENDING + + def _fail_job_response(job, org_id: str, message: str, *, job_saved: bool) -> Response: """Shared cleanup for any post-acquire submit failure (spec §5.3/§5.4). @@ -343,20 +355,35 @@ def delete(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): job.id, job.organization_id, JobStatus.CANCELLED ) if won: - # Release the concurrency slot, exactly as JobCancelView does - # and for the same reason: the slot is taken at submit and - # released by _fail_job_response, the finalize callback and the - # sweep -- but a job terminalized HERE before dispatch gets no - # finalize callback, and the sweep's phase-1 only targets - # PENDING, never CANCELLED. Without this, deleting in-flight - # jobs leaks a slot each until the 6h TTL, and enough deletes - # exhaust the org's allowance and start rejecting new submits. + # Release the concurrency slot ONLY for a job that was never + # dispatched. + # + # The slot is taken at submit and released by + # `_fail_job_response`, the finalize callback and the sweep. A + # job cancelled BEFORE dispatch gets no finalize callback, and + # the sweep's phase-1 only targets PENDING, never CANCELLED -- + # so without this release its slot would leak until the 6h TTL. + # That is what this release is for, and all it is for. + # + # It used to fire for ANY non-terminal job, including one + # mid-run. Nothing revokes a running executor -- `job.task_id` + # is written at dispatch and never read again -- so the engine + # kept running, kept calling LLMs and kept billing while its + # slot was handed to the next submit. Submit-then-cancel in a + # loop therefore ran arbitrarily many concurrent extractions + # against a ceiling of `AGENT_KV_CONCURRENT_LIMIT`, all paid for. + # + # Narrowing loses nothing: a mid-run cancel's slot is released + # by `FinalizeView`'s `finally` when the executor's callback + # lands, and `release()` is idempotent (zrem). # # Guarded on `won` so a lost race (a concurrent cancel or # finalize terminalized it first) does not release a slot that - # the winner is still accounting for; release() is idempotent - # (zrem), so the winner's own release stays correct. - AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + # the winner is still accounting for. + if _never_dispatched(job): + AgentKVConcurrencyLimiter.release( + str(job.organization_id), str(job.id) + ) # This request owns the terminal notification: no fresh finalize # can follow a cancel that won, so nothing else will send it. dispatch_cancelled_webhook(job) @@ -423,14 +450,29 @@ def post(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): job = _get_job(agent_kv_key, job_id) won = AgentKVJob.mark_terminal(job.id, job.organization_id, JobStatus.CANCELLED) if won: - # The slot is acquired at submit and released by _fail_job_response, - # the finalize callback, and the sweep. A job cancelled BEFORE - # dispatch gets no finalize callback, and the sweep's phase-1 only - # targets PENDING (not CANCELLED) -- so without this release its - # slot would leak until the 6h TTL (pre-Greptile important #4). - # release() is idempotent (zrem), so a later finalize-callback - # release on a cancel-mid-run is harmless. - AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + # Release the concurrency slot ONLY for a job that was never + # dispatched. + # + # The slot is taken at submit and released by + # `_fail_job_response`, the finalize callback and the sweep. A + # job cancelled BEFORE dispatch gets no finalize callback, and + # the sweep's phase-1 only targets PENDING, never CANCELLED -- + # so without this release its slot would leak until the 6h TTL. + # That is what this release is for, and all it is for. + # + # It used to fire for ANY non-terminal job, including one + # mid-run. Nothing revokes a running executor -- `job.task_id` + # is written at dispatch and never read again -- so the engine + # kept running, kept calling LLMs and kept billing while its + # slot was handed to the next submit. Submit-then-cancel in a + # loop therefore ran arbitrarily many concurrent extractions + # against a ceiling of `AGENT_KV_CONCURRENT_LIMIT`, all paid for. + # + # Narrowing loses nothing: a mid-run cancel's slot is released + # by `FinalizeView`'s `finally` when the executor's callback + # lands, and `release()` is idempotent (zrem). + if _never_dispatched(job): + AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) # Docs §8 promises a terminal notification, and cancellation never # reaches finalize -- so this path has to send it. Guarded on `won` # so a cancel that LOST leaves the notification to the finalize that diff --git a/backend/agent_kv/internal_views.py b/backend/agent_kv/internal_views.py index 606b0af63a..40f446e84f 100644 --- a/backend/agent_kv/internal_views.py +++ b/backend/agent_kv/internal_views.py @@ -96,9 +96,31 @@ def post(self, request, *args, job_id=None, **kwargs): ) job = job_qs.first() if job is None: - # Late report for a job that already reached a terminal state - # (completed/failed/cancelled) -- a no-op, not an error. - return Response({"ok": True, "noop": True}) + # Two very different situations used to share this response: a late + # report for an already-terminal job (ordinary), and a report for a + # job_id/org_id pair that matches NO row at all (never ordinary). + # + # The second is what an org-slug-vs-FK-pk mix-up looks like, and + # `dispatch.py` documents that exact confusion shipping once + # already. If it recurs, every stage report is a silent 200 no-op, + # every job stays non-terminal and gets reaped as "Job timed out" -- + # a 100% failure rate presenting as timeouts, with nothing in any + # log. So say which one happened. + exists = AgentKVJob.objects.filter(id=job_id, organization_id=org_id).exists() + if not exists: + logger.warning( + "agent-kv stage report for unknown job %s (org=%s); no row " + "matches that job/org pair -- check the org identifier", + job_id, + org_id, + ) + return Response( + { + "ok": True, + "noop": True, + "reason": "already_terminal" if exists else "unknown_job", + } + ) entry = {"status": status_value} if "seconds" in body: @@ -230,11 +252,24 @@ def post(self, request, *args, job_id=None, **kwargs): finally: AgentKVConcurrencyLimiter.release(org_id, str(job_id)) + if job is None: + logger.warning( + "agent-kv finalize for unknown job %s (org=%s); no row matches " + "that job/org pair, so nothing was terminalized -- the job (if " + "it exists under another org identifier) will be reaped as a " + "timeout", + job_id, + org_id, + ) + return Response( { "finalized": finalized, "webhook_url": job.webhook_url if job else "", "status": job.status.lower() if job else "", + # Lets the callback worker tell "already terminal" (ordinary) + # from "no such job" (a wiring fault) -- they were byte-identical. + "reason": "ok" if job else "unknown_job", } ) diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py index a448030849..42154fcf65 100644 --- a/backend/agent_kv/maintenance.py +++ b/backend/agent_kv/maintenance.py @@ -19,6 +19,7 @@ single-row write, never a queryset-wide one. """ +import logging from datetime import timedelta from django.conf import settings @@ -29,6 +30,8 @@ from agent_kv.rate_limiter import AgentKVConcurrencyLimiter from agent_kv.storage import delete_job_files +logger = logging.getLogger(__name__) + _MAINTENANCE_BATCH_LIMIT = 500 # Slots in each TTL-cleanup batch held for rows whose file delete failed before. # Caps the retry lane (so failures cannot crowd out new expirations) and @@ -152,6 +155,21 @@ def run_sweep() -> dict: timed_out += 1 AgentKVConcurrencyLimiter.release(str(org_id), str(job.id)) + # These two counts are the only evidence the sweep ran and the only + # evidence it had to do anything. A sweep that terminalizes a thousand jobs + # as FAILED used to emit nothing at all -- the module has had no logger + # since it was written -- so a backlog of stranded jobs looked identical to + # a quiet, healthy system. + if swept or timed_out: + logger.warning( + "agent-kv sweep terminalized %s never-dispatched and %s stuck job(s); " + "a non-zero count here means jobs were stranded and their " + "concurrency slots held", + swept, + timed_out, + ) + else: + logger.info("agent-kv sweep: nothing to terminalize") return {"swept": swept, "timed_out": timed_out} @@ -246,4 +264,16 @@ def run_ttl_cleanup() -> dict: # stays high across ticks is the signal that something is wrong with the # object store rather than with one job -- the split stops either side # blocking the other, it does not make a persistent fault harmless. + # `retained` is the count this pass could NOT clean -- a file delete that + # failed keeps its ref so the next pass retries it. A `retained` that never + # falls is a stuck object, and stays invisible without this. + if retained: + logger.warning( + "agent-kv TTL cleanup removed %s job(s) and RETAINED %s whose files " + "could not be deleted; those refs are kept for the next pass", + cleaned, + retained, + ) + elif cleaned: + logger.info("agent-kv TTL cleanup removed %s expired job(s)", cleaned) return {"cleaned": cleaned, "retained": retained} diff --git a/backend/agent_kv/tests/test_internal_views.py b/backend/agent_kv/tests/test_internal_views.py index c0c4fbf9ae..5d91b6bcad 100644 --- a/backend/agent_kv/tests/test_internal_views.py +++ b/backend/agent_kv/tests/test_internal_views.py @@ -365,6 +365,7 @@ def test_finalize_success_writes_result_then_marks_terminal( "finalized": True, "webhook_url": "https://example.com/hook", "status": "completed", + "reason": "ok", } assert [c[0] for c in manager.mock_calls] == ["write_result", "mark_terminal"] m_write.assert_called_once_with("org1", str(job_id), {"foo": "bar"}) @@ -532,6 +533,7 @@ def test_finalize_duplicate_does_not_rewrite_result( "finalized": False, "webhook_url": "https://example.com/hook", "status": "completed", + "reason": "ok", } assert not m_write.called assert not m_mark_terminal.called @@ -584,7 +586,12 @@ def test_finalize_failure_records_error(m_jobs, m_write, m_mark_terminal, m_rele ) assert resp.status_code == 200 - assert resp.data == {"finalized": True, "webhook_url": "", "status": "failed"} + assert resp.data == { + "finalized": True, + "webhook_url": "", + "status": "failed", + "reason": "ok", + } assert not m_write.called m_mark_terminal.assert_called_once_with( job_id, "org1", JobStatus.FAILED, error="LLM provider timed out" @@ -684,3 +691,72 @@ def test_frozen_internal_urls_resolve_to_the_right_views(): finalize = resolve(f"/internal/v1/agent-kv/jobs/{job_id}/finalize/") assert finalize.func.cls is iv.FinalizeView assert finalize.kwargs == {"job_id": job_id} + + +# --------------------------------------------------------------------------- +# An unknown job/org pair must be distinguishable from an ordinary duplicate. +# +# Both used to return a byte-identical 200 no-op with nothing logged. The +# second is what an org-slug-vs-FK-pk mix-up looks like -- and `dispatch.py` +# documents that exact confusion shipping once already. If it recurs, every +# finalize is a silent no-op, every job stays non-terminal and is reaped as +# "Job timed out": a 100% failure rate presenting as timeouts, invisible. +# +# Reported as 2.12 in the branch review. +# --------------------------------------------------------------------------- +@mock.patch.object(iv.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "objects") +def test_finalize_for_an_unknown_job_says_so_and_logs(m_objects, m_release, caplog): + import logging + + m_objects.filter.return_value.first.return_value = None + + with caplog.at_level(logging.WARNING, logger="agent_kv.internal_views"): + resp = iv.FinalizeView.as_view()( + _post("/x", {"org_id": "org1", "success": True}), job_id=uuid.uuid4() + ) + + assert resp.status_code == 200 + assert resp.data["reason"] == "unknown_job" + assert resp.data["finalized"] is False + assert "unknown job" in caplog.text + + +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_for_an_unknown_job_says_so_and_logs(m_objects, caplog): + """`already_terminal` is ordinary and stays quiet; `unknown_job` is not.""" + import logging + + m_objects.filter.return_value.exclude.return_value.first.return_value = None + m_objects.filter.return_value.exists.return_value = False + + with caplog.at_level(logging.WARNING, logger="agent_kv.internal_views"): + resp = iv.StageReportView.as_view()( + _post( + "/x", {"org_id": "org1", "stage": "table_extraction", "status": "running"} + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 200 + assert resp.data["reason"] == "unknown_job" + assert "unknown job" in caplog.text + + +@mock.patch.object(AgentKVJob, "objects") +def test_stage_report_for_an_already_terminal_job_is_quiet(m_objects, caplog): + import logging + + m_objects.filter.return_value.exclude.return_value.first.return_value = None + m_objects.filter.return_value.exists.return_value = True + + with caplog.at_level(logging.WARNING, logger="agent_kv.internal_views"): + resp = iv.StageReportView.as_view()( + _post( + "/x", {"org_id": "org1", "stage": "table_extraction", "status": "running"} + ), + job_id=uuid.uuid4(), + ) + + assert resp.data["reason"] == "already_terminal" + assert caplog.records == [], "a late report for a finished job is ordinary" diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index dc1890111c..b2ff721645 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -390,12 +390,56 @@ def test_delete_on_running_job_cancels_before_deleting_files( @mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) @mock.patch.object(AgentKVJob, "objects") @mock.patch.object(AgentKVKey, "objects") -def test_delete_on_running_job_releases_the_concurrency_slot( +def test_delete_on_a_running_job_does_NOT_release_the_slot( m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release ): + """Inverted: this used to assert the release, which was the bug. + + Nothing revokes a running executor -- `job.task_id` is written at dispatch + and never read again -- so the engine keeps running, keeps calling LLMs and + keeps billing. Handing its slot back immediately let the next submit start + alongside it, so submit-then-delete in a loop ran arbitrarily many + concurrent extractions against a ceiling of AGENT_KV_CONCURRENT_LIMIT, all + paid for. + + The slot is released by `FinalizeView`'s `finally` when the executor's + callback lands. Reported as 2.5 in the branch review. + """ m_keys.get.return_value = kv_key() job = AgentKVJob(status=JobStatus.RUNNING) job.organization_id = "org1" + job.dispatched_at = timezone.now() + m_jobs.get.return_value = job + + resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) + + assert resp.status_code == 204 + assert not m_release.called, ( + "the executor is still running and still billing; its slot belongs to " + "the finalize callback, not to this request" + ) + + +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "save") +@mock.patch.object(ev, "delete_job_files", return_value=["input_ref", "result_ref"]) +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_delete_on_a_never_dispatched_job_DOES_release_the_slot( + m_keys, m_jobs, m_mark_terminal, m_delete_files, m_save, m_release, m_hook +): + """The case the release exists for, and the only one. + + A job cancelled before dispatch gets no finalize callback, and the sweep's + phase-1 only targets PENDING, never CANCELLED -- so without this its slot + leaks until the 6h TTL. + """ + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.PENDING) + job.organization_id = "org1" + job.dispatched_at = None m_jobs.get.return_value = job resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) @@ -677,3 +721,60 @@ def test_delete_that_cancels_a_running_job_also_queues_the_webhook( assert resp.status_code == 204 assert m_hook.called + + +# --------------------------------------------------------------------------- +# (8k) The same narrowing on JobCancelView, which has the identical shape. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_on_a_running_job_does_NOT_release_the_slot( + m_keys, m_jobs, m_mark_terminal, m_release, m_hook +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.RUNNING) + job.organization_id = "org1" + job.dispatched_at = timezone.now() + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert not m_release.called + + +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_cancel_on_a_never_dispatched_job_DOES_release_the_slot( + m_keys, m_jobs, m_mark_terminal, m_release, m_hook +): + m_keys.get.return_value = kv_key() + job = AgentKVJob(status=JobStatus.PENDING) + job.organization_id = "org1" + job.dispatched_at = None + m_jobs.get.return_value = job + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + m_release.assert_called_once_with("org1", str(job.id)) + + +def test_a_dispatched_but_still_pending_job_is_not_treated_as_undispatched(): + """`dispatched_at` and `status` are checked together on purpose. + + Post-enqueue bookkeeping can fail after the task is already on the queue + (dispatch.py wraps it precisely because losing it must not fail the + dispatch), leaving a row that still reads PENDING while an executor runs. + Releasing that slot would over-subscribe exactly as the RUNNING case did. + """ + job = AgentKVJob(status=JobStatus.PENDING) + job.dispatched_at = timezone.now() + + assert not ev._never_dispatched(job) diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index 615434bc79..65aba8f73a 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -80,7 +80,19 @@ def _resolve_error(failed_task_id: str, explicit: str | None = None) -> str: if res.result: return str(res.result) except Exception: - pass + # Was a bare `pass`. The common case here is a kombu deserialization + # failure: the executor raised a cloud-plugin exception class that is + # not importable in the OSS ide_callback image, so the result backend + # has the error and this process cannot read it. The job is then + # finalized with "Executor failed without an error message" -- the exact + # useless error this lookup exists to avoid -- and silently, so the real + # cause was unrecoverable even from logs. + logger.warning( + "Could not read the executor error from the result backend for " + "task %s; the job will be finalized with a generic message", + failed_task_id, + exc_info=True, + ) return _UNKNOWN diff --git a/workers/scheduler/agent_kv_tasks.py b/workers/scheduler/agent_kv_tasks.py index 0f266f7e46..6806299c14 100644 --- a/workers/scheduler/agent_kv_tasks.py +++ b/workers/scheduler/agent_kv_tasks.py @@ -62,10 +62,18 @@ def _get_api_client(): @worker_task(name="agent_kv.sweep") def agent_kv_sweep() -> dict[str, Any]: """Terminalize never-dispatched PENDING and stuck Agent-KV jobs (spec §5.4).""" - return _get_api_client().agent_kv_sweep() + result = _get_api_client().agent_kv_sweep() + # Logged here as well as backend-side: this is the only place that records + # the periodic actually FIRED. The counts alone cannot distinguish "ran and + # found nothing" from "never ran", which is precisely the failure this + # task pair shipped with (see the backend migration that schedules them). + logger.info("agent_kv.sweep completed: %s", result) + return result @worker_task(name="agent_kv.ttl_cleanup") def agent_kv_ttl_cleanup() -> dict[str, Any]: """Delete staged Agent-KV input/result files past their TTL (spec §5.4).""" - return _get_api_client().agent_kv_ttl_cleanup() + result = _get_api_client().agent_kv_ttl_cleanup() + logger.info("agent_kv.ttl_cleanup completed: %s", result) + return result diff --git a/workers/tests/test_agent_kv_scheduler_tasks.py b/workers/tests/test_agent_kv_scheduler_tasks.py index 7718a63f77..1cd3f939c7 100644 --- a/workers/tests/test_agent_kv_scheduler_tasks.py +++ b/workers/tests/test_agent_kv_scheduler_tasks.py @@ -63,11 +63,14 @@ def test_sweep_calls_the_sweep_client_method(self): def test_ttl_cleanup_calls_the_ttl_cleanup_client_method(self): mock_api = MagicMock() - mock_api.agent_kv_ttl_cleanup.return_value = {"cleaned": 2} + mock_api.agent_kv_ttl_cleanup.return_value = {"cleaned": 2, "retained": 1} with patch.object(akt, "_get_api_client", return_value=mock_api): result = akt.agent_kv_ttl_cleanup() mock_api.agent_kv_ttl_cleanup.assert_called_once_with() - assert result == {"cleaned": 2} + # `retained` is part of the contract: it counts jobs whose files could not + # be deleted and whose refs are kept for the next pass. Pinned so it cannot + # be dropped silently. + assert result == {"cleaned": 2, "retained": 1} class TestInternalClientAgentKvMethods: @@ -87,9 +90,9 @@ def test_agent_kv_sweep_posts_to_the_sweep_endpoint(self): def test_agent_kv_ttl_cleanup_posts_to_the_ttl_cleanup_endpoint(self): with patch.object( - InternalAPIClient, "post", return_value={"cleaned": 1} + InternalAPIClient, "post", return_value={"cleaned": 1, "retained": 0} ) as m_post: client = InternalAPIClient.__new__(InternalAPIClient) result = client.agent_kv_ttl_cleanup() m_post.assert_called_once_with("v1/agent-kv/ttl-cleanup/", data={}) - assert result == {"cleaned": 1} + assert result == {"cleaned": 1, "retained": 0} From 835a46d7b35c00a6820324b6cb15d294d82329ed Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 15:22:31 +0530 Subject: [PATCH 09/18] =?UTF-8?q?UN-4232=20[FIX]=202.6,=202.7,=202.8,=202.?= =?UTF-8?q?13,=202.14,=202.15=20=E2=80=94=20correctness=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.14 — THE SWEEP'S NULL BACKSTOP COULD NEVER FIRE UNDER BACKLOG Phase 2's second Q arm exists to recover rows whose post-enqueue bookkeeping was lost -- they have `dispatched_at IS NULL`. But the batch was `.order_by("dispatched_at")[:500]`, and Postgres sorts ascending NULLS LAST, so with 500 non-NULL stuck rows ahead of them those rows were never selected. The backstop could not fire in exactly the situation it exists for. Now orders by `Coalesce("dispatched_at", "created_at")`. The test asserted `order_by("dispatched_at")` -- it pinned the bug -- so it is inverted with the reasoning recorded rather than deleted. 2.15 — TTL CLEANUP DELETED THE INPUT OF A RUNNING JOB The candidate query filtered on `expires_at` and a non-blank ref, with no status filter, so a job still RUNNING past its TTL had its staged input deleted out from under the executor. The TTL is a retention policy for finished work, not a kill switch for running work. Reachable whenever a job is stuck non-terminal longer than AGENT_KV_RESULT_TTL_DAYS -- which is what the sweep's phase 2 exists to catch, and which until this branch was never scheduled at all. TERMINAL-only now. The sweep terminalizes stuck jobs first; then this cleans them. The mocks follow the new three-filter chain and the status predicate is asserted, so it cannot be dropped silently. 2.13 — UNTRUSTED COUNTERS COULD REWRITE THE STATUS DOCUMENT `_status_document` builds each stage entry as `{"name": name, **entry}` with the spread LAST, so a persisted `name` counter overrode it -- renaming the stage in every later status response and breaking the `if name in stages_json` filter that decides which stages are shown. One counter could make a job's progress disappear. `name` is now reserved alongside `status`/`seconds`. Two more on the same endpoint: `seconds` was taken verbatim, so a dict or list was persisted and echoed back to the caller (now type-checked, with `bool` excluded since it subclasses `int`); and `stage` is varchar(32), so a longer name was an unhandled 500 and `job.stages` could grow unbounded distinct keys. 2.7 — THE `extractors` FILE PART WAS MATERIALISED BEFORE ANY SIZE CHECK The cap lives in `validate_extractors`, i.e. after the read, and `DATA_UPLOAD_MAX_MEMORY_SIZE` excludes file-typed parts -- so a 500 MB part was read in full (plus up to 4x again for the `str`) before being rejected at 256 KiB. One request per worker process OOMs the pod: a cheap denial of service. Now reads `limit + 1` bytes. Also `errors="strict"`: a latin-1 key name used to decode to U+FFFD and then compile cleanly, so a malformed payload became a job running against a schema the caller never wrote. 2.8 — A DB FAILURE AFTER STAGING ORPHANED THE UPLOAD PERMANENTLY If `stage_input` succeeded and `job.save()` raised, the upload existed with no row to carry its ref -- and `run_ttl_cleanup` selects candidates from AgentKVJob rows, so an object with no row is structurally unreachable by every cleanup path there is. Customer data in the bucket that nobody can find or delete. Now deleted on that path, with its own failure logged rather than swallowed. 2.6 — AN E2E ASSERTION AND THE DOCS BOTH ASSERTED THE WRONG CASE Both claimed the cancel 409 body carries the RAW uppercase enum, and the docs pre-empted correction with "not a typo in this document", citing a unit test as proof. `JobCancelView` returns `job.status.lower()`, and the cited test asserts `{"status": "completed"}`. The e2e lane had never run, so the wrong assertion was never executed. Both corrected, and the docs now record that they were wrong rather than quietly flipping. 228 backend tests (from 221). 2.8, 2.14 and 2.15 mutation-checked. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 43 ++++++- backend/agent_kv/internal_views.py | 22 +++- backend/agent_kv/maintenance.py | 22 +++- backend/agent_kv/tests/test_internal_views.py | 74 ++++++++++++ backend/agent_kv/tests/test_submit_view.py | 107 ++++++++++++++++++ backend/agent_kv/tests/test_sweeps.py | 26 ++++- docs/agent-kv-api.md | 15 ++- tests/e2e/agent_kv/test_agent_kv_e2e.py | 11 +- 8 files changed, 301 insertions(+), 19 deletions(-) diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index d3a4821412..d9fe362fc0 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -5,6 +5,7 @@ from django.conf import settings from django.utils import timezone from plugins import get_plugin +from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.views import APIView @@ -230,7 +231,31 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): data = request.data.copy() part = data.get("extractors") if hasattr(part, "read"): # `extractors` uploaded as a file part (§7.1) - data["extractors"] = part.read().decode("utf-8", errors="replace") + # Bounded read. The size cap lives in `validate_extractors`, i.e. + # AFTER this point -- and `DATA_UPLOAD_MAX_MEMORY_SIZE` excludes + # file-typed parts, so a 500 MB `extractors` part was materialised + # in full (plus up to 4x that again for the `str`) before being + # rejected at 256 KiB. One such request per worker process OOMs the + # pod, which makes it a cheap denial of service. + # + # Read one byte past the cap: enough to know it is over without + # ever holding more than the cap plus one. + limit = settings.AGENT_KV_MAX_SCHEMA_BYTES + raw = part.read(limit + 1) + if len(raw) > limit: + raise ValidationError( + {"extractors": f"extractors payload exceeds {limit} bytes"} + ) + try: + # `errors="strict"`, not "replace". A latin-1 key name used to + # decode to U+FFFD and then compile cleanly, so a malformed + # payload became a job that ran against a schema the caller + # never wrote. + data["extractors"] = raw.decode("utf-8") + except UnicodeDecodeError: + raise ValidationError( + {"extractors": "extractors must be valid UTF-8"} + ) from None serializer = SubmitSerializer(data=data) serializer.is_valid(raise_exception=True) v = serializer.validated_data @@ -256,6 +281,22 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): job_saved = True except Exception: logger.exception("agent-kv staging/save failed for job %s", job.id) + # Delete the staged object before responding. If `stage_input` + # succeeded and `job.save()` then raised, the upload exists with no + # row to carry its ref -- and `run_ttl_cleanup` selects candidates + # from `AgentKVJob` rows, so an object with no row is structurally + # unreachable by every cleanup path there is. It would sit in the + # bucket forever, holding customer data nobody can find or delete. + if job.input_ref: + try: + delete_job_files(job) + except Exception: + logger.exception( + "agent-kv: could not remove the staged input for failed " + "submit %s; it is now orphaned (no job row carries its " + "ref, so TTL cleanup cannot reach it)", + job.id, + ) return _fail_job_response( job, org_id, diff --git a/backend/agent_kv/internal_views.py b/backend/agent_kv/internal_views.py index 40f446e84f..6d8206b4ad 100644 --- a/backend/agent_kv/internal_views.py +++ b/backend/agent_kv/internal_views.py @@ -23,7 +23,13 @@ logger = logging.getLogger(__name__) _VALID_STAGE_STATUSES = frozenset({"running", "done"}) -_RESERVED_STAGE_ENTRY_KEYS = frozenset({"status", "seconds"}) +# `name` is reserved too: `_status_document` builds each stage entry as +# `{"name": name, **stages_json[name]}`, and the spread comes LAST -- so a +# persisted `name` counter overrides it. That renames the stage in every later +# status response and breaks the `if name in stages_json` filter the document +# uses to decide which stages to show, i.e. one counter can make a job's +# progress disappear. +_RESERVED_STAGE_ENTRY_KEYS = frozenset({"status", "seconds", "name"}) _SCALAR_COUNTER_TYPES = (str, int, float, bool) @@ -122,9 +128,21 @@ def post(self, request, *args, job_id=None, **kwargs): } ) + # `job.stage` is varchar(32). A longer name was an unhandled 500, and + # `job.stages` could grow unbounded distinct keys from a misbehaving + # or malicious executor. + if len(stage) > 32: + return Response({"detail": "stage name too long"}, status=400) + entry = {"status": status_value} if "seconds" in body: - entry["seconds"] = body["seconds"] + # Type-checked rather than taken verbatim: this is untrusted + # executor payload and it is echoed back in the status document, + # so a dict or list here was persisted and returned to the caller. + seconds = body["seconds"] + if not isinstance(seconds, (int, float)) or isinstance(seconds, bool): + return Response({"detail": "seconds must be a number"}, status=400) + entry["seconds"] = seconds entry.update(_sanitize_counters(body.get("counters"))) updates = { diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py index 42154fcf65..6ea8845a6a 100644 --- a/backend/agent_kv/maintenance.py +++ b/backend/agent_kv/maintenance.py @@ -24,6 +24,7 @@ from django.conf import settings from django.db.models import Q +from django.db.models.functions import Coalesce from django.utils import timezone from agent_kv.models import AgentKVJob, JobStatus @@ -139,7 +140,13 @@ def run_sweep() -> dict: Q(dispatched_at__lt=stuck_cutoff) | Q(dispatched_at__isnull=True, created_at__lt=stuck_cutoff) ) - .order_by("dispatched_at")[:_MAINTENANCE_BATCH_LIMIT] + # Coalesce, not a bare `dispatched_at`. The second Q arm exists to + # recover rows whose post-enqueue bookkeeping was lost -- they have + # `dispatched_at IS NULL` -- but Postgres sorts ascending NULLS LAST, + # so with a full batch of non-NULL stuck rows ahead of them those rows + # were never selected. The backstop could not fire in exactly the + # situation it exists for: a backlog. + .order_by(Coalesce("dispatched_at", "created_at"))[:_MAINTENANCE_BATCH_LIMIT] ) timed_out = 0 @@ -210,8 +217,17 @@ def run_ttl_cleanup() -> dict: row before applying the limit, which grows with the backlog. """ now = timezone.now() - expired = AgentKVJob.objects.filter(expires_at__lt=now).filter( - Q(input_ref__gt="") | Q(result_ref__gt="") + # TERMINAL only. Without this, a job still RUNNING past `expires_at` had + # its staged input deleted out from under the executor -- the TTL is a + # retention policy for finished work, not a kill switch for running work. + # Reachable whenever a job is stuck non-terminal for longer than + # AGENT_KV_RESULT_TTL_DAYS, which is precisely what the sweep's phase 2 + # exists to catch; the sweep terminalizes those first, and then this + # cleans them. + expired = ( + AgentKVJob.objects.filter(expires_at__lt=now) + .filter(status__in=list(AgentKVJob.TERMINAL)) + .filter(Q(input_ref__gt="") | Q(result_ref__gt="")) ) # Retries first, capped at the reserve so they cannot crowd out fresh work. diff --git a/backend/agent_kv/tests/test_internal_views.py b/backend/agent_kv/tests/test_internal_views.py index 5d91b6bcad..20cba4c677 100644 --- a/backend/agent_kv/tests/test_internal_views.py +++ b/backend/agent_kv/tests/test_internal_views.py @@ -760,3 +760,77 @@ def test_stage_report_for_an_already_terminal_job_is_quiet(m_objects, caplog): assert resp.data["reason"] == "already_terminal" assert caplog.records == [], "a late report for a finished job is ordinary" + + +# --------------------------------------------------------------------------- +# Untrusted executor payload must not be able to rewrite the status document. +# Reported as 2.13 in the branch review. +# --------------------------------------------------------------------------- +def test_a_name_counter_cannot_rename_the_stage(): + """`_status_document` builds `{"name": name, **entry}` with the spread + LAST, so a persisted `name` wins -- renaming the stage in every later status + response and breaking the `if name in stages_json` filter that decides + which stages are shown at all. + """ + assert "name" in iv._RESERVED_STAGE_ENTRY_KEYS + assert iv._sanitize_counters({"name": "pwned", "steps": 3}) == {"steps": 3} + + +@mock.patch.object(AgentKVJob, "objects") +def test_a_non_numeric_seconds_is_rejected(m_objects): + """`seconds` is echoed back in the status document, so a dict or list here + was persisted and returned to the caller verbatim. + """ + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "table_extraction", + "status": "running", + "seconds": {"nested": "payload"}, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 400 + assert "seconds" in str(resp.data) + + +@mock.patch.object(AgentKVJob, "objects") +def test_a_boolean_seconds_is_rejected(m_objects): + """`bool` is a subclass of `int`, so a bare isinstance check would let + `True` through and persist it as a duration. + """ + resp = iv.StageReportView.as_view()( + _post( + "/x", + { + "org_id": "org1", + "stage": "table_extraction", + "status": "running", + "seconds": True, + }, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 400 + + +@mock.patch.object(AgentKVJob, "objects") +def test_an_overlong_stage_name_is_rejected(m_objects): + """`job.stage` is varchar(32); a longer name was an unhandled 500, and + `job.stages` could grow unbounded distinct keys. + """ + resp = iv.StageReportView.as_view()( + _post( + "/x", + {"org_id": "org1", "stage": "x" * 33, "status": "running"}, + ), + job_id=uuid.uuid4(), + ) + + assert resp.status_code == 400 + assert "too long" in str(resp.data) diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index 565662fce2..ba105f32dd 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -756,3 +756,110 @@ def test_real_serializer_rejects_the_old_flat_shape_with_400(m_keys, m_plugin, m assert resp.status_code == 400 assert "extractors" in str(resp.data) + + +# --------------------------------------------------------------------------- +# (9) The `extractors` file part is read under a bound, and decoded strictly. +# +# The size cap lives in `validate_extractors`, i.e. AFTER the part was +# materialised, and `DATA_UPLOAD_MAX_MEMORY_SIZE` excludes file-typed parts -- +# so a 500 MB part was read in full before being rejected at 256 KiB. One such +# request per worker process OOMs the pod: a cheap denial of service. +# +# Reported as 2.7 in the branch review. +# --------------------------------------------------------------------------- +from django.core.files.uploadedfile import SimpleUploadedFile # noqa: E402 + + +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_an_oversized_extractors_part_is_rejected_by_size(m_keys, m_plugin, m_rate): + """The cap is now consulted BEFORE the part is materialised. + + Note what this does and does not prove: it pins the 400, which is the + behaviour a caller sees. It cannot observe the read size, because the + multipart parser builds its own file object from the wire and the instance + constructed here never reaches the view. The bounded read itself is visible + in `SubmitView.post` as `part.read(limit + 1)`; a test that claimed to + verify it through this path would be asserting on an object the view never + touched. + """ + m_keys.get.return_value = _key_with_org() + cap = 64 + part = SimpleUploadedFile("extractors.json", b"x" * (cap * 50)) + + req = APIRequestFactory().post("/agent-kv/", {"extractors": part}, format="multipart") + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + with mock.patch.object(ev.settings, "AGENT_KV_MAX_SCHEMA_BYTES", cap): + resp = ev.SubmitView.as_view()(req) + + assert resp.status_code == 400 + assert "exceeds" in str(resp.data) + + +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_a_non_utf8_extractors_part_is_rejected(m_keys, m_plugin, m_rate): + """`errors="replace"` let a latin-1 key name decode to U+FFFD and then + compile cleanly -- a malformed payload became a job that ran against a + schema the caller never wrote. + """ + m_keys.get.return_value = _key_with_org() + + class _Latin1Part: + def read(self, n=-1): + return b'[{"name": "table", "keys": {"target_table": "\xe9"}}]' + + req = APIRequestFactory().post( + "/agent-kv/", {"extractors": _Latin1Part()}, format="multipart" + ) + req.META["HTTP_AUTHORIZATION"] = "Bearer 123e4567-e89b-12d3-a456-426614174001" + resp = ev.SubmitView.as_view()(req) + + assert resp.status_code == 400 + assert "UTF-8" in str(resp.data) + + +# --------------------------------------------------------------------------- +# (10) A DB failure after staging must not orphan the upload. +# +# `run_ttl_cleanup` selects candidates from AgentKVJob rows, so an object whose +# row was never saved is unreachable by every cleanup path there is -- customer +# data sitting in the bucket that nobody can find or delete. +# +# Reported as 2.8 in the branch review. +# --------------------------------------------------------------------------- +@mock.patch.object(ev, "delete_job_files") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "check_and_acquire", return_value=True) +@mock.patch.object(AgentKVJob, "mark_terminal") +@mock.patch.object(AgentKVJob, "save", side_effect=RuntimeError("db down")) +@mock.patch.object(ev, "stage_input", return_value="org1/job/in.pdf") +@mock.patch.object(ev, "SubmitSerializer") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_a_save_failure_after_staging_removes_the_staged_object( + m_keys, + m_plugin, + m_rate, + m_serializer_cls, + m_stage, + m_save, + m_mark_terminal, + m_acquire, + m_release, + m_delete, +): + m_keys.get.return_value = _key_with_org() + _mock_serializer(m_serializer_cls) + + resp = ev.SubmitView.as_view()(_authed_post()) + + assert resp.status_code == 500 + assert m_delete.called, ( + "the staged upload was left in the bucket with no job row carrying its " + "ref; TTL cleanup selects from job rows, so it can never be reached" + ) diff --git a/backend/agent_kv/tests/test_sweeps.py b/backend/agent_kv/tests/test_sweeps.py index be1cf6b4f0..13cb1e28f7 100644 --- a/backend/agent_kv/tests/test_sweeps.py +++ b/backend/agent_kv/tests/test_sweeps.py @@ -37,6 +37,7 @@ from django.conf import settings # noqa: E402 from django.db.models import Q # noqa: E402 +from django.db.models.functions import Coalesce # noqa: E402 from django.utils import timezone # noqa: E402 from rest_framework.test import APIRequestFactory # noqa: E402 @@ -224,7 +225,13 @@ def test_stuck_sweep_queries_dispatched_and_running_older_than_stuck_grace( assert age_q == ( Q(dispatched_at__lt=cutoff) | Q(dispatched_at__isnull=True, created_at__lt=cutoff) ) - phase2_qs.order_by.assert_called_once_with("dispatched_at") + # Coalesce, NOT a bare "dispatched_at". This assertion used to pin the + # bare column, which is the bug: Postgres sorts ascending NULLS LAST, so + # with a full batch of non-NULL stuck rows ahead of them, the + # `dispatched_at IS NULL` rows the second Q arm exists to recover were + # never selected. The backstop could not fire in exactly the situation it + # exists for -- a backlog. (2.14 in the branch review.) + phase2_qs.order_by.assert_called_once_with(Coalesce("dispatched_at", "created_at")) phase2_qs.order_by.return_value.__getitem__.assert_called_once_with( slice(None, 500, None) ) @@ -360,9 +367,14 @@ def filter(self, *_args, **kwargs): def _ttl_lanes(m_objects, *, retries=(), fresh=()): - """Wire a mocked ``AgentKVJob.objects`` to both TTL-cleanup lanes.""" + """Wire a mocked ``AgentKVJob.objects`` to both TTL-cleanup lanes. + + Three chained filters now: `expires_at < now`, then TERMINAL-only (2.15 -- + a running job must not have its input deleted out from under it), then the + non-blank-ref Q. + """ lanes = _Lanes(retries, fresh) - m_objects.filter.return_value.filter.return_value = lanes + m_objects.filter.return_value.filter.return_value.filter.return_value = lanes return lanes @@ -383,7 +395,13 @@ def test_ttl_cleanup_queries_expired_jobs_with_a_nonblank_ref(m_objects, m_delet assert resp.status_code == 200 assert resp.data == {"cleaned": 0, "retained": 0} assert m_objects.filter.call_args_list[0].kwargs == {"expires_at__lt": frozen_now} - (q_arg,), q_kwargs = m_objects.filter.return_value.filter.call_args + # TERMINAL-only: a job still RUNNING past its TTL must keep its staged + # input. The retention policy covers finished work, not running work. + status_filter = m_objects.filter.return_value.filter.call_args + assert set(status_filter.kwargs["status__in"]) == set(AgentKVJob.TERMINAL) + (q_arg,), q_kwargs = ( + m_objects.filter.return_value.filter.return_value.filter.call_args + ) assert q_kwargs == {} assert q_arg == (Q(input_ref__gt="") | Q(result_ref__gt="")) # Two lanes off that one narrowed set: retries first, then new expirations. diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md index 493e13083e..4d6ebfd424 100644 --- a/docs/agent-kv-api.md +++ b/docs/agent-kv-api.md @@ -540,12 +540,15 @@ metered; work not yet started is not. A job that finishes before the cancel is n simply has its result discarded on arrival. - Won the race (job was non-terminal): `200` `{"status": "cancelled"}`. -- Already terminal: `409` `{"status": ""}` — **note this one is the raw - uppercase enum value** (e.g. `"COMPLETED"`), unlike every lowercased `status` value - elsewhere in this API (status doc, result 409). This is the code as shipped - (`execution_views.py::JobCancelView`, confirmed by - `test_job_views.py::test_cancel_on_completed_is_409_and_result_untouched`), not a - typo in this document. +- Already terminal: `409` `{"status": ""}`, lowercased (e.g. + `"completed"`) — consistent with every other `status` value in this API. + + *An earlier version of this document claimed the opposite: that this one field + carried the raw uppercase enum, and that it was "not a typo". It was wrong. + `JobCancelView` returns `job.status.lower()`, and the unit test cited as proof + (`test_job_views.py::test_cancel_on_completed_is_409_and_result_untouched`) + asserts `{"status": "completed"}`. The e2e lane carried the same wrong + assertion and had never been run.* ```bash curl -X POST https://api.unstract.example/agent-kv/5b6e9b0a-.../cancel \ diff --git a/tests/e2e/agent_kv/test_agent_kv_e2e.py b/tests/e2e/agent_kv/test_agent_kv_e2e.py index edfff5dbf0..6bff877d2e 100644 --- a/tests/e2e/agent_kv/test_agent_kv_e2e.py +++ b/tests/e2e/agent_kv/test_agent_kv_e2e.py @@ -342,9 +342,14 @@ def test_cancel_mid_run(agent_kv_key: AgentKVAuth) -> None: assert cancel_body == {"status": "cancelled"}, cancel_body assert final_status == "cancelled", status_resp.json() else: - # docs §7: the 409 body's status is the RAW uppercase enum value, - # unlike every other status field in this API. - assert cancel_body["status"] == final_status.upper(), (cancel_body, final_status) + # Lowercased, like every other status field in this API. The docs and + # this assertion both used to claim the 409 body carried the RAW + # uppercase enum -- it does not: `JobCancelView` returns + # `job.status.lower()`, and the unit test the docs cited as proof + # (`test_cancel_on_completed_is_409_and_result_untouched`) asserts + # `{"status": "completed"}`. This lane had never run, so the wrong + # assertion was never executed. (2.6 in the branch review.) + assert cancel_body["status"] == final_status, (cancel_body, final_status) def test_cancelled_job_does_not_leak_its_concurrency_slot( From a1c6c9dc4daba5bb7314783e1ea3bb405ab6afb0 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 15:35:28 +0530 Subject: [PATCH 10/18] UN-4232 [FIX] Six Greptile findings on this branch's own new code All six land on code added earlier in this branch, and all six are real. Two of them are consequences of narrowing the slot release, which is worth stating plainly: fixing the over-subscription opened the opposite hole at both ends. 1. CANCELLED BEFORE ENQUEUE -> PAID WORK WITH NO SLOT HELD (P1) A cancel can land between the submit's `job.save()` and `dispatch_job`'s enqueue. The cancel sees a PENDING, never-dispatched row, so it terminalizes it AND releases the slot -- correctly, nothing had been dispatched. But dispatch then enqueued anyway: paid work for a job the caller already cancelled, with its slot already handed to the next submit, so the concurrency ceiling is bypassed too. `dispatch_job` now re-reads the row immediately before enqueueing. Re-read, not `job.status`: the in-memory row predates the cancel by construction. 2. CANCELLED AFTER DISPATCH -> SLOT HELD FOR SIX HOURS (P1) The other end of the same narrowing. Cancelling a dispatched job deliberately leaves the slot to the finalize callback, because the executor is still running and still billing. If that executor dies, no callback arrives -- and neither sweep phase selects a CANCELLED row (phase 1 wants PENDING, phase 2 wants DISPATCHED/RUNNING), so the slot sat occupied until Redis expired it. New sweep phase 3, bounded on BOTH sides: older than the stuck grace, so a live executor is not cut short, and newer than the slot TTL, past which Redis has already dropped the entry and there is nothing left to release. Without that second bound it would rescan every cancelled job ever, forever. 3. A FAILED CANCELLATION WEBHOOK WAS ACKNOWLEDGED (P1) `agent_kv_cancelled` ignored `send_webhook`'s return, so a non-2xx or a connection failure was reported as success, the queue message acknowledged, and the caller simply never received the terminal notification the task exists to deliver. `_send_webhook` now returns the result and the task raises on a failed delivery, under the same retry budget `agent_kv_error` got. 4. THE ORPHAN CLEANUP DID NOT CHECK WHETHER IT WORKED (P1) The 2.8 fix called `delete_job_files` and ignored the result. That helper reports which refs are confirmed gone and swallows the rest -- normally the ref survives as the retry handle, but here there is no row to hold it. A failed delete therefore still orphaned the object, silently. Now checks, and logs the ref itself at ERROR: it is the only way anyone can clean it up by hand. 5. A NUMERIC `stage` RETURNED 500 (P2) A truthy non-string passed the presence check and then raised TypeError at `len(stage)` -- my own bounds check from 2.13, turning a malformed report into a server error. Now a 400. 6. CANCELLATION WEBHOOK CAN REPEAT ON REDELIVERY (P2) Acknowledged, not fixed here: it is inherent to at-least-once delivery and the finalize webhook has the same property. Deduplicating needs delivery state on the job row, which is the design question raised under 2.10 and not something to decide inside this commit. Test-mock changes worth flagging: `test_dispatch.py`'s positional `call_args_list[N]` assertions broke on every added query, so they are now content-based (`_filtered_with`). The two bookkeeping-failure tests had a blanket `filter.side_effect` that now hits the new terminal check -- a different, earlier failure -- so the failure is scoped to the bookkeeping call and their original meaning is preserved. 232 backend tests (from 228), ide_callback 20. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/dispatch.py | 18 ++++ backend/agent_kv/execution_views.py | 24 ++++- backend/agent_kv/internal_views.py | 4 + backend/agent_kv/maintenance.py | 37 +++++++- backend/agent_kv/rate_limiter.py | 6 +- backend/agent_kv/tests/test_dispatch.py | 114 +++++++++++++++++++++--- backend/agent_kv/tests/test_sweeps.py | 67 +++++++++++++- workers/ide_callback/agent_kv_tasks.py | 30 ++++++- 8 files changed, 274 insertions(+), 26 deletions(-) diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py index d2d8348686..de29cf50f6 100644 --- a/backend/agent_kv/dispatch.py +++ b/backend/agent_kv/dispatch.py @@ -125,6 +125,24 @@ def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None: "pages_total": job.pages_total, }, ) + # Last check before spending money. A cancel can land between the + # submit's `job.save()` and this enqueue: the cancel sees a PENDING, + # never-dispatched row, so it terminalizes it AND releases its + # concurrency slot -- correctly, because nothing had been dispatched + # yet. Enqueueing anyway would then run paid work for a job the caller + # already cancelled, with its slot already handed to someone else. + # + # Re-read rather than trusting the in-memory row, which predates the + # cancel by construction. + if AgentKVJob.objects.filter( + id=job.id, status__in=list(AgentKVJob.TERMINAL) + ).exists(): + logger.info( + "agent-kv: job %s was terminalized before dispatch; not " "enqueueing", + job.id, + ) + return + cb_kwargs = {"callback_kwargs": {"job_id": str(job.id), "org_id": org_id}} _dispatcher().dispatch_with_callback( context, diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index d9fe362fc0..eb3bcb9645 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -288,14 +288,30 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): # unreachable by every cleanup path there is. It would sit in the # bucket forever, holding customer data nobody can find or delete. if job.input_ref: + # `delete_job_files` reports which refs are confirmed gone and + # swallows the rest -- normally the ref survives as the retry + # handle. Here there is no row to hold it, so a delete that + # failed means the object is orphaned with nothing anywhere + # pointing at it. Log the ref itself: it is the only way anyone + # can clean it up by hand. try: - delete_job_files(job) + cleared = delete_job_files(job) except Exception: + cleared = [] logger.exception( - "agent-kv: could not remove the staged input for failed " - "submit %s; it is now orphaned (no job row carries its " - "ref, so TTL cleanup cannot reach it)", + "agent-kv: staged-input cleanup raised for failed " + "submit %s (ref=%s)", job.id, + job.input_ref, + ) + if "input_ref" not in cleared: + logger.error( + "agent-kv: the staged input for failed submit %s could " + "NOT be removed and no job row exists to carry its ref " + "-- object %s is orphaned and unreachable by TTL " + "cleanup; remove it manually", + job.id, + job.input_ref, ) return _fail_job_response( job, diff --git a/backend/agent_kv/internal_views.py b/backend/agent_kv/internal_views.py index 6d8206b4ad..de4583f5ff 100644 --- a/backend/agent_kv/internal_views.py +++ b/backend/agent_kv/internal_views.py @@ -131,6 +131,10 @@ def post(self, request, *args, job_id=None, **kwargs): # `job.stage` is varchar(32). A longer name was an unhandled 500, and # `job.stages` could grow unbounded distinct keys from a misbehaving # or malicious executor. + if not isinstance(stage, str): + # A truthy non-string (e.g. `1`) passed the presence check above and + # then raised TypeError at `len()` -- a 500 for a malformed report. + return Response({"detail": "stage must be a string"}, status=400) if len(stage) > 32: return Response({"detail": "stage name too long"}, status=400) diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py index 6ea8845a6a..c391761ffe 100644 --- a/backend/agent_kv/maintenance.py +++ b/backend/agent_kv/maintenance.py @@ -28,7 +28,7 @@ from django.utils import timezone from agent_kv.models import AgentKVJob, JobStatus -from agent_kv.rate_limiter import AgentKVConcurrencyLimiter +from agent_kv.rate_limiter import SLOT_TTL_SECONDS, AgentKVConcurrencyLimiter from agent_kv.storage import delete_job_files logger = logging.getLogger(__name__) @@ -162,6 +162,41 @@ def run_sweep() -> dict: timed_out += 1 AgentKVConcurrencyLimiter.release(str(org_id), str(job.id)) + # Phase 3: release slots still held by jobs that were CANCELLED after being + # dispatched. + # + # Cancelling a dispatched job deliberately does NOT release its slot -- the + # executor is still running and still billing, so the slot belongs to the + # finalize callback that will arrive when it finishes. But if that executor + # dies, no callback ever arrives, and neither phase above selects a + # CANCELLED row: phase 1 wants PENDING, phase 2 wants DISPATCHED/RUNNING. + # The slot then sits occupied until Redis expires it. + # + # Bounded on BOTH sides, which is what keeps this from rescanning the same + # ancient rows forever: older than the stuck grace (so a live executor is + # not cut short) and newer than the slot TTL (past which Redis has already + # dropped the entry, so there is nothing left to release). + slot_ttl_floor = now - timedelta(seconds=SLOT_TTL_SECONDS) + abandoned_cancelled = AgentKVJob.objects.filter( + status=JobStatus.CANCELLED, + dispatched_at__isnull=False, + completed_at__lt=stuck_cutoff, + completed_at__gt=slot_ttl_floor, + ).order_by("completed_at")[:_MAINTENANCE_BATCH_LIMIT] + + released = 0 + for job in abandoned_cancelled: + # `release` is an idempotent zrem, so re-releasing a slot the callback + # already freed costs nothing and is not worth a guard. + AgentKVConcurrencyLimiter.release(str(job.organization_id), str(job.id)) + released += 1 + if released: + logger.info( + "agent-kv sweep released %s concurrency slot(s) held by cancelled " + "jobs whose executor never called back", + released, + ) + # These two counts are the only evidence the sweep ran and the only # evidence it had to do anything. A sweep that terminalizes a thousand jobs # as FAILED used to emit nothing at all -- the module has had no logger diff --git a/backend/agent_kv/rate_limiter.py b/backend/agent_kv/rate_limiter.py index 85796ef5d2..0102e2c1f7 100644 --- a/backend/agent_kv/rate_limiter.py +++ b/backend/agent_kv/rate_limiter.py @@ -26,7 +26,11 @@ def _limiter_failure_allows_request() -> bool: return bool(getattr(settings, "AGENT_KV_LIMITER_FAIL_OPEN", False)) -_SLOT_TTL_SECONDS = 6 * 3600 +#: How long a held slot survives in Redis without being released. Public +#: because the sweep needs it: past this age there is nothing left to release, +#: which is what bounds its cancelled-job scan. +SLOT_TTL_SECONDS = 6 * 3600 +_SLOT_TTL_SECONDS = SLOT_TTL_SECONDS def _redis(): diff --git a/backend/agent_kv/tests/test_dispatch.py b/backend/agent_kv/tests/test_dispatch.py index bfcbeea953..c63854ae6c 100644 --- a/backend/agent_kv/tests/test_dispatch.py +++ b/backend/agent_kv/tests/test_dispatch.py @@ -17,6 +17,43 @@ from agent_kv.models import AgentKVJob, JobStatus # noqa: E402 +def _fail_only_bookkeeping(m_objects, exc): + """Let the pre-dispatch terminal check succeed, then fail the next query. + + These tests are about a DB error during POST-ENQUEUE bookkeeping, which + must not be reported as a failed dispatch. A blanket `filter.side_effect` + now hits the pre-dispatch terminal check instead -- a different, earlier + failure that correctly DOES abort the dispatch, since a job that cannot be + verified as live must not have money spent on it. + """ + live = mock.MagicMock() + live.exists.return_value = False + m_objects.filter.side_effect = [live, exc] + + +def _filtered_with(m_objects, **expected): + """True if `objects.filter` was ever called with exactly these kwargs. + + Content-based rather than positional: `dispatch_job` makes several filter + calls (the pre-dispatch terminal check, the guarded bookkeeping UPDATE, and + on the slow path a narrowed fallback), and index-pinned assertions broke + every time one was added. + """ + return any(call.kwargs == expected for call in m_objects.filter.call_args_list) + + +def _not_cancelled(m_objects): + """Make the pre-dispatch terminal check report "still live". + + `dispatch_job` re-reads the row immediately before enqueueing, so a cancel + that landed between the submit's save() and the enqueue cannot result in + paid work for a job whose slot was already released. With `objects` mocked + the default `.exists()` is a truthy Mock, which would short-circuit every + dispatch in this file. + """ + m_objects.filter.return_value.exists.return_value = False + + def _job(): from account_v2.models import Organization @@ -34,6 +71,7 @@ def _job(): @mock.patch.object(dispatch, "_dispatcher") @mock.patch.object(AgentKVJob, "objects") def test_dispatch_success_stamps_job(m_objects, m_disp, m_key): + _not_cancelled(m_objects) job = _job() dispatch.dispatch_job( job, @@ -74,7 +112,9 @@ def test_dispatch_success_stamps_job(m_objects, m_disp, m_key): assert job.dispatched_at is not None # Guarded queryset UPDATE (not job.save()): only a still-PENDING row may # be advanced to DISPATCHED. - m_objects.filter.assert_called_once_with(id=job.id, status=JobStatus.PENDING) + # The pre-dispatch terminal check filters first; the guarded bookkeeping + # UPDATE is the last filter call. + assert _filtered_with(m_objects, id=job.id, status=JobStatus.PENDING) m_objects.filter.return_value.update.assert_called_once_with( task_id=job.task_id, status=JobStatus.DISPATCHED, @@ -97,6 +137,7 @@ def test_dispatch_guarded_update_cannot_overwrite_a_terminal_row( pinning the exact WHERE-clause kwargs and simulating the "no rows matched" outcome, since this suite runs with no real DB. """ + _not_cancelled(m_objects) m_objects.filter.return_value.update.return_value = 0 # simulates a FAILED row job = _job() @@ -104,10 +145,7 @@ def test_dispatch_guarded_update_cannot_overwrite_a_terminal_row( # the update's row count; it already told the caller it dispatched. dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) - assert m_objects.filter.call_args_list[0].kwargs == { - "id": job.id, - "status": JobStatus.PENDING, - } + assert _filtered_with(m_objects, id=job.id, status=JobStatus.PENDING) # The 0-row result now triggers a SECOND guarded write (below), whose # `.exclude(status__in=TERMINAL)` is what keeps a terminal row untouched # here -- so the terminal case is still structurally safe. @@ -131,6 +169,7 @@ def test_dispatch_guarded_update_cannot_overwrite_a_terminal_row( def test_dispatch_stamps_dispatched_at_when_the_row_already_moved_to_running( m_objects, m_disp, m_key ): + _not_cancelled(m_objects) m_objects.filter.return_value.update.return_value = 0 # PENDING guard missed job = _job() @@ -138,10 +177,7 @@ def test_dispatch_stamps_dispatched_at_when_the_row_already_moved_to_running( # Second write is narrowed to a row that still has no dispatch time, and # excludes terminal rows. - assert m_objects.filter.call_args_list[1].kwargs == { - "id": job.id, - "dispatched_at__isnull": True, - } + assert _filtered_with(m_objects, id=job.id, dispatched_at__isnull=True) fallback = m_objects.filter.return_value.exclude.return_value update_kwargs = fallback.update.call_args.kwargs assert update_kwargs["dispatched_at"] == job.dispatched_at @@ -157,12 +193,14 @@ def test_dispatch_stamps_dispatched_at_when_the_row_already_moved_to_running( def test_dispatch_does_not_attempt_the_fallback_when_the_pending_guard_won( m_objects, m_disp, m_key ): + _not_cancelled(m_objects) m_objects.filter.return_value.update.return_value = 1 # normal path job = _job() dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) - assert len(m_objects.filter.call_args_list) == 1 + # Terminal check + the single guarded bookkeeping UPDATE; no fallback. + assert len(m_objects.filter.call_args_list) == 2 assert not m_objects.filter.return_value.exclude.called @@ -187,7 +225,10 @@ def test_dispatch_job_uses_platform_api_key_lookup(m_disp): return_value=mock.Mock(key="the-real-key"), ): job = _job() - with mock.patch.object(AgentKVJob, "objects"): + with mock.patch.object(AgentKVJob, "objects") as m_objects: + # Without this the pre-dispatch terminal check reads a truthy Mock + # and short-circuits, so nothing is ever enqueued. + _not_cancelled(m_objects) dispatch.dispatch_job( job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={} ) @@ -272,7 +313,8 @@ def test_dispatcher_factory_call_matches_the_live_signature(): def test_bookkeeping_failure_does_not_fail_an_already_queued_dispatch( m_objects, m_disp, m_key ): - m_objects.filter.side_effect = OSError("db gone") + _not_cancelled(m_objects) + _fail_only_bookkeeping(m_objects, OSError("db gone")) job = _job() # Must not raise: raising is what would mark the live job FAILED. @@ -289,9 +331,55 @@ def test_bookkeeping_failure_is_logged_at_error_level(m_objects, m_disp, m_key, """Swallowing it silently would trade one bad failure mode for another: the sweep reconciles the row, but nothing would say why it had to. """ - m_objects.filter.side_effect = OSError("db gone") + _not_cancelled(m_objects) + _fail_only_bookkeeping(m_objects, OSError("db gone")) with caplog.at_level("ERROR", logger=dispatch.logger.name): dispatch.dispatch_job( _job(), extractor=TABLE_EXTRACTOR_NAME, schema={}, options={} ) assert any(r.levelname == "ERROR" and r.exc_info for r in caplog.records) + + +# --------------------------------------------------------------------------- +# A job cancelled between submit's save() and the enqueue must not be dispatched. +# +# The cancel sees a PENDING, never-dispatched row, so it terminalizes it AND +# releases its concurrency slot -- correctly, nothing had been dispatched yet. +# Enqueueing anyway would then run paid work for a job the caller already +# cancelled, with its slot already handed to the next submit: the concurrency +# ceiling bypassed and the customer billed for a cancelled job. +# +# Reported by Greptile on PR #2317. +# --------------------------------------------------------------------------- +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_a_job_cancelled_before_enqueue_is_not_dispatched(m_objects, m_disp, m_key): + job = _job() + m_objects.filter.return_value.exists.return_value = True # already terminal + + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + assert ( + not m_disp.return_value.dispatch_with_callback.called + ), "paid work was enqueued for a job that was already cancelled" + # And no bookkeeping UPDATE either -- there is nothing to advance. + assert not m_objects.filter.return_value.update.called + + +@mock.patch.object(dispatch, "_platform_api_key", return_value="pk") +@mock.patch.object(dispatch, "_dispatcher") +@mock.patch.object(AgentKVJob, "objects") +def test_the_terminal_check_reads_the_row_rather_than_the_in_memory_copy( + m_objects, m_disp, m_key +): + """The in-memory job predates the cancel by construction, so trusting + `job.status` here would never see it. + """ + job = _job() + job.status = JobStatus.PENDING # stale: the row may already be CANCELLED + _not_cancelled(m_objects) + + dispatch.dispatch_job(job, extractor=TABLE_EXTRACTOR_NAME, schema={}, options={}) + + assert _filtered_with(m_objects, id=job.id, status__in=list(AgentKVJob.TERMINAL)) diff --git a/backend/agent_kv/tests/test_sweeps.py b/backend/agent_kv/tests/test_sweeps.py index 13cb1e28f7..49542aca04 100644 --- a/backend/agent_kv/tests/test_sweeps.py +++ b/backend/agent_kv/tests/test_sweeps.py @@ -63,21 +63,27 @@ def _post(path, body=None): # other phase's identical-shaped chain aliasing it. -def _wire_sweep_phases(m_objects, never_dispatched=(), stuck=()): - """Wire the sweep's two phase queries. +def _wire_sweep_phases(m_objects, never_dispatched=(), stuck=(), cancelled=()): + """Wire the sweep's three phase queries. Phase 2 is `filter(status__in=...).filter(Q(...) | Q(...))` -- two levels, because the age test is an OR: `dispatched_at < cutoff` OR `dispatched_at IS NULL AND created_at < cutoff`. A `__lt` filter alone can never match a NULL (SQL `NULL < x` is not true), so without that second arm a DISPATCHED/RUNNING row with a NULL `dispatched_at` hangs forever. + + Phase 3 releases slots still held by jobs CANCELLED after dispatch, whose + executor died without calling back -- neither phase above selects a + CANCELLED row, so the slot would sit occupied until Redis expired it. """ phase1_qs = mock.MagicMock() phase1_qs.order_by.return_value.__getitem__.return_value = list(never_dispatched) phase2_status_qs = mock.MagicMock() phase2_qs = phase2_status_qs.filter.return_value phase2_qs.order_by.return_value.__getitem__.return_value = list(stuck) - m_objects.filter.side_effect = [phase1_qs, phase2_status_qs] + phase3_qs = mock.MagicMock() + phase3_qs.order_by.return_value.__getitem__.return_value = list(cancelled) + m_objects.filter.side_effect = [phase1_qs, phase2_status_qs, phase3_qs] return phase1_qs, phase2_qs, phase2_status_qs @@ -662,3 +668,58 @@ def test_frozen_sweep_and_ttl_cleanup_urls_resolve_to_the_right_views(): ttl_cleanup = resolve("/internal/v1/agent-kv/ttl-cleanup/") assert ttl_cleanup.func.cls is iv.TTLCleanupView + + +# --------------------------------------------------------------------------- +# Phase 3: slots held by jobs CANCELLED after dispatch. +# +# Cancelling a dispatched job deliberately does not release its slot -- the +# executor is still running and still billing, so the slot belongs to the +# finalize callback. But if that executor dies no callback arrives, and neither +# phase 1 (PENDING) nor phase 2 (DISPATCHED/RUNNING) selects a CANCELLED row, +# so the slot sat occupied until Redis expired it six hours later. +# +# Reported by Greptile on PR #2317. +# --------------------------------------------------------------------------- +@mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +def test_sweep_releases_slots_held_by_abandoned_cancelled_jobs( + m_objects, m_mark_terminal, m_release +): + job = AgentKVJob(status=JobStatus.CANCELLED) + job.organization_id = "org1" + _wire_sweep_phases(m_objects, cancelled=[job]) + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + m_release.assert_called_once_with("org1", str(job.id)) + # The job is ALREADY terminal; phase 3 only frees the slot. + assert not m_mark_terminal.called + + +@mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "objects") +def test_phase_three_is_bounded_on_both_sides(m_objects, m_release): + """Older than the stuck grace, so a live executor is not cut short; and + newer than the slot TTL, past which Redis has already dropped the entry and + there is nothing left to release. Without the second bound this would + rescan every cancelled job ever, forever. + """ + frozen_now = timezone.now() + _wire_sweep_phases(m_objects) + + with mock.patch.object(timezone, "now", return_value=frozen_now): + iv.SweepView.as_view()(_post("/x")) + + kwargs = m_objects.filter.call_args_list[2].kwargs + assert kwargs["status"] == JobStatus.CANCELLED + assert kwargs["dispatched_at__isnull"] is False + stuck_cutoff = frozen_now - timedelta( + seconds=settings.AGENT_KV_STUCK_JOB_GRACE_SECONDS + ) + assert kwargs["completed_at__lt"] == stuck_cutoff + assert kwargs["completed_at__gt"] == frozen_now - timedelta( + seconds=maintenance.SLOT_TTL_SECONDS + ) diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index 65aba8f73a..ddef2f5b22 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -201,8 +201,17 @@ def agent_kv_error( raise -@worker_task(name="agent_kv_cancelled") +@worker_task( + bind=True, + name="agent_kv_cancelled", + autoretry_for=(Exception,), + max_retries=3, + retry_backoff=True, + retry_backoff_max=300, + retry_jitter=True, +) def agent_kv_cancelled( + self, callback_kwargs: dict[str, Any] | None = None, ) -> dict[str, Any] | None: """Fire the terminal webhook for a job cancelled by the API. @@ -226,14 +235,27 @@ def agent_kv_cancelled( if not url: return None try: - _send_webhook(url, job_id, "cancelled") + delivered = _send_webhook(url, job_id, "cancelled") except Exception: logger.exception("agent_kv_cancelled webhook failed: job_id=%s", job_id) raise + if not delivered: + # `send_webhook` returns False for a non-2xx response or a connection + # failure. Returning success here acknowledged the queue message, so + # the caller simply never received the terminal notification this task + # exists to deliver -- and nothing retried or recorded it. Raise so the + # retry budget applies, exactly as a transport exception would. + logger.error( + "agent_kv_cancelled: webhook not delivered for job %s (retry %s of " "%s)", + job_id, + self.request.retries, + self.max_retries, + ) + raise RuntimeError(f"cancellation webhook not delivered for job {job_id}") return {"job_id": job_id, "status": "cancelled"} -def _send_webhook(url: str, job_id: str, status: str) -> None: +def _send_webhook(url: str, job_id: str, status: str) -> bool: """Deliver one terminal notification. Single place the SSRF waiver is read, so the cancel path cannot drift from @@ -244,7 +266,7 @@ def _send_webhook(url: str, job_id: str, status: str) -> None: allow_insecure = os.environ.get( "AGENT_KV_WEBHOOK_INSECURE_ALLOW_HTTP_PRIVATE", "" ).lower() in ("1", "true", "yes") - send_webhook( + return send_webhook( url, {"job_id": job_id, "status": status}, allow_insecure=allow_insecure, From bd82bc8dd61b37de9f0bb482e67a26e30763d6f4 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Tue, 6 Oct 2026 15:39:03 +0530 Subject: [PATCH 11/18] UN-4232 [FIX] Webhook delivery results, and two comments that described the opposite of the code WEBHOOK DELIVERY RESULT (2.10, partial) `send_webhook` returns False for a non-2xx or a connection failure, and both call sites discarded it -- so a terminal notification could fail to land with no retry, no record, and for a non-2xx not even a log line. The cancellation task raises (its retry budget then applies). The finalize path logs at ERROR instead: it runs AFTER finalize has already terminalized the job, so raising would re-run a finalize that is no longer free. Three tests. Durable delivery -- attempt counts and a `webhook_delivered_at` the status document can expose -- is the open design question in 2.10 and is deliberately not decided here. TWO COMMENTS THAT SAID THE OPPOSITE OF WHAT THE CODE DOES `constants.py` line 1 -- the first line of the file that defines the routing table -- said "v1 accepts exactly one extractor (`kv`), so the job row does not carry which one it ran". Both halves are now false: `kv` is the one extractor this deployment does NOT accept, and `AgentKVJob.extractor` has carried the name since migration 0002. `test_queue_consumer_wiring.py`'s docstring described only "every queue we dispatch to must have a consumer" and then narrated how `celery_executor_agentic_kv` ought to be wired in -- while the file's actual assertion is that it must NOT be advertised. That is the one thing a guard with an inverted assertion must not say: a reader checking the file against its own description would have concluded the test was wrong. It now states both directions and why each exists. Both were mine, and both are the kind of stale comment that is worse than no comment -- a reader trusts them over the code. 232 backend tests, ide_callback 23, queue wiring 12. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/constants.py | 11 +++- workers/ide_callback/agent_kv_tasks.py | 18 +++++- .../tests/test_agent_kv_callbacks.py | 63 +++++++++++++++++++ workers/tests/test_queue_consumer_wiring.py | 32 ++++++---- 4 files changed, 108 insertions(+), 16 deletions(-) diff --git a/backend/agent_kv/constants.py b/backend/agent_kv/constants.py index 431316e078..04cf856e04 100644 --- a/backend/agent_kv/constants.py +++ b/backend/agent_kv/constants.py @@ -1,6 +1,11 @@ -#: v1 accepts exactly one extractor (`kv`), so the job row does not carry which -#: one it ran. When fan-out lands this becomes per-job state rather than a -#: constant -- the wire format (spec §7.0) is already shaped for that. +#: The `kv` extractor's name. Kept as a constant, but NOT routable on this +#: deployment -- see `EXTRACTOR_ROUTES` below, which carries `table` only. +#: +#: An earlier version of this comment said "v1 accepts exactly one extractor +#: (`kv`), so the job row does not carry which one it ran". Both halves are now +#: wrong, on line 1 of the file that defines the routing table: `kv` is the one +#: extractor this deployment does NOT accept, and `AgentKVJob.extractor` has +#: carried the name since migration 0002. V1_EXTRACTOR_NAME = "kv" STAGE_NAMES = [ diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index ddef2f5b22..4b13142892 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -290,4 +290,20 @@ def _maybe_webhook(finalize_response: dict[str, Any], job_id: str) -> None: url = finalize_response.get("webhook_url") or "" if not url: return - _send_webhook(url, job_id, finalize_response.get("status", "")) + status = finalize_response.get("status", "") + if not _send_webhook(url, job_id, status): + # `send_webhook` returns False for a non-2xx or a connection failure. + # Discarding that meant a terminal notification could fail to land with + # no retry, no record on the job, and -- for a non-2xx -- not even a log + # line. Logged at ERROR here rather than raised: unlike the cancellation + # task, this runs AFTER finalize has already terminalized the job, so + # raising would re-run a finalize that is no longer idempotent-free. + # + # Durable delivery (attempt counts, a `webhook_delivered_at` the status + # document can expose) is the open design question in 2.10. + logger.error( + "agent-kv webhook not delivered for job %s (status=%s); the caller " + "was not notified and nothing will retry", + job_id, + status, + ) diff --git a/workers/ide_callback/tests/test_agent_kv_callbacks.py b/workers/ide_callback/tests/test_agent_kv_callbacks.py index 07ad5bf24b..6278b48d82 100644 --- a/workers/ide_callback/tests/test_agent_kv_callbacks.py +++ b/workers/ide_callback/tests/test_agent_kv_callbacks.py @@ -430,3 +430,66 @@ def test_webhook_fires_from_error_link_too(self, mock_send_webhook, cb_kwargs): {"job_id": "job-1", "status": "failed"}, allow_insecure=False, ) + + +class TestWebhookDeliveryResult: + """A webhook that does not land must not pass silently. + + `send_webhook` returns False for a non-2xx response or a connection + failure, and both call sites used to discard it. The cancellation task now + raises (so its retry budget applies); the finalize path logs, because it + runs after the job is already terminalized and re-running finalize is not + free. Reported as 2.10 in the branch review and by Greptile on #2317. + """ + + @patch(_PATCH_SEND_WEBHOOK, return_value=False) + def test_a_failed_finalize_webhook_is_logged(self, mock_send, caplog): + import logging + + from ide_callback import agent_kv_tasks as akt + + with caplog.at_level(logging.ERROR, logger="ide_callback.agent_kv_tasks"): + akt._maybe_webhook( + { + "finalized": True, + "webhook_url": "https://hook.example/x", + "status": "completed", + }, + "job-1", + ) + + assert "not delivered" in caplog.text + assert "job-1" in caplog.text + + @patch(_PATCH_SEND_WEBHOOK, return_value=True) + def test_a_delivered_finalize_webhook_is_quiet(self, mock_send, caplog): + import logging + + from ide_callback import agent_kv_tasks as akt + + with caplog.at_level(logging.ERROR, logger="ide_callback.agent_kv_tasks"): + akt._maybe_webhook( + { + "finalized": True, + "webhook_url": "https://hook.example/x", + "status": "completed", + }, + "job-1", + ) + + assert caplog.records == [] + + @patch(_PATCH_SEND_WEBHOOK, return_value=False) + def test_a_failed_cancellation_webhook_raises_so_it_retries(self, mock_send): + from ide_callback.agent_kv_tasks import agent_kv_cancelled + + with pytest.raises(Exception, match="not delivered"): + agent_kv_cancelled.apply( + kwargs={ + "callback_kwargs": { + "job_id": "job-1", + "webhook_url": "https://hook.example/x", + } + }, + throw=True, + ) diff --git a/workers/tests/test_queue_consumer_wiring.py b/workers/tests/test_queue_consumer_wiring.py index 0868d6f35c..6f98c2675f 100644 --- a/workers/tests/test_queue_consumer_wiring.py +++ b/workers/tests/test_queue_consumer_wiring.py @@ -1,20 +1,28 @@ -"""Guard: every queue we dispatch to must have a consumer in the deployed fleet. +"""Guard: queues and routes must match the fleet this deployment actually runs. -This is the OSS analogue of the cloud chart's ``validate-pg-worker-fleet.yaml``. -It exists because the failure it catches is **silent**: since UN-4046 made the -PG transport unconditional, a queue whose consumer is not configured still +The OSS analogue of the cloud chart's ``validate-pg-worker-fleet.yaml``, and it +exists because the failure it catches is **silent**: since UN-4046 made the PG +transport unconditional, a queue whose consumer is not configured still *accepts* work -- ``enqueue_task`` succeeds, rows land in ``pg_queue_message``, and nothing errors at the producer. The job simply sits in DISPATCHED forever. -That is exactly what happened to ``celery_executor_agentic_kv``: the Agent-KV -branch was written when the Celery executor was live, so its queue was wired -into ``CELERY_QUEUES_EXECUTOR`` -- a variable read only by -``workers/executor/worker.py`` (the now-disabled Celery worker). The live -``pg-queue-consumer`` reads ``WORKER_PG_QUEUE_CONSUMER_QUEUE``, which did not -list it. +The assertions run in BOTH directions, which is the part to read carefully: -Each test therefore asserts the queue is present in the variable the *running* -consumer actually reads, at every site that configures one. +* ``celery_executor_agentic_table`` and ``agent_kv_callback`` must HAVE a + consumer -- they are what this deployment dispatches onto. +* ``celery_executor_agentic_kv`` must NOT be advertised. This deployment ships + no ``agentic_kv`` plugin, so a fleet listing that queue would accept work + nothing can drain -- the same silent failure, arrived at from the other side. + +An earlier version of this docstring described only the first direction and +narrated how ``celery_executor_agentic_kv`` ought to be wired in. That is the +one thing a guard with an inverted assertion must not say: a reader checking +the file against its own description would have concluded the test was wrong. + +Each test asserts against the variable the *running* consumer actually reads, +at every site that configures one -- plus the traefik rules, where the same +"configured-looking but unreachable" failure shows up as a request served by +the SPA instead of Django. """ from __future__ import annotations From 7efbc4c6b11f9cd02f4f931cc5d43619ebe72f6c Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Wed, 7 Oct 2026 10:33:28 +0530 Subject: [PATCH 12/18] UN-4232 [FIX] Address the remaining open review threads on #2317 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nine open PR threads, in four groups. **Silent-skip bugs that reported success (2.19)** `constraints.py` called itself fail-closed in its module docstring and on the `except Exception` line, but `evaluate_constraints` records only `is False` as a violation -- so a constraint that raised was reported as "no violation", a positive assurance that nothing was checked. The path is reachable, not theoretical: `_BIN` has `truediv` and `_ALLOWED_NODES` has `ast.Div`, so a row with `quantity == 0` raises `ZeroDivisionError`. Two senses of "fail-closed" were conflated: the grammar IS closed (nothing outside the allowlist runs), the outcome is not. Relabelled, and every drop is now logged -- `exception` level for a raise, `debug` for the ordinary missing-operand skip, which is normal on a document with optional keys. `_aggregate` returned a PARTIAL sum/avg when some cells failed `coerce_number`, which is then compared against a scalar key covering the whole column and reports a violation on a CORRECT document. One blank cell in a 40-row invoice was enough. sum/avg now require every row to contribute; min/max/count are left total over a subset, which is what they mean. **Schemas accepted that could never validate (2.18)** `format: "Number"` is not a known format, so it became a free-text LLM hint, so `validate_format` returned True for every value forever and `/validate` reported `{"valid": true}` -- validation configured, none delivered. `format: "enum:"` and `format: "regex:"` are worse: they do not disable validation, they invert it, and no value can ever conform. An array `_key` naming a non-existent column silently falls back to positional identity, costing accuracy on exactly the arrays the author cared enough to key. All four are refused at `compile_schema` now, with the intended spelling in the message. Free-text format stays accepted -- that is deliberate. `KeySpec` and `ArraySpec` are frozen: they are compile output, the one derived variant already goes through `dataclasses.replace`. **Docs and docstrings that documented the opposite of the code** - docs §12 described both limiters as failing OPEN on Redis errors. They fail CLOSED, and have for the life of this branch. An operator sizing a Redis outage from that row would expect "requests get through" and get "every submit 429s" -- the opposite incident. Corrected, `AGENT_KV_LIMITER_FAIL_OPEN` documented with its real default, and added to both sample.env files. - `internal_client.agent_kv_stage_report`'s examples (`"extract"`, `"started"`, `"completed"`) are all rejected: `StageReportView` accepts only `running`/`done`, and an off-list stage name is stored then filtered out of every status response by `_status_document`. - `ValidateView` had no docstring. A reader of `execution_views.py` sees a complete, decorated, live-looking endpoint; the only thing that decided otherwise was `execution_urls.py`, a file they may never open. - The schema package's pyproject still called itself the "single source of truth for API validation and the extraction engine", which `compile.py:7-15` explicitly retracts. **Latent (2.16)** `mark_terminal`'s guarded UPDATE excludes terminal ROWS, not non-terminal ARGUMENTS, so `mark_terminal(..., RUNNING)` would stamp `completed_at=now()`: a row that reads as finished to the TTL filter and the sweep's cancelled-job phase while staying invisible to the terminal guard, so nothing could ever terminalize it again. No caller does this; the method is named for the invariant, so it enforces it. **Verification** 248 backend tests (was 233) and 128 schema-package tests, all green. Every new guard mutation-tested: reintroducing each bug fails the test that covers it -- the format/`_key` checks (14 tests), the partial-sum skip (3), the drop logging (1), the freeze (1), the `mark_terminal` guard (3), the ValidateView docstring (1) and the two docs rows (2). `unstract/agent-kv-schema/tests/test_constraints.py` was 0 bytes against a 258-line module -- an empty file with that name reads as coverage. It now carries the operator, arithmetic, boolean and aggregate matrix, plus a test that `compile._ALLOWED_NODES`/`_ALLOWED_CALLS` and `constraints._CMP`/`_BIN`/`_AGG` agree: two hand-maintained allowlists in two files, where a node accepted at submit but absent from the evaluator is a constraint silently skipped for every document (2.20). Not done here, and why: surfacing a skipped-with-reason third state in the QA output is a public contract change on the `kv` extractor, which this deployment does not ship. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 21 ++ backend/agent_kv/maintenance.py | 15 +- backend/agent_kv/models.py | 14 + backend/agent_kv/tests/test_job_views.py | 37 +++ backend/agent_kv/tests/test_models.py | 42 +++ backend/agent_kv/tests/test_rate_limiter.py | 37 +++ backend/agent_kv/tests/test_submit_view.py | 64 +++-- backend/agent_kv/tests/test_sweeps.py | 26 ++ .../tests/test_table_extractor_routing.py | 35 +++ backend/sample.env | 1 + docker/sample.env | 1 + docs/agent-kv-api.md | 5 +- unstract/agent-kv-schema/pyproject.toml | 2 +- .../src/unstract/agent_kv_schema/compile.py | 81 +++++- .../unstract/agent_kv_schema/constraints.py | 69 ++++- .../unstract/agent_kv_schema/dataclasses.py | 12 +- .../agent-kv-schema/tests/test_compile.py | 144 ++++++++++ .../agent-kv-schema/tests/test_constraints.py | 258 ++++++++++++++++++ workers/shared/api/internal_client.py | 13 +- 19 files changed, 840 insertions(+), 37 deletions(-) diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index eb3bcb9645..262888fe0c 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -353,6 +353,27 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): class ValidateView(APIView): + """Dry-run schema compile. **NOT ROUTED ON THIS DEPLOYMENT.** + + ``execution_urls.py`` deliberately omits this view, and that file carries + the full rationale. The short version: it compiles a `kv` keys schema and + does nothing else, and `kv` is the one extractor this build refuses + (``EXTRACTOR_ROUTES`` carries `table` only), so publishing it would + advertise validation for an extractor every submit 400s. The `table` + extractor's ``keys`` is ``{"target_table": ...}``, validated by + ``TableKeysSerializer`` at submit instead. + + Kept in the tree, not deleted: restoring the endpoint is one line in + ``execution_urls.py``, and deleting it would force a content merge in the + file the branch carrying the KV engine rewrites most. + + So read the code below as dormant. Nothing reaches it but the unit tests + that keep it honest -- in particular, it is NOT a live surface for the + rate limiter or the key validator it still decorates, and a reader looking + for the deployment's public endpoints should look at ``execution_urls.py``, + which is the only place that decides. + """ + authentication_classes: list = [] permission_classes: list = [] diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py index c391761ffe..a0a6bbe545 100644 --- a/backend/agent_kv/maintenance.py +++ b/backend/agent_kv/maintenance.py @@ -182,7 +182,20 @@ def run_sweep() -> dict: dispatched_at__isnull=False, completed_at__lt=stuck_cutoff, completed_at__gt=slot_ttl_floor, - ).order_by("completed_at")[:_MAINTENANCE_BATCH_LIMIT] + # NEWEST first, the opposite of the other two phases and deliberate. + # Releasing a slot does not remove its row from this query -- there is + # no "released" marker -- so oldest-first would re-select the same 500 + # rows every sweep whenever the backlog exceeds the batch limit, and + # rows arriving behind them would age out of the window never having + # been looked at. + # + # Newest-first inverts which rows lose: the ones skipped are the oldest, + # i.e. closest to the slot TTL floor, where Redis is about to drop the + # entry anyway and a release buys almost nothing. Every row is seen + # while its release still matters, unless more than + # `_MAINTENANCE_BATCH_LIMIT` become eligible inside one sweep interval + # -- and the TTL remains the backstop for that. + ).order_by("-completed_at")[:_MAINTENANCE_BATCH_LIMIT] released = 0 for job in abandoned_cancelled: diff --git a/backend/agent_kv/models.py b/backend/agent_kv/models.py index fa28b4d68e..7afb051364 100644 --- a/backend/agent_kv/models.py +++ b/backend/agent_kv/models.py @@ -126,7 +126,21 @@ def mark_terminal( Guarded UPDATE: at-least-once callbacks, cancel, and the sweep can all race; whoever lands first wins and everyone else no-ops. + + The `.exclude(status__in=TERMINAL)` below guards the ROW, not the + argument -- so without the check that opens this method, + `mark_terminal(..., JobStatus.RUNNING)` would stamp + `status=RUNNING, completed_at=now()`: a row that reads as finished to + every TTL/sweep query that keys off `completed_at`, is invisible to + the terminal guard, and can never be terminalized again by anything + that trusts `completed_at`. No caller does this today; the method is + named for the invariant, so it enforces it rather than documenting it. """ + if new_status not in cls.TERMINAL: + raise ValueError( + f"mark_terminal called with non-terminal status {new_status!r}; " + f"expected one of {sorted(cls.TERMINAL)}" + ) fields = {"status": new_status, "completed_at": timezone.now()} if error: fields["error"] = error diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index b2ff721645..a5332748b3 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -4,6 +4,7 @@ from unittest import mock import django +import pytest from django.apps import apps os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") @@ -37,6 +38,42 @@ def test_foreign_org_job_is_404(m_keys, m_jobs): assert resp.status_code == 404 +# --------------------------------------------------------------------------- +# (1b) ...and the lookup is actually ORG-SCOPED. +# +# The test above forces `DoesNotExist` and asserts the view 404s, which says +# nothing about the `organization_id` filter: delete +# `organization_id=agent_kv_key.organization_id` from `_get_job` and it stays +# green -- while every org could then read, cancel, delete and fetch the result +# of every other org's job. +# +# The production code IS correctly scoped. What was missing is a test that can +# detect its removal, on the one security-relevant path in the app. Every +# job-scoped view funnels through `_get_job`, so each is covered. +# +# Reported by Greptile on PR #2317. +# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + ("view", "method"), + [ + (ev.JobStatusView, "get"), + (ev.JobResultView, "get"), + (ev.JobCancelView, "post"), + (ev.JobDeleteView, "delete"), + ], +) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_every_job_scoped_view_filters_by_organization(m_keys, m_jobs, view, method): + m_keys.get.return_value = kv_key(org_pk=7) + m_jobs.get.side_effect = AgentKVJob.DoesNotExist + job_id = uuid.uuid4() + + view.as_view()(_authed(method=method), job_id=job_id) + + m_jobs.get.assert_called_once_with(id=job_id, organization_id=7) + + # --------------------------------------------------------------------------- # (2) status running -> stages list ordered per STAGE_NAMES, lowercased # status, only the stages actually present are included. diff --git a/backend/agent_kv/tests/test_models.py b/backend/agent_kv/tests/test_models.py index 6966fac515..231252c135 100644 --- a/backend/agent_kv/tests/test_models.py +++ b/backend/agent_kv/tests/test_models.py @@ -5,6 +5,7 @@ from unittest import mock import django +import pytest from django.apps import apps os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") @@ -52,3 +53,44 @@ def test_mark_terminal_on_already_terminal_row_is_noop_false(m_objects): new_status=JobStatus.COMPLETED, ) assert ok is False + + +# -------------------------------------------------------------------------- +# 2.16: the guarded UPDATE excludes terminal ROWS, not non-terminal ARGUMENTS. +# -------------------------------------------------------------------------- + + +@mock.patch.object(AgentKVJob, "objects") +@pytest.mark.parametrize( + "bad_status", [JobStatus.PENDING, JobStatus.DISPATCHED, JobStatus.RUNNING] +) +def test_mark_terminal_refuses_a_non_terminal_status(m_objects, bad_status): + """`mark_terminal(..., RUNNING)` used to stamp `completed_at=now()`. + + That leaves a row that reads as finished to everything keying off + `completed_at` (the TTL filter, the sweep's cancelled-job phase) while + still being invisible to the terminal guard -- so nothing can ever + terminalize it again. No caller does this today; the method is named for + the invariant, so it enforces it. + """ + with pytest.raises(ValueError, match="non-terminal status"): + AgentKVJob.mark_terminal( + job_id=uuid.uuid4(), + organization_id="org1", + new_status=bad_status, + ) + assert not m_objects.filter.called, "no write may be attempted" + + +@mock.patch.object(AgentKVJob, "objects") +@pytest.mark.parametrize("good_status", sorted(AgentKVJob.TERMINAL)) +def test_mark_terminal_accepts_every_terminal_status(m_objects, good_status): + m_objects.filter.return_value.exclude.return_value.update.return_value = 1 + assert ( + AgentKVJob.mark_terminal( + job_id=uuid.uuid4(), + organization_id="org1", + new_status=good_status, + ) + is True + ) diff --git a/backend/agent_kv/tests/test_rate_limiter.py b/backend/agent_kv/tests/test_rate_limiter.py index 58fa295fd0..d7d4cdd9a1 100644 --- a/backend/agent_kv/tests/test_rate_limiter.py +++ b/backend/agent_kv/tests/test_rate_limiter.py @@ -1,7 +1,9 @@ import os +import pathlib from unittest import mock import django +import pytest from django.apps import apps os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") @@ -153,3 +155,38 @@ def test_release_still_tolerates_an_unreachable_backend(m_redis): work, and a slot it cannot free expires on its own TTL. """ rl.AgentKVConcurrencyLimiter.release("org1", "job1") + + +# --------------------------------------------------------------------------- +# The published env table said both limiters "fail open on Redis errors", for +# the whole life of the branch that made them fail CLOSED. An operator reading +# it would size a Redis outage as "requests get through" when the real +# behaviour is "every submit 429s" -- the opposite incident. Docs are the +# contract for a public API, so this is pinned rather than trusted. +# --------------------------------------------------------------------------- + +_DOCS = pathlib.Path(__file__).resolve().parents[3] / "docs" / "agent-kv-api.md" + + +@pytest.mark.parametrize( + "variable", + ["AGENT_KV_CONCURRENT_LIMIT", "AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE"], +) +def test_the_docs_do_not_describe_either_limiter_as_failing_open(variable): + row = next(line for line in _DOCS.read_text().splitlines() if f"`{variable}`" in line) + assert "fails open" not in row.casefold(), row + assert "closed" in row.casefold(), ( + f"the {variable} row must say what a Redis outage does, and it fails " + "closed -- see _limiter_failure_allows_request" + ) + + +def test_the_waiver_flag_is_documented_with_its_real_default(): + text = _DOCS.read_text() + assert "`AGENT_KV_LIMITER_FAIL_OPEN`" in text, ( + "the only way to restore fail-open is undocumented, so the only " + "documented behaviour was the wrong one" + ) + assert not rl._limiter_failure_allows_request() or getattr( + settings, "AGENT_KV_LIMITER_FAIL_OPEN", False + ), "the default must be fail-closed" diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index ba105f32dd..b9d7ba70ba 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -58,20 +58,26 @@ def _authed_post(data=None): def _valid_validated_data(**overrides): """`SubmitSerializer.validated_data` in the extractor-scoped shape (§7.0).""" - keys = overrides.pop("keys", {"total": {"description": "Grand total"}}) + # `table`, not `kv`. The carve-out re-pointed the real-serializer tests and + # missed this mocked payload, so every view test driven by it called + # `dispatch_job(extractor="kv")` -- whose FIRST statement is + # `EXTRACTOR_ROUTES["kv"]`, a KeyError raised OUTSIDE the try. That landed + # in SubmitView's belt-and-braces `except Exception` and produced the exact + # 500 some of these tests assert, so they passed while never reaching the + # code they name. It also meant the whole file exercised the view with an + # `extractors` payload the real serializer rejects with a 400. + keys = overrides.pop("keys", {"target_table": "Rent roll"}) options = { - "qa": True, - "challenge": True, - "extraction_mode": "whole-doc", - "structured_output": False, - "calculations": "", - "document_class": "", - "key_notes": "", + "instructions": "", + "json_structure": "", + "enable_header_mapping": False, + "correct_number_separators": False, + "number_format": "US", } options.update({k: overrides.pop(k) for k in list(overrides) if k not in _JOB_LEVEL}) data = { "file": mock.Mock(name="uploaded_file"), - "extractors": [{"name": "kv", "keys": keys, "options": options}], + "extractors": [{"name": "table", "keys": keys, "options": options}], "page_start": 1, "page_end": None, "timeout": 0, @@ -265,19 +271,15 @@ def test_dispatch_job_called_with_expected_options_and_schema( m_dispatch, ): m_keys.get.return_value = kv_key() - keys_schema = {"total": {"description": "Grand total"}} + keys_schema = {"target_table": "Rent roll"} _mock_serializer( m_serializer_cls, keys=keys_schema, - qa=False, - challenge=False, - extraction_mode="per-page", - structured_output=True, + number_format="EU", + enable_header_mapping=True, + instructions="skip the header row", page_start=2, page_end=5, - document_class="invoice", - key_notes="ignore footers", - calculations="annualize rent", ) m_limiter.check_and_acquire.return_value = True m_save.side_effect = _stamp_created_at @@ -286,17 +288,19 @@ def test_dispatch_job_called_with_expected_options_and_schema( assert m_dispatch.called kwargs = m_dispatch.call_args.kwargs + # The extractor name itself was never asserted, so this test could not tell + # a `table` dispatch from a `kv` one -- which is how the mocked payload + # stayed on `kv` right through the carve-out. + assert kwargs["extractor"] == "table" assert kwargs["schema"] == keys_schema assert kwargs["options"] == { - "qa": False, - "challenge": False, - "extraction_mode": "per-page", - "structured_output": True, + "instructions": "skip the header row", + "json_structure": "", + "enable_header_mapping": True, + "correct_number_separators": False, + "number_format": "EU", "page_start": 2, "page_end": 5, - "document_class": "invoice", - "key_notes": "ignore footers", - "calculations": "annualize rent", } @@ -425,6 +429,18 @@ def test_platform_key_lookup_failure_inside_real_dispatch_is_caught_end_to_end( resp = ev.SubmitView.as_view()(_authed_post()) + # The assertion this test was missing. Without it it passed while never + # reaching the code it names: the mocked payload said `kv`, so + # `dispatch_job`'s first statement -- `EXTRACTOR_ROUTES["kv"]` -- raised a + # KeyError OUTSIDE the try, the view's belt-and-braces handler produced the + # same 500, and `_platform_api_key` was never called. The + # "leaked-secret-xyz" check below was vacuous: that string was never + # produced. + assert m_platform_key.called, ( + "the platform-key lookup was never reached, so this test proves " + "nothing about the widened dispatch.py try it exists to cover" + ) + assert resp.status_code == 500 assert "leaked-secret-xyz" not in str(resp.data) assert resp.data["status"] == JobStatus.FAILED.lower() diff --git a/backend/agent_kv/tests/test_sweeps.py b/backend/agent_kv/tests/test_sweeps.py index 49542aca04..cfbee37b7e 100644 --- a/backend/agent_kv/tests/test_sweeps.py +++ b/backend/agent_kv/tests/test_sweeps.py @@ -723,3 +723,29 @@ def test_phase_three_is_bounded_on_both_sides(m_objects, m_release): assert kwargs["completed_at__gt"] == frozen_now - timedelta( seconds=maintenance.SLOT_TTL_SECONDS ) + + +def test_phase_three_takes_the_newest_eligible_rows_first(): + """Releasing a slot does not remove its row from phase 3's query -- there + is no "released" marker -- so oldest-first would re-select the same batch + every sweep under backlog, and rows arriving behind it would age out of the + window unprocessed. + + Newest-first inverts which rows lose: the skipped ones are the oldest, i.e. + closest to the slot TTL floor where Redis is about to drop the entry anyway + and a release buys almost nothing. + + Reported by Greptile on PR #2317. + """ + phase1 = mock.MagicMock() + phase1.order_by.return_value.__getitem__.return_value = [] + phase2_status = mock.MagicMock() + phase2_status.filter.return_value.order_by.return_value.__getitem__.return_value = [] + phase3 = mock.MagicMock() + phase3.order_by.return_value.__getitem__.return_value = [] + + with mock.patch.object(AgentKVJob, "objects") as m_objects: + m_objects.filter.side_effect = [phase1, phase2_status, phase3] + maintenance.run_sweep() + + phase3.order_by.assert_called_once_with("-completed_at") diff --git a/backend/agent_kv/tests/test_table_extractor_routing.py b/backend/agent_kv/tests/test_table_extractor_routing.py index 44c012f5b9..07ded10fcd 100644 --- a/backend/agent_kv/tests/test_table_extractor_routing.py +++ b/backend/agent_kv/tests/test_table_extractor_routing.py @@ -239,3 +239,38 @@ def test_a_job_whose_extractor_has_no_stage_list_reports_no_stages(caplog): assert doc["extractors"]["retired_extractor"]["stages"] == [] assert doc["status"] == job.status.lower() assert "retired_extractor" in caplog.text + + +# --------------------------------------------------------------------------- +# `ValidateView` is part of the carve-out: the class stays in the tree and the +# route does not. Both halves are asserted here, including that the class says +# so itself -- a reader of `execution_views.py` sees a complete, decorated, +# live-looking endpoint, and the only thing that decided otherwise was a file +# they may never open. +# --------------------------------------------------------------------------- + + +def test_validate_is_not_a_public_route_on_this_deployment(): + from agent_kv import execution_urls + + names = {p.name for p in execution_urls.urlpatterns} + assert names == { + "agent_kv_submit", + "agent_kv_status", + "agent_kv_result", + "agent_kv_cancel", + }, ( + "the public route set changed; `/validate` compiles a `kv` schema and " + "`kv` is the one extractor this build refuses, so routing it would " + "advertise validation for an extractor every submit 400s" + ) + + +def test_validate_view_documents_that_it_is_unrouted(): + from agent_kv.execution_views import ValidateView + + doc = ValidateView.__doc__ or "" + assert "NOT ROUTED" in doc, ( + "ValidateView is a complete, decorated, live-looking endpoint that " + "nothing reaches. Say so on the class, not only in execution_urls.py" + ) diff --git a/backend/sample.env b/backend/sample.env index c32b815e46..78b526f1d1 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -197,6 +197,7 @@ AGENT_KV_RESULT_TTL_DAYS=7 # Result/input retention win AGENT_KV_MAX_TIMEOUT_SECONDS=300 # Upper bound on the submit `timeout` (sync-wait) field AGENT_KV_CONCURRENT_LIMIT=5 # Per-org concurrent in-flight job cap AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE=60 # Per-key request rate limit (submit + validate) +AGENT_KV_LIMITER_FAIL_OPEN=false # true = both limiters above allow the request when Redis is down (default: refuse with 429) AGENT_KV_SWEEP_GRACE_SECONDS=3600 # Age before a never-dispatched PENDING job is swept AGENT_KV_STUCK_JOB_GRACE_SECONDS=21600 # Age (from dispatch) before a stuck DISPATCHED/RUNNING job is force-failed (6h) AGENT_KV_CALCULATIONS_ENABLED=false # Gates the submit `calculations` field; engine can't run it yet diff --git a/docker/sample.env b/docker/sample.env index 39155d8ba7..de64df726f 100644 --- a/docker/sample.env +++ b/docker/sample.env @@ -132,6 +132,7 @@ AGENT_KV_RESULT_TTL_DAYS=7 # Result/input retention win AGENT_KV_MAX_TIMEOUT_SECONDS=300 # Upper bound on the submit `timeout` (sync-wait) field AGENT_KV_CONCURRENT_LIMIT=5 # Per-org concurrent in-flight job cap AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE=60 # Per-key request rate limit (submit + validate) +AGENT_KV_LIMITER_FAIL_OPEN=false # true = both limiters above allow the request when Redis is down (default: refuse with 429) AGENT_KV_SWEEP_GRACE_SECONDS=3600 # Age before a never-dispatched PENDING job is swept AGENT_KV_STUCK_JOB_GRACE_SECONDS=21600 # Age (from dispatch) before a stuck DISPATCHED/RUNNING job is force-failed (6h) AGENT_KV_CALCULATIONS_ENABLED=false # Gates the submit `calculations` field; engine can't run it yet diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md index 4d6ebfd424..8cb78e06f6 100644 --- a/docs/agent-kv-api.md +++ b/docs/agent-kv-api.md @@ -982,8 +982,9 @@ with the default shown: | `AGENT_KV_MAX_SCHEMA_BYTES` | `262144` | Byte cap on the raw `keys` JSON document (256 KiB). | | `AGENT_KV_RESULT_TTL_DAYS` | `7` | Result retention window (and a cancelled job's input, which rides the same TTL — a completed/failed job's input is deleted immediately at finalize instead), stamped at submit time (see [§6](#9-retention-and-ttl)). | | `AGENT_KV_MAX_TIMEOUT_SECONDS` | `300` | Upper bound on the submit `timeout` (synchronous-wait) field. | -| `AGENT_KV_CONCURRENT_LIMIT` | `5` | Per-organization concurrent in-flight job cap (own Redis namespace, fails open on Redis errors). | -| `AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE` | `60` | Per-key request rate limit (submit + validate), fails open on Redis errors. | +| `AGENT_KV_CONCURRENT_LIMIT` | `5` | Per-organization concurrent in-flight job cap (own Redis namespace). Fails **closed** (429) when Redis is unreachable — see `AGENT_KV_LIMITER_FAIL_OPEN`. | +| `AGENT_KV_KEY_RATE_LIMIT_PER_MINUTE` | `60` | Per-key request rate limit (submit + validate). Fails **closed** (429) when Redis is unreachable — see `AGENT_KV_LIMITER_FAIL_OPEN`. | +| `AGENT_KV_LIMITER_FAIL_OPEN` | `false` | What both limiters above do when Redis is unreachable. Default `false` = fail **closed**: the request is refused with a 429. Set `true` to restore the old fail-open behaviour, where an unreachable Redis removed the concurrency ceiling **and** the per-key rate ceiling at once while the API went on accepting billable work — a state that cannot be observed from the outside. A 429 is recoverable by the caller; an unbounded fan-out is not. | | `AGENT_KV_SWEEP_GRACE_SECONDS` | `3600` | Age (from `created_at`) before a never-dispatched `PENDING` job is eligible for the sweep. | | `AGENT_KV_STUCK_JOB_GRACE_SECONDS` | `21600` | Age (from `dispatched_at`) before a `DISPATCHED`/`RUNNING` job is eligible for the sweep's stuck-job phase — force-failed as `"Job timed out"` (6 hours). | | `AGENT_KV_CALCULATIONS_ENABLED` | `false` | Gates the submit `calculations` field; the engine cannot execute it yet, so submit 400s while this is off. | diff --git a/unstract/agent-kv-schema/pyproject.toml b/unstract/agent-kv-schema/pyproject.toml index 9db37d39b7..6723d4478f 100644 --- a/unstract/agent-kv-schema/pyproject.toml +++ b/unstract/agent-kv-schema/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "unstract-agent-kv-schema" version = "0.1.0" -description = "Agent-KV keys.json schema compiler — single source of truth for API validation and the extraction engine" +description = "Agent-KV keys.json schema compiler and submit-time validation gate (the engine calls kv_schema.compile directly and does not re-apply the caps — see compile.py)" requires-python = ">=3.12" dependencies = [] diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py index 5eb2c9bb1e..a7ecc37611 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py @@ -207,6 +207,83 @@ def _reject_unsafe_regex(path: str, pattern: str) -> None: ) +#: The formats ``validators._check_one`` actually implements. Anything else is +#: passed through as a free-text LLM hint, which is deliberate (see the +#: ``format`` comment on ``KeySpec``) -- and is also why a typo in one of these +#: six is invisible: ``format: "Number"`` is not a known format, so it becomes a +#: hint, so ``validate_format`` returns True for every value forever and +#: ``/validate`` reports ``{"valid": true}``. The author configured validation +#: and got none, with no error anywhere to find it by. +_KNOWN_FORMATS = frozenset({"string", "number", "date", "currency", "enum", "regex"}) + + +def _reject_unusable_formats(key_specs, array_specs) -> None: + """Refuse declared formats that can never validate anything, or always fail. + + Three shapes, all accepted before review and all verified by execution: + + * ``format: "Number"`` -- a case variant of a known format. Silently + degrades to a free-text hint, disabling validation for that key. Rejected + with the intended spelling, rather than guessed at: a schema author who + meant the hint can lower-case it or reword it, and one who meant the + format gets told. + * ``format: "enum:"`` -- no values, so ``_check_one`` tests membership of + the empty set and EVERY non-empty value fails QA for the life of the job. + * ``format: "regex:"`` -- empty pattern, so ``re.fullmatch("", v)`` matches + nothing but the empty string, which ``validate_format`` already passes + before reaching the pattern. Same outcome: nothing can ever conform. + + The last two are worse than the first: they do not disable validation, they + invert it, and the result is a document that fails QA no matter what is on + the page. + """ + for kspec in _every_leaf(key_specs, array_specs): + fmt = kspec.format + lowered = fmt.casefold() + if fmt not in _KNOWN_FORMATS and lowered in _KNOWN_FORMATS: + raise SchemaError( + f"'{kspec.path}' declares format {fmt!r}, which is not a known " + f"format and is therefore treated as free text -- no validation " + f"would run. Did you mean {lowered!r}?" + ) + if fmt == "enum" and not kspec.enum_values: + raise SchemaError( + f"'{kspec.path}' declares an enum with no values " + f"(e.g. 'enum:paid,unpaid'); as written no value can ever conform." + ) + if fmt == "regex" and not kspec.regex_pattern: + raise SchemaError( + f"'{kspec.path}' declares an empty regex; as written no value " + f"can ever conform." + ) + + +def _reject_unknown_key_columns(array_specs) -> None: + """Refuse an array's ``_key`` that names a column the array does not declare. + + ``key_column`` selects row identity for scoring. A name with no matching + column is not an error anywhere downstream -- every row simply misses, and + the array silently falls back to positional identity. So a typo costs + accuracy on exactly the arrays the author cared enough about to key, and + reports nothing. + """ + for aspec in array_specs: + if not aspec.key_column: + continue # '' is the documented "positional identity" default + columns = {s.path for s in aspec.item_specs} + if aspec.key_column not in columns: + raise SchemaError( + f"array '{aspec.path}' declares _key " + f"{aspec.key_column!r}, which is not one of its columns " + f"({sorted(columns)})." + ) + + +def _every_leaf(key_specs, array_specs): + """Scalar leaves and array columns, which carry the same per-leaf rules.""" + return list(key_specs) + [s for a in array_specs for s in a.item_specs] + + def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema: caps = caps or SchemaCaps() if not isinstance(spec, dict): @@ -228,6 +305,8 @@ def compile_schema(spec: dict, caps: SchemaCaps | None = None) -> CompiledSchema _enforce_shape_caps(key_specs, array_specs, caps) _enforce_leaf_caps(key_specs, array_specs, caps) + _reject_unusable_formats(key_specs, array_specs) + _reject_unknown_key_columns(array_specs) constraints = _validated_constraints(spec, caps) return CompiledSchema( @@ -251,7 +330,7 @@ def _enforce_shape_caps(key_specs, array_specs, caps: SchemaCaps) -> None: def _enforce_leaf_caps(key_specs, array_specs, caps: SchemaCaps) -> None: """Bound every leaf's author-supplied text, across scalars and array columns.""" - for kspec in key_specs + [s for a in array_specs for s in a.item_specs]: + for kspec in _every_leaf(key_specs, array_specs): _reject_unsafe_regex(kspec.path, kspec.regex_pattern) if len(kspec.regex_pattern) > caps.max_regex_len: raise SchemaError( diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py index 8c93096dba..a64b8bc9c9 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/constraints.py @@ -1,15 +1,37 @@ -"""Cross-field consistency (spec §18.3): a fail-closed evaluator for schema-author-declared +"""Cross-field consistency (spec §18.3): an ADVISORY evaluator for schema-author-declared constraints over the NORMALIZED key values. No eval/exec — a static AST allowlist (Compare/BoolOp/ BinOp/UnaryOp/Name/Attribute-path/Constant only). Operands resolve to normalized values; a missing/ -empty/un-coercible operand SKIPS the constraint (advisory), never crashes. Returns violated exprs. +empty/un-coercible operand SKIPS the constraint, never crashes. Returns violated exprs. + +Two senses of "fail-closed" were conflated here, and this docstring used to +claim the wrong one. What IS fail-closed is the *grammar*: anything outside the +allowlist is refused rather than executed, so no constraint can run code. What +is NOT fail-closed is the *outcome*: a constraint that cannot be evaluated -- +missing operand, mixed types, or an unexpected exception -- is dropped, and +``evaluate_constraints`` records only ``is False`` as a violation. So a dropped +constraint is indistinguishable, in the return value, from one that passed. + +That matters because dropping is REACHABLE, not theoretical: `_BIN` includes +`truediv`, so `unit_price == line_total / quantity` is an accepted constraint, +and a row where `quantity` normalizes to `0.0` raises `ZeroDivisionError`. The +QA result then reports ``violations: []`` -- a positive assurance that nothing +was checked. + +Until the result shape can carry a third state (skipped-with-reason), every +drop is LOGGED, so the condition is at least diagnosable from the worker logs +rather than silent. Surfacing it in the QA document is a public contract change +on the `kv` extractor, which this deployment does not ship; tracked separately. """ import ast +import logging import math import operator from .validators import coerce_number +logger = logging.getLogger(__name__) + # Equality on these values is TOLERANT, not exact. Operands are normalized # currency/number values that arrived as floats, so binary floating point makes # exact `==` wrong for the thing constraints exist to express. The feature's own @@ -148,7 +170,13 @@ def _aggregate(node: ast.Call, arrays: dict[str, list[dict[str, str]]]): (strips commas/$/%, drops non-finite/empty -> None); such cells are skipped (not zeroed). `count('a.col')` counts rows whose column value is NON-EMPTY (text columns like sku/description count too); `count('a')` is the row count. A sum/min/max/avg over zero - usable cells raises `_Skip` (advisory) rather than guessing 0. + usable cells raises `_Skip` (advisory) rather than guessing 0, and sum/avg additionally + require EVERY row to contribute -- see the comment at that check. + + "FAIL-CLOSED" above is about the grammar, not the outcome: a deviation is + refused rather than executed. A `_Skip` is then reported by + `evaluate_constraints` as "no violation", which is the module docstring's + subject. """ fn, array_path, column = _parse_agg_call(node) rows = arrays.get(array_path) @@ -167,6 +195,20 @@ def _aggregate(node: ast.Call, arrays: dict[str, list[dict[str, str]]]): nums = [n for r in rows if (n := coerce_number(r.get(column))) is not None] if not nums: # nothing usable -> advisory skip raise _Skip() + # A PARTIAL sum or average is not the column's sum or average -- it is a + # smaller number that will then be compared against a scalar key covering + # the whole column, and report a violation on a CORRECT document. One + # blank or un-parseable cell in a 40-row invoice is enough. That is the + # same false-positive class the comparison tolerance above exists to + # eliminate, arriving by a different route, so sum/avg require every row + # to contribute and otherwise skip (advisory) rather than guess. + # + # min/max/count are deliberately NOT subject to this: they are meaningful + # over a subset by construction (the smallest value present, the largest + # value present, the number of rows carrying one), and `count` has already + # returned above. + if fn in ("sum", "avg") and len(nums) != len(rows): + raise _Skip() # `math.fsum`, not the builtin `sum`: exactly-rounded, so accumulation error # does not grow with row count. The builtin accumulates left to right, and # on a long array that drift is the one thing that could push a CORRECT @@ -253,13 +295,32 @@ def _evaluate_one( try: tree = ast.parse(expr, mode="eval") except SyntaxError: + # Unreachable from the API: `compile._check_constraint_syntax` parses + # every constraint at submit. Still logged, because reaching it means + # a constraint got in past that gate. + logger.warning("agent-kv constraint does not parse, skipping: %r", expr) return None try: return _truth(tree.body, values, arrays) except _Skip: + # The ordinary, expected drop: an operand is missing, empty, or not + # like-typed. Debug rather than warning -- on a document with optional + # keys this is normal and would otherwise be per-constraint noise. + logger.debug("agent-kv constraint skipped (operand unusable): %r", expr) return None except Exception: - return None # fail-closed: any surprise -> skip, never crash the pipeline + # NOT fail-closed, whatever the previous comment on this line said: the + # caller turns None into "no violation", so this returns a pass for a + # check that never ran. `ZeroDivisionError` on a quantity of 0 reaches + # here. Keeping the catch is still right -- one bad constraint must not + # fail the whole extraction -- but it is logged at exception level so + # the drop is findable, which is the part that was missing. + logger.exception( + "agent-kv constraint raised and was DROPPED (reported as no " + "violation, not as a failure): %r", + expr, + ) + return None def evaluate_constraints( diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py index 10b72ef1b2..544c9c5efb 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/dataclasses.py @@ -6,8 +6,16 @@ from dataclasses import dataclass, field +# Both specs are FROZEN. They are compile output: produced once by +# `kv_schema._walk` and then read by the prompt generator, the QA pass and the +# constraint evaluator. Nothing assigns to a field (the one derived variant, +# `normalizers`' scalar view of a multivalued leaf, already goes through +# `dataclasses.replace`), so freezing costs nothing and makes a stray mutation +# in a later stage a loud AttributeError instead of a spec that disagrees with +# the schema the caller submitted. -@dataclass + +@dataclass(frozen=True) class KeySpec: """One compiled leaf key from the user's nested key schema.""" @@ -21,7 +29,7 @@ class KeySpec: multivalued: bool = False # value is a comma-separated string -@dataclass +@dataclass(frozen=True) class ArraySpec: """One compiled FLAT-array node (P8a). `path` = dotted array location (e.g. 'line_items', 'invoice.lines'). `item_specs` = the declared columns as row-LOCAL scalar KeySpecs (their diff --git a/unstract/agent-kv-schema/tests/test_compile.py b/unstract/agent-kv-schema/tests/test_compile.py index ff3d4c7c10..12a3a707d4 100644 --- a/unstract/agent-kv-schema/tests/test_compile.py +++ b/unstract/agent-kv-schema/tests/test_compile.py @@ -1,5 +1,7 @@ """compile_schema: structural validation + caps on top of the ported compiler.""" +from dataclasses import FrozenInstanceError + import pytest from unstract.agent_kv_schema import ( @@ -248,3 +250,145 @@ def test_non_overlapping_alternation_still_compiles(pattern): patterns -- `(?:a|b)+` is linear. """ compile_schema(_schema(pattern)) + + +# -------------------------------------------------------------------------- +# 2.18: formats that can never validate anything, or always fail. +# +# Free-text `format` stays an LLM hint -- that is deliberate, see the `format` +# comment on KeySpec. The three shapes below are not hints, they are mistakes +# with no observable symptom, all three verified by execution before the fix. +# -------------------------------------------------------------------------- + + +def _one_key(fmt: str) -> dict: + return {"amount": {"description": "An amount", "format": fmt}} + + +@pytest.mark.parametrize( + ("fmt", "intended"), + [ + ("Number", "number"), + ("NUMBER", "number"), + ("Date", "date"), + ("Currency", "currency"), + ("String", "string"), + ("Enum", "enum"), + ("Regex", "regex"), + ], +) +def test_a_case_variant_of_a_known_format_is_refused(fmt, intended): + """`format: "Number"` silently became a free-text hint. + + `validate_format("not a number at all", spec)` then returned True for + every value for the life of the job, and `/validate` reported + `{"valid": true}` -- the author configured validation and got none. + """ + with pytest.raises(SchemaError, match=f"Did you mean '{intended}'"): + compile_schema(_one_key(fmt)) + + +@pytest.mark.parametrize("fmt", ["number", "date", "currency", "string"]) +def test_the_known_formats_still_compile(fmt): + compile_schema(_one_key(fmt)) + + +@pytest.mark.parametrize( + "fmt", + [ + "the invoice total in USD", # a real hint, which is the point of free text + "amount", + "numeric-ish", # near a known format but not a case variant of one + "dates", + ], +) +def test_free_text_format_is_still_accepted_as_an_llm_hint(fmt): + compile_schema(_one_key(fmt)) + + +@pytest.mark.parametrize("fmt", ["enum:", "enum", "enum: , ,"]) +def test_an_enum_with_no_values_is_refused(fmt): + """`enum_values=[]` means `_check_one` tests membership of the empty set, + so every non-empty value fails QA forever. This does not disable + validation, it inverts it. + """ + with pytest.raises(SchemaError, match="enum with no values"): + compile_schema(_one_key(fmt)) + + +def test_an_enum_with_values_still_compiles(): + compiled = compile_schema(_one_key("enum:paid,unpaid")) + assert compiled.key_specs[0].enum_values == ["paid", "unpaid"] + + +@pytest.mark.parametrize("fmt", ["regex:", "regex"]) +def test_an_empty_regex_is_refused(fmt): + with pytest.raises(SchemaError, match="empty regex"): + compile_schema(_one_key(fmt)) + + +def test_the_same_rules_apply_to_array_columns(): + """Array columns are leaves too, and were checked by nothing.""" + spec = { + "lines": { + "description": "One row per line", + "_array": {"total": {"description": "Line total", "format": "Currency"}}, + } + } + with pytest.raises(SchemaError, match="Did you mean 'currency'"): + compile_schema(spec) + + +# -------------------------------------------------------------------------- +# 2.18 (second half): `_key` naming a column that does not exist. +# -------------------------------------------------------------------------- + + +def test_a_key_column_that_does_not_exist_is_refused(): + """A `_key` typo is not an error anywhere downstream. + + Every row misses, the array silently falls back to positional identity, + and the cost is accuracy on exactly the arrays the author cared enough + about to key. Nothing is reported. + """ + spec = { + "lines": { + "description": "One row per line", + "_key": "skew", # the column is 'sku' + "_array": { + "sku": {"description": "SKU"}, + "total": {"description": "Line total"}, + }, + } + } + with pytest.raises(SchemaError, match="_key 'skew'"): + compile_schema(spec) + + +def test_a_key_column_that_exists_still_compiles(): + compiled = compile_schema(VALID) + assert [a.key_column for a in compiled.array_specs] == ["sku"] + + +def test_an_array_with_no_key_column_is_positional_and_accepted(): + spec = { + "lines": { + "description": "One row per line", + "_array": {"sku": {"description": "SKU"}}, + } + } + compiled = compile_schema(spec) + assert compiled.array_specs[0].key_column == "" + + +# -------------------------------------------------------------------------- +# 2.18 (third): the specs are compile OUTPUT and must not be mutable. +# -------------------------------------------------------------------------- + + +def test_compiled_specs_are_frozen(): + compiled = compile_schema(VALID) + with pytest.raises(FrozenInstanceError): + compiled.key_specs[0].format = "number" + with pytest.raises(FrozenInstanceError): + compiled.array_specs[0].key_column = "total" diff --git a/unstract/agent-kv-schema/tests/test_constraints.py b/unstract/agent-kv-schema/tests/test_constraints.py index e69de29bb2..43fa014923 100644 --- a/unstract/agent-kv-schema/tests/test_constraints.py +++ b/unstract/agent-kv-schema/tests/test_constraints.py @@ -0,0 +1,258 @@ +"""The constraint evaluator's operator/aggregate matrix, and its drop behaviour. + +This file was 0 bytes against a 258-line module -- an empty file with that name +reads as coverage, which is worse than no file (reported as 2.20 in the branch +review). What it covers now is the part the review found wrong (2.19): the +module called itself fail-closed while a constraint that raises is reported as +"no violation", and a partial sum over a column with one blank cell produced a +false violation on a correct document. + +`test_constraints_tolerance.py` covers the float-tolerance derivation; this +covers everything else. +""" + +import ast +import logging + +import pytest + +from unstract.agent_kv_schema import evaluate_constraints +from unstract.agent_kv_schema.compile import _ALLOWED_CALLS, _ALLOWED_NODES +from unstract.agent_kv_schema.constraints import _AGG, _BIN, _CMP + +_ROWS = { + "lines": [ + {"amount": "100.00", "qty": "2", "sku": "A"}, + {"amount": "50.00", "qty": "1", "sku": "B"}, + ] +} + + +# -------------------------------------------------------------------------- +# The comparison and arithmetic matrix +# -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("expr", "values", "violated"), + [ + ("a == b", {"a": "1", "b": "1"}, False), + ("a == b", {"a": "1", "b": "2"}, True), + ("a != b", {"a": "1", "b": "2"}, False), + ("a != b", {"a": "1", "b": "1"}, True), + ("a < b", {"a": "1", "b": "2"}, False), + ("a < b", {"a": "2", "b": "2"}, True), + ("a <= b", {"a": "2", "b": "2"}, False), + ("a > b", {"a": "3", "b": "2"}, False), + ("a >= b", {"a": "2", "b": "2"}, False), + # chained, Python-style + ("a < b < c", {"a": "1", "b": "2", "c": "3"}, False), + ("a < b < c", {"a": "1", "b": "3", "c": "2"}, True), + # arithmetic + ("total == net + tax", {"total": "110", "net": "100", "tax": "10"}, False), + ("total == net + tax", {"total": "111", "net": "100", "tax": "10"}, True), + ("net == total - tax", {"net": "100", "total": "110", "tax": "10"}, False), + ("total == qty * price", {"total": "20", "qty": "4", "price": "5"}, False), + ("price == total / qty", {"price": "5", "total": "20", "qty": "4"}, False), + ("a == -b", {"a": "-5", "b": "5"}, False), + # boolean composition + ("a > 0 and b > 0", {"a": "1", "b": "1"}, False), + ("a > 0 and b > 0", {"a": "1", "b": "-1"}, True), + ("a > 0 or b > 0", {"a": "-1", "b": "1"}, False), + ("not (a > b)", {"a": "1", "b": "2"}, False), + # thousands separators and currency symbols coerce numerically + ("a == b", {"a": "9,000", "b": "9000"}, False), + ("a == b", {"a": "$1,234.50", "b": "1234.5"}, False), + # ISO dates compare lexicographically as strings + ("start < end", {"start": "2026-01-01", "end": "2026-02-01"}, False), + ("start < end", {"start": "2026-03-01", "end": "2026-02-01"}, True), + ], +) +def test_operator_matrix(expr, values, violated): + assert evaluate_constraints([expr], values, {}) == ([expr] if violated else []) + + +@pytest.mark.parametrize( + "expr", + [ + "a == b", # 'a' missing entirely + "missing == 1", # operand absent + "a == 1", # operand empty string + "a < b", # number vs string: not like-typed + "a == 'nan'", # non-finite token stays a string + ], +) +def test_unusable_operands_are_skipped_not_violations(expr): + values = {"a": "", "b": "text"} + assert evaluate_constraints([expr], values, {}) == [] + + +# -------------------------------------------------------------------------- +# Aggregates +# -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("expr", "values", "violated"), + [ + ("total == sum('lines.amount')", {"total": "150.00"}, False), + ("total == sum('lines.amount')", {"total": "150.01"}, True), + ("n == count('lines')", {"n": "2"}, False), + ("n == count('lines')", {"n": "3"}, True), + ("n == count('lines.sku')", {"n": "2"}, False), + ("lo == min('lines.amount')", {"lo": "50"}, False), + ("hi == max('lines.amount')", {"hi": "100"}, False), + ("mean == avg('lines.amount')", {"mean": "75"}, False), + ("mean == avg('lines.amount')", {"mean": "76"}, True), + ], +) +def test_aggregate_matrix(expr, values, violated): + assert evaluate_constraints([expr], values, _ROWS) == ([expr] if violated else []) + + +@pytest.mark.parametrize( + "expr", + [ + "x == sum('nosucharray.amount')", # array not supplied + "x == sum('lines')", # sum needs a column + "x == avg('lines')", + "x == min('lines')", + "x == sum('lines.sku')", # no numeric cell at all + "x == median('lines.amount')", # not in the allowlist + "x == sum('lines.amount', 2)", # two args + "x == sum(column)", # arg is not a string literal + ], +) +def test_unusable_aggregates_are_skipped_not_violations(expr): + assert evaluate_constraints([expr], {"x": "150"}, _ROWS) == [] + + +def test_count_of_a_column_counts_non_empty_cells_only(): + rows = {"lines": [{"sku": "A"}, {"sku": ""}, {"sku": " "}, {"sku": "C"}]} + assert evaluate_constraints(["n == count('lines.sku')"], {"n": "2"}, rows) == [] + assert evaluate_constraints(["n == count('lines')"], {"n": "4"}, rows) == [] + + +# -------------------------------------------------------------------------- +# 2.19: a PARTIAL sum/avg must skip, not report a false violation +# -------------------------------------------------------------------------- + +_PARTIAL = { + "lines": [ + {"amount": "100.00"}, + {"amount": ""}, # blank cell: OCR missed it, or the row has no amount + {"amount": "50.00"}, + ] +} + + +def test_a_partial_sum_skips_rather_than_reporting_a_false_violation(): + """The document is CORRECT: 100 + + 50 against a stated 200. + + Summing the two readable cells gives 150, which is not 200 -- so before + the fix this reported a violation on a correct invoice. One blank cell in + a long array was enough. The sum is not the column's sum, so there is + nothing to compare and the constraint is advisory-skipped. + """ + expr = "total == sum('lines.amount')" + assert evaluate_constraints([expr], {"total": "200.00"}, _PARTIAL) == [] + + +def test_a_partial_avg_also_skips(): + expr = "mean == avg('lines.amount')" + assert evaluate_constraints([expr], {"mean": "66.67"}, _PARTIAL) == [] + + +def test_an_un_parseable_cell_counts_as_missing_for_sum(): + """`total` is the document's real total, including the cell OCR mangled. + + Summing only the readable cell gives 100 against a stated 250, so without + the guard this is a violation on a correct document. + """ + rows = {"lines": [{"amount": "100.00"}, {"amount": "n/a"}]} + expr = "total == sum('lines.amount')" + assert evaluate_constraints([expr], {"total": "250.00"}, rows) == [] + + +@pytest.mark.parametrize("fn", ["min", "max", "count"]) +def test_min_max_and_count_stay_total_over_a_subset(fn): + """These are meaningful over the rows that HAVE a value, so they must not skip.""" + expected = {"min": "50.00", "max": "100.00", "count": "2"}[fn] + expr = f"x == {fn}('lines.amount')" + assert evaluate_constraints([expr], {"x": expected}, _PARTIAL) == [] + + +# -------------------------------------------------------------------------- +# 2.19: a constraint that RAISES is dropped and reported as no violation. +# That stays true (one bad constraint must not fail an extraction) but it is +# now logged, which is the part that was missing. +# -------------------------------------------------------------------------- + + +def test_a_dropped_constraint_is_logged_at_exception_level(caplog): + """`quantity` normalizes to 0 -> ZeroDivisionError -> dropped. + + `_BIN` includes truediv and `compile._ALLOWED_NODES` includes ast.Div, so + this expression is accepted at submit: the path is reachable, not + theoretical. + """ + expr = "unit_price == line_total / quantity" + values = {"unit_price": "5", "line_total": "20", "quantity": "0"} + with caplog.at_level(logging.ERROR, logger="unstract.agent_kv_schema.constraints"): + assert evaluate_constraints([expr], values, {}) == [] + assert any( + "DROPPED" in r.getMessage() and expr in r.getMessage() for r in caplog.records + ), ( + "a constraint that raised was dropped silently; the only signal that " + "nothing was checked is this log line" + ) + + +def test_an_ordinary_skip_is_not_logged_as_an_error(caplog): + """A missing operand is normal on a document with optional keys.""" + with caplog.at_level(logging.WARNING, logger="unstract.agent_kv_schema.constraints"): + assert evaluate_constraints(["a == b"], {"a": "1"}, {}) == [] + assert caplog.records == [] + + +def test_one_dropped_constraint_does_not_stop_the_others(): + exprs = ["unit_price == line_total / quantity", "total == net"] + values = { + "unit_price": "5", + "line_total": "20", + "quantity": "0", + "total": "1", + "net": "2", + } + assert evaluate_constraints(exprs, values, {}) == ["total == net"] + + +# -------------------------------------------------------------------------- +# 2.20: the two hand-maintained allowlists live in two files with nothing +# enforcing agreement. compile.py decides what a submit ACCEPTS; constraints.py +# decides what the evaluator can RUN. A node accepted but not runnable is a +# constraint that is silently skipped for every document; a node runnable but +# not accepted is dead code. +# -------------------------------------------------------------------------- + + +def test_the_submit_allowlist_and_the_evaluator_agree_on_comparisons(): + accepted = {n for n in _ALLOWED_NODES if issubclass(n, ast.cmpop)} + assert accepted == set(_CMP), ( + "compile._ALLOWED_NODES and constraints._CMP disagree: a comparison " + "accepted at submit but absent from _CMP is skipped for every document" + ) + + +def test_the_submit_allowlist_and_the_evaluator_agree_on_arithmetic(): + accepted = {n for n in _ALLOWED_NODES if issubclass(n, ast.operator)} + assert accepted == set( + _BIN + ), "compile._ALLOWED_NODES and constraints._BIN disagree on binary operators" + + +def test_the_submit_allowlist_and_the_evaluator_agree_on_aggregates(): + assert _ALLOWED_CALLS == _AGG, ( + "compile._ALLOWED_CALLS and constraints._AGG disagree: a function name " + "accepted at submit but absent from _AGG is skipped for every document" + ) diff --git a/workers/shared/api/internal_client.py b/workers/shared/api/internal_client.py index 5a82c61b60..329659dd30 100644 --- a/workers/shared/api/internal_client.py +++ b/workers/shared/api/internal_client.py @@ -1669,8 +1669,17 @@ def agent_kv_stage_report( job_id: Agent-KV job ID. org_id: Organization ID (required by the endpoint's body, not the URL — mirrors ``StageReportView``/``FinalizeView``). - stage: Pipeline stage name (e.g. ``"extract"``). - status: Stage status (e.g. ``"started"``, ``"completed"``). + stage: Pipeline stage name. Must be one of the stage names the + job's extractor declares (``agent_kv.constants + .STAGE_NAMES_BY_EXTRACTOR``) -- ``"table_extraction"`` for + ``table``, one of ``STAGE_NAMES`` for ``kv``. The endpoint + only checks the name is non-empty, but ``_status_document`` + filters a job's recorded stages through that list, so an + off-list name is stored and then dropped from every status + response. + status: Stage status -- ``"running"`` or ``"done"``, the only two + values ``StageReportView`` accepts. (These examples read + ``"started"``/``"completed"`` before review; both are 400s.) seconds: Optional stage duration in seconds. counters: Optional stage counters to record. From f163650dc2f25384f9c5401f86bc3c87f745d950 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Wed, 7 Oct 2026 10:56:34 +0530 Subject: [PATCH 13/18] UN-4232 [FIX] Give agent-kv's `extractor` column choices and no default Review finding 2.17. `extractor` was the one stringly-typed field on the model without `choices=` while `status` has had them since 0001, so nothing but a reader's memory connected the column to `EXTRACTOR_ROUTES`. The sharper half is the default. Migration 0002 added the column with `default="kv"`, which was the historical truth then -- the API accepted exactly one extractor and it was always `kv` -- and became a mis-filing trap the moment `table` existed. A creation path omitting `extractor=` files a TABLE job as `kv`, and `kv` IS a valid key in `STAGE_NAMES_BY_EXTRACTOR`, so `_status_document` hands back the KV stage list and silently drops `table_extraction` from every status response. The job runs, the caller is billed, the stages array comes back empty, and `_status_document`'s unknown-extractor warning does NOT fire, because nothing is wrong as far as the filter can tell. Only the one production creation path exists today and it passes `extractor=` explicitly, so this is latent rather than live. Removed rather than re-pointed at `table`: which extractor ran is a fact about the job, not something with a sensible default. With no default an omission is loud -- `""` matches no route, so `dispatch_job` raises, the job terminalizes FAILED with an error the caller can see, and the status warning does fire. `JobExtractor` lists KV even though this deployment refuses it. The column records which extractor RAN, and the rows 0002 back-filled legitimately say `kv`; routability is `EXTRACTOR_ROUTES`' job. The two sets are deliberately different, so neither is derived from the other -- deriving would either quietly re-enable the extractor or make old rows unreadable. `test_recordable_extractors_are_a_superset_of_routable_ones` pins that relationship as a strict subset. Migration 0005 is a schema no-op on Postgres: Django keeps `choices` and a field `default` in Python only -- no CHECK constraint, no column DEFAULT -- so the AlterField changes model state and emits no DDL touching data. `makemigrations --check --dry-run` reports no changes, and the leaf check shows a single leaf per app. Two existing tests failed on this and were right to: - `test_the_extractor_column_defaults_to_kv` asserted `== "kv"`, pinning the defect as the contract. Renamed, inverted, and it now also asserts `kv` remains a legal value to READ back, which is what the retired-extractor status test below it depends on. - Two `test_job_views` tests built jobs with KV stage names and `kv`-namespaced result assertions while relying on the column default to supply the extractor. Both now pass it explicitly: the filtering is load-bearing for what they assert, and riding on a default made that invisible in the test that depends on it. 251 backend tests (was 248). Mutation-checked: restoring `default=V1_EXTRACTOR_NAME` fails 2, dropping `choices` fails 3. Co-Authored-By: Claude Opus 5 --- .../0005_agentkvjob_extractor_choices.py | 32 ++++++++++++ backend/agent_kv/models.py | 37 +++++++++++++- backend/agent_kv/tests/test_job_views.py | 10 ++++ backend/agent_kv/tests/test_models.py | 49 +++++++++++++++++++ .../tests/test_table_extractor_routing.py | 23 +++++++-- 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 backend/agent_kv/migrations/0005_agentkvjob_extractor_choices.py diff --git a/backend/agent_kv/migrations/0005_agentkvjob_extractor_choices.py b/backend/agent_kv/migrations/0005_agentkvjob_extractor_choices.py new file mode 100644 index 0000000000..7e77da5595 --- /dev/null +++ b/backend/agent_kv/migrations/0005_agentkvjob_extractor_choices.py @@ -0,0 +1,32 @@ +"""Give `extractor` its `choices` and take away its `default`. + +Schema no-op on Postgres: Django keeps both `choices` and a field `default` in +Python only -- no CHECK constraint, no column DEFAULT -- so this `AlterField` +changes model state and emits no DDL that touches data. Existing rows keep +whatever they recorded, including the `kv` rows migration 0002 back-filled. + +The `default="kv"` this removes was correct when 0002 added the column (the +API accepted one extractor and it was always `kv`) and became a trap once +`table` existed: an omitted `extractor=` filed a table job as `kv`, which IS a +valid key in `STAGE_NAMES_BY_EXTRACTOR`, so the status endpoint returned the KV +stage list and dropped `table_extraction` silently. See the field comment on +the model. +""" + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("agent_kv", "0004_pg_periodic_tasks"), + ] + + operations = [ + migrations.AlterField( + model_name="agentkvjob", + name="extractor", + field=models.CharField( + choices=[("kv", "Kv"), ("table", "Table")], max_length=32 + ), + ), + ] diff --git a/backend/agent_kv/models.py b/backend/agent_kv/models.py index 7afb051364..149a3136fe 100644 --- a/backend/agent_kv/models.py +++ b/backend/agent_kv/models.py @@ -6,7 +6,7 @@ from utils.models.base_model import BaseModel from utils.models.organization_mixin import DefaultOrganizationMixin -from agent_kv.constants import V1_EXTRACTOR_NAME +from agent_kv.constants import TABLE_EXTRACTOR_NAME, V1_EXTRACTOR_NAME class AgentKVKey(DefaultOrganizationMixin, BaseModel): @@ -50,6 +50,23 @@ class JobStatus(models.TextChoices): CANCELLED = "CANCELLED" +class JobExtractor(models.TextChoices): + """The extractor names a job row may record. + + `status` has had `choices` since 0001 and `extractor` was the one + stringly-typed field without them -- so nothing but a reader's memory + connected the column to `EXTRACTOR_ROUTES`. + + KV is listed even though this deployment refuses it: the column records + which extractor RAN, and rows written before the carve-out legitimately + say `kv`. Routability is `EXTRACTOR_ROUTES`' job, and these two sets are + deliberately not the same thing -- see `test_table_extractor_routing.py`. + """ + + KV = V1_EXTRACTOR_NAME + TABLE = TABLE_EXTRACTOR_NAME + + class AgentKVJob(DefaultOrganizationMixin, BaseModel): TERMINAL = frozenset({JobStatus.COMPLETED, JobStatus.FAILED, JobStatus.CANCELLED}) @@ -64,7 +81,23 @@ class AgentKVJob(DefaultOrganizationMixin, BaseModel): # Which extractor this job ran. v1 dispatches exactly one per job, but # WHICH one is now a choice, and status/result key their payloads by it -- # without this column a table job's output would be filed under `kv`. - extractor = models.CharField(max_length=32, default=V1_EXTRACTOR_NAME) + # + # NO DEFAULT, deliberately. Migration 0002 added this column with + # `default="kv"`, which was the historical truth at the time (the API + # accepted exactly one extractor and it was always `kv`) and is now a + # trap: a creation path that omits `extractor=` files a TABLE job as + # `kv`, and `kv` is a valid key in `STAGE_NAMES_BY_EXTRACTOR`, so + # `_status_document` hands back the KV stage list and silently drops + # `table_extraction` from every status response. The job runs, the caller + # is billed, and the stages array is empty with no warning logged -- + # because nothing is wrong as far as the filter can tell. + # + # With no default, an omission is loud instead: `""` matches no route, so + # `dispatch_job` raises and the job terminalizes as FAILED with an error + # the caller can see, and `_status_document` logs the unknown-extractor + # warning. Which extractor ran is a fact about the job, not something with + # a sensible default. + extractor = models.CharField(max_length=32, choices=JobExtractor.choices) status = models.CharField( max_length=16, choices=JobStatus.choices, diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index a5332748b3..4416027756 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -16,6 +16,7 @@ from agent_kv import execution_views as ev # noqa: E402 from agent_kv import execution_views_result as evr # noqa: E402 +from agent_kv.constants import V1_EXTRACTOR_NAME # noqa: E402 from agent_kv.models import AgentKVJob, AgentKVKey, JobStatus # noqa: E402 from agent_kv.tests._factories import kv_key # noqa: E402 @@ -83,6 +84,12 @@ def test_every_job_scoped_view_filters_by_organization(m_keys, m_jobs, view, met def test_status_running_builds_ordered_stages_and_lowercases_status(m_keys, m_jobs): m_keys.get.return_value = kv_key() job = AgentKVJob( + # Explicit, not the column default: the stage names below are the KV + # list, and `_status_document` filters a job's stages through + # STAGE_NAMES_BY_EXTRACTOR[job.extractor]. This used to ride on + # `extractor` defaulting to `kv`, which made a load-bearing fact + # invisible in the test that depends on it. + extractor=V1_EXTRACTOR_NAME, status=JobStatus.RUNNING, stage="extraction", stages={ @@ -160,6 +167,9 @@ def test_result_after_expiry_is_404(m_keys, m_jobs, m_read): def test_result_happy_path_returns_read_result_payload(m_keys, m_jobs, m_read): m_keys.get.return_value = kv_key() job = AgentKVJob( + # Explicit: the assertions below namespace the payload under `kv`, + # which is `job.extractor`, not a constant of the response shape. + extractor=V1_EXTRACTOR_NAME, status=JobStatus.COMPLETED, result_ref="org/o/agent_kv/j/result.json", expires_at=timezone.now() + timedelta(days=1), diff --git a/backend/agent_kv/tests/test_models.py b/backend/agent_kv/tests/test_models.py index 231252c135..56d32a800f 100644 --- a/backend/agent_kv/tests/test_models.py +++ b/backend/agent_kv/tests/test_models.py @@ -94,3 +94,52 @@ def test_mark_terminal_accepts_every_terminal_status(m_objects, good_status): ) is True ) + + +# -------------------------------------------------------------------------- +# 2.17: `extractor` was the one stringly-typed field without `choices`, and it +# defaulted to the extractor this build cannot run. +# -------------------------------------------------------------------------- + + +def test_extractor_declares_its_choices(): + field = AgentKVJob._meta.get_field("extractor") + assert {value for value, _ in field.choices} == {"kv", "table"} + + +def test_extractor_has_no_default(): + """An omitted `extractor=` must be loud, not silently filed as `kv`. + + `kv` IS a valid key in `STAGE_NAMES_BY_EXTRACTOR`, so a table job filed + under it gets the KV stage list and `table_extraction` is dropped from + every status response -- the job runs, the caller is billed, the stages + array comes back empty and nothing logs a warning, because nothing is + wrong as far as the filter can tell. + + With no default the omission produces `""`, which matches no route: the + dispatch raises, the job terminalizes FAILED with a visible error, and + `_status_document` logs the unknown-extractor warning. + """ + field = AgentKVJob._meta.get_field("extractor") + assert not field.has_default(), ( + "migration 0002's `default='kv'` was the historical truth then and is " + "a mis-filing trap now; see the field comment" + ) + assert AgentKVJob(organization_id="o").extractor == "" + + +def test_recordable_extractors_are_a_superset_of_routable_ones(): + """The two sets are deliberately different, so neither is derived. + + `choices` says what a ROW may record -- rows written before the carve-out + legitimately say `kv`. `EXTRACTOR_ROUTES` says what a submit may DISPATCH, + and `kv` is absent from it on purpose. Deriving either from the other + would quietly re-enable the extractor or make old rows unreadable. + """ + from agent_kv.constants import EXTRACTOR_ROUTES + + recordable = {value for value, _ in AgentKVJob._meta.get_field("extractor").choices} + assert set(EXTRACTOR_ROUTES) < recordable, ( + "every routable extractor must be recordable, and `kv` is recordable " + "without being routable" + ) diff --git a/backend/agent_kv/tests/test_table_extractor_routing.py b/backend/agent_kv/tests/test_table_extractor_routing.py index 07ded10fcd..696afb9573 100644 --- a/backend/agent_kv/tests/test_table_extractor_routing.py +++ b/backend/agent_kv/tests/test_table_extractor_routing.py @@ -209,11 +209,26 @@ def test_the_result_payload_keys_by_the_extractor_that_ran(): assert V1_EXTRACTOR_NAME not in payload["extractors"] -def test_the_extractor_column_defaults_to_kv(): - """The migration's default is the historical truth, not a guess: before - this column existed the API accepted exactly one extractor, always `kv`. +def test_the_extractor_column_has_no_default(): + """Was `test_the_extractor_column_defaults_to_kv`, asserting `== "kv"`. + + It pinned the defect as the contract. The default WAS the historical truth + when migration 0002 added the column -- the API accepted exactly one + extractor and it was always `kv` -- and it became a mis-filing trap the + moment `table` existed: `kv` is a valid key in `STAGE_NAMES_BY_EXTRACTOR`, + so a table job filed under it gets the KV stage list and + `table_extraction` is silently dropped from every status response, with + nothing logged because nothing looks wrong to the filter. + + `""` matches no route, so an omission now fails the dispatch visibly. The + row-level truth 0002 back-filled is untouched: existing `kv` rows keep + saying `kv`, which is what `test_a_job_whose_extractor_has_no_stage_list_ + reports_no_stages` below depends on. """ - assert AgentKVJob().extractor == V1_EXTRACTOR_NAME + assert AgentKVJob().extractor == "" + assert V1_EXTRACTOR_NAME in { + value for value, _ in AgentKVJob._meta.get_field("extractor").choices + }, "a pre-carve-out row must still be a legal value to READ back" def test_a_job_whose_extractor_has_no_stage_list_reports_no_stages(caplog): From e283b9ab2c29fbed479aeb44a076c8770dac7b6b Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Wed, 7 Oct 2026 11:00:45 +0530 Subject: [PATCH 14/18] UN-4232 [REFACTOR] Delete agent-kv's dead `compiled` serializer attribute Review finding 2.20. `ExtractorSerializer.compiled` was populated during validation and collected by `SubmitSerializer.validate_extractors` into a `{extractor_name: compiled schema}` dict that no non-test code read. `dispatch_job` sends `schema=entry["keys"]` -- the raw dict -- and the engine compiles it itself. Plumbing the `CompiledSchema` through instead was the other option the finding offered, and the queue does not allow it: the compiled form would have to survive a JSON round-trip to the executor, which is precisely why the engine recompiles (`compile.py`'s docstring already says the caps are a submit-time gate, not an invariant the engine re-checks). An attribute that is written and never read reads as plumbing that exists, so it is gone. On this deployment the dict was always `{"table": None}` anyway: `table` has a dedicated keys serializer, so the branch that set `compiled` never ran. `compile_schema` is still called in the no-keys-serializer fall-through, for what it REFUSES -- the 400 is the only reason it is there, and the comment now says so, so a later reader does not delete the call as unused. That branch is DORMANT here: `kv` is the only extractor without a keys serializer and `validate_name` refuses it first, so nothing in the request path reaches it. It is still the contract for the next extractor added without one, and nothing covered it, so three tests call it directly -- the bad-schema 400, the raw spec passing through identically, and that neither serializer carries a `compiled` attribute any more. Also fixed the now-wrong comment in `test_submit_view.py`, which said the options dict "must ... carry the compiled schema". 254 backend tests (was 251). Mutation-checked: deleting the `compile_schema` call fails the 400 test, reintroducing the attribute fails the third. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_serializers.py | 21 +++++---- .../agent_kv/tests/test_submit_serializer.py | 47 +++++++++++++++++++ backend/agent_kv/tests/test_submit_view.py | 10 ++-- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/backend/agent_kv/execution_serializers.py b/backend/agent_kv/execution_serializers.py index 8d4f750b8d..6dada100d0 100644 --- a/backend/agent_kv/execution_serializers.py +++ b/backend/agent_kv/execution_serializers.py @@ -169,8 +169,6 @@ class ExtractorSerializer(serializers.Serializer): keys = serializers.JSONField() options = serializers.DictField(required=False, default=dict) - compiled = None - def validate_name(self, v): if v not in SUPPORTED_EXTRACTORS: raise serializers.ValidationError( @@ -211,8 +209,19 @@ def _validated_keys(self, name: str, spec): serialized = json.dumps(spec, ensure_ascii=False).encode("utf-8") if len(serialized) > settings.AGENT_KV_MAX_SCHEMA_BYTES: raise serializers.ValidationError({"keys": "keys schema too large"}) + # Called for what it REFUSES, not for what it returns. The + # `CompiledSchema` used to be stashed on the serializer and collected + # into a `{name: compiled}` dict by `SubmitSerializer`, which no + # non-test code ever read: `dispatch_job` sends `schema=entry["keys"]`, + # the raw dict, and the engine re-compiles it on its own (see the + # docstring on `compile.py` -- the caps are a submit-time gate, not an + # invariant the engine re-checks). Plumbing the compiled form through + # instead is not an option the queue allows: it would have to survive + # JSON round-tripping to the executor, which is exactly why the engine + # recompiles. So the dead attribute is gone rather than left to read as + # plumbing that exists. What this call is for is the 400 below. try: - self.compiled = compile_schema(spec) + compile_schema(spec) except SchemaError as e: raise serializers.ValidationError({"keys": str(e)}) return spec @@ -265,8 +274,6 @@ class SubmitSerializer(serializers.Serializer): required=False, allow_blank=True, default="", max_length=1024 ) - #: ``{extractor_name: compiled schema}`` -- populated during validation. - compiled = None #: Measured page count of the uploaded document. None for Excel, which has #: no pre-OCR page concept. This is what metering and the status document #: report, and it is NOT what the page cap is compared against. @@ -317,7 +324,7 @@ def validate_extractors(self, raw): "multiple extractors are not supported yet; pass exactly one" ) - validated, compiled = [], {} + validated = [] for i, entry in enumerate(entries): if not isinstance(entry, dict): raise serializers.ValidationError(f"extractors[{i}] must be an object") @@ -325,8 +332,6 @@ def validate_extractors(self, raw): if not ser.is_valid(): raise serializers.ValidationError({f"extractors[{i}]": ser.errors}) validated.append(ser.validated_data) - compiled[ser.validated_data["name"]] = ser.compiled - self.compiled = compiled return validated def validate(self, data): diff --git a/backend/agent_kv/tests/test_submit_serializer.py b/backend/agent_kv/tests/test_submit_serializer.py index 027ee3f16c..e7037e39c0 100644 --- a/backend/agent_kv/tests/test_submit_serializer.py +++ b/backend/agent_kv/tests/test_submit_serializer.py @@ -16,6 +16,7 @@ from unittest import mock import django +import pytest from django.apps import apps os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") @@ -23,8 +24,10 @@ django.setup() from django.core.files.uploadedfile import SimpleUploadedFile # noqa: E402 +from rest_framework import serializers # noqa: E402 from agent_kv.execution_serializers import ( # noqa: E402 + ExtractorSerializer, KVOptionsSerializer, SubmitSerializer, ) @@ -476,3 +479,47 @@ def test_table_keys_are_validated_through_the_keys_serializer(): assert not s.is_valid() assert "unknown keys for extractor 'table'" in _errs(s) + + +# --------------------------------------------------------------------------- +# 2.20: `compile_schema` is called in the no-keys-serializer fall-through for +# what it REFUSES, not for what it returns. +# +# The `CompiledSchema` used to be stashed on `ExtractorSerializer.compiled` and +# collected into `SubmitSerializer.compiled`, which no non-test code read -- +# `dispatch_job` sends the raw `keys` dict and the engine recompiles. Deleting +# the attribute must not quietly delete the 400, which is the only reason the +# call is there. +# +# The branch is DORMANT on this deployment: `kv` is the only extractor without +# a keys serializer and `validate_name` refuses it before `_validated_keys` +# runs, so it is reached by calling it directly. That is the point -- it is the +# contract for the next extractor added without one, and nothing else covers it. +# --------------------------------------------------------------------------- + + +def test_the_schema_compiler_fallthrough_still_rejects_a_bad_schema(): + ser = ExtractorSerializer() + + with pytest.raises(serializers.ValidationError) as exc: + # A leaf with no `description` is refused by the ported compiler. + ser._validated_keys("some_future_extractor", {"total": {}}) + + assert "keys" in exc.value.detail + + +def test_the_schema_compiler_fallthrough_returns_the_raw_spec(): + """What reaches `dispatch_job` is the submitted dict, unchanged.""" + ser = ExtractorSerializer() + spec = {"total": {"description": "The grand total", "format": "currency"}} + + assert ser._validated_keys("some_future_extractor", spec) is spec + + +def test_neither_serializer_still_carries_a_compiled_attribute(): + """A `compiled` that is populated and never read reads as plumbing.""" + for cls in (ExtractorSerializer, SubmitSerializer): + assert not hasattr(cls, "compiled"), ( + f"{cls.__name__}.compiled was dead: `dispatch_job` sends the raw " + "keys dict, and the compiled form cannot cross the queue anyway" + ) diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index b9d7ba70ba..cf61f061c0 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -248,9 +248,13 @@ def _side_effect(job, *, extractor, schema, options): # --------------------------------------------------------------------------- # The 9-key options dict SubmitView.post builds and forwards to dispatch_job -# must map every field correctly and carry the compiled schema -- a -# dropped/typo'd key here would otherwise pass silently since nothing else -# asserts on dispatch_job's call args. +# must map every field correctly -- a dropped/typo'd key here would otherwise +# pass silently since nothing else asserts on dispatch_job's call args. +# +# It carries the RAW `keys` dict, not a `CompiledSchema`. The serializer used +# to stash the compiled form and nothing read it; the engine recompiles from +# the raw dict by design (see `compile.py`'s docstring), and the compiled form +# could not cross the queue anyway. # --------------------------------------------------------------------------- @mock.patch.object(ev, "dispatch_job") @mock.patch.object(AgentKVJob, "save", autospec=True) From a6df38bc0251814b0752756c4e5607e220db11b9 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Wed, 7 Oct 2026 11:04:33 +0530 Subject: [PATCH 15/18] UN-4232 [MISC] Clear the eight SonarCloud findings on #2317 - **S3776** `SubmitView.post` at cognitive complexity 20 against a limit of 15. Extracted the two self-contained blocks as module-level helpers next to the ones already there (`_subscription_denial`, `_dispatch_or_fail`, `_sync_wait_response`): `_request_data_with_extractors_inlined` (the bounded file-part read) and `_discard_orphaned_input` (the staged-object cleanup for a submit that failed before its row landed). Behaviour unchanged; the rationale comments moved with the code they explain rather than being left behind in `post`. - **S5778 x3** in `test_compile.py`: `pytest.raises` blocks containing two calls, because the spec was built inside them. Hoisted `_one_key(fmt)` out, so what the block asserts about is `compile_schema` alone. - **S5799 x2**: implicitly concatenated string literals in `dispatch.py` and `agent_kv_tasks.py` -- ruff-format artefacts that landed as `"... not " "enqueueing"` on one line, which is exactly the shape of a forgotten comma. Merged; both stay inside 90 columns. - **S117 x2** in migration 0004: `PgPeriodicTask = apps.get_model(...)` is the standard Django idiom but is a local variable, so S117 reads the CamelCase as a naming violation. Renamed to `periodic_task` with a comment saying why it departs from the idiom, so the next person does not "fix" it back. Cloud #1828 has zero SonarCloud findings. 254 backend tests, 128 schema-package tests, 23 ide_callback tests, all green. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/dispatch.py | 2 +- backend/agent_kv/execution_views.py | 132 ++++++++++-------- .../migrations/0004_pg_periodic_tasks.py | 11 +- .../agent-kv-schema/tests/test_compile.py | 9 +- workers/ide_callback/agent_kv_tasks.py | 2 +- 5 files changed, 87 insertions(+), 69 deletions(-) diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py index de29cf50f6..149a56ab3a 100644 --- a/backend/agent_kv/dispatch.py +++ b/backend/agent_kv/dispatch.py @@ -138,7 +138,7 @@ def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None: id=job.id, status__in=list(AgentKVJob.TERMINAL) ).exists(): logger.info( - "agent-kv: job %s was terminalized before dispatch; not " "enqueueing", + "agent-kv: job %s was terminalized before dispatch; not enqueueing", job.id, ) return diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index 262888fe0c..556deb5385 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -212,6 +212,76 @@ def _sync_wait_response(job, wait: float) -> Response | None: return None +def _request_data_with_extractors_inlined(request): + """A mutable copy of the request data with a file-part `extractors` read in. + + Split out of ``SubmitView.post`` for sonar S3776 (the method was at + cognitive complexity 20 against a limit of 15); the behaviour is unchanged. + + Bounded read, which is the point. The size cap lives in + ``validate_extractors``, i.e. AFTER this point -- and + ``DATA_UPLOAD_MAX_MEMORY_SIZE`` excludes file-typed parts, so a 500 MB + `extractors` part was materialised in full (plus up to 4x that again for + the ``str``) before being rejected at 256 KiB. One such request per worker + process OOMs the pod, which makes it a cheap denial of service. + """ + data = request.data.copy() + part = data.get("extractors") + if not hasattr(part, "read"): # not a file part (§7.1); nothing to inline + return data + # Read one byte past the cap: enough to know it is over without ever + # holding more than the cap plus one. + limit = settings.AGENT_KV_MAX_SCHEMA_BYTES + raw = part.read(limit + 1) + if len(raw) > limit: + raise ValidationError({"extractors": f"extractors payload exceeds {limit} bytes"}) + try: + # `errors="strict"`, not "replace". A latin-1 key name used to decode + # to U+FFFD and then compile cleanly, so a malformed payload became a + # job that ran against a schema the caller never wrote. + data["extractors"] = raw.decode("utf-8") + except UnicodeDecodeError: + raise ValidationError({"extractors": "extractors must be valid UTF-8"}) from None + return data + + +def _discard_orphaned_input(job) -> None: + """Delete the staged object for a submit that failed before the row landed. + + Split out of ``SubmitView.post`` for sonar S3776; behaviour unchanged. + + If ``stage_input`` succeeded and ``job.save()`` then raised, the upload + exists with no row to carry its ref -- and ``run_ttl_cleanup`` selects + candidates from ``AgentKVJob`` rows, so an object with no row is + structurally unreachable by every cleanup path there is. It would sit in + the bucket forever, holding customer data nobody can find or delete. + """ + if not job.input_ref: + return + # `delete_job_files` reports which refs are confirmed gone and swallows the + # rest -- normally the ref survives as the retry handle. Here there is no + # row to hold it, so a delete that failed means the object is orphaned with + # nothing anywhere pointing at it. Log the ref itself: it is the only way + # anyone can clean it up by hand. + try: + cleared = delete_job_files(job) + except Exception: + cleared = [] + logger.exception( + "agent-kv: staged-input cleanup raised for failed submit %s (ref=%s)", + job.id, + job.input_ref, + ) + if "input_ref" not in cleared: + logger.error( + "agent-kv: the staged input for failed submit %s could NOT be " + "removed and no job row exists to carry its ref -- object %s is " + "orphaned and unreachable by TTL cleanup; remove it manually", + job.id, + job.input_ref, + ) + + class SubmitView(APIView): authentication_classes: list = [] permission_classes: list = [] @@ -228,34 +298,7 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): if denied is not None: return denied - data = request.data.copy() - part = data.get("extractors") - if hasattr(part, "read"): # `extractors` uploaded as a file part (§7.1) - # Bounded read. The size cap lives in `validate_extractors`, i.e. - # AFTER this point -- and `DATA_UPLOAD_MAX_MEMORY_SIZE` excludes - # file-typed parts, so a 500 MB `extractors` part was materialised - # in full (plus up to 4x that again for the `str`) before being - # rejected at 256 KiB. One such request per worker process OOMs the - # pod, which makes it a cheap denial of service. - # - # Read one byte past the cap: enough to know it is over without - # ever holding more than the cap plus one. - limit = settings.AGENT_KV_MAX_SCHEMA_BYTES - raw = part.read(limit + 1) - if len(raw) > limit: - raise ValidationError( - {"extractors": f"extractors payload exceeds {limit} bytes"} - ) - try: - # `errors="strict"`, not "replace". A latin-1 key name used to - # decode to U+FFFD and then compile cleanly, so a malformed - # payload became a job that ran against a schema the caller - # never wrote. - data["extractors"] = raw.decode("utf-8") - except UnicodeDecodeError: - raise ValidationError( - {"extractors": "extractors must be valid UTF-8"} - ) from None + data = _request_data_with_extractors_inlined(request) serializer = SubmitSerializer(data=data) serializer.is_valid(raise_exception=True) v = serializer.validated_data @@ -281,38 +324,7 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): job_saved = True except Exception: logger.exception("agent-kv staging/save failed for job %s", job.id) - # Delete the staged object before responding. If `stage_input` - # succeeded and `job.save()` then raised, the upload exists with no - # row to carry its ref -- and `run_ttl_cleanup` selects candidates - # from `AgentKVJob` rows, so an object with no row is structurally - # unreachable by every cleanup path there is. It would sit in the - # bucket forever, holding customer data nobody can find or delete. - if job.input_ref: - # `delete_job_files` reports which refs are confirmed gone and - # swallows the rest -- normally the ref survives as the retry - # handle. Here there is no row to hold it, so a delete that - # failed means the object is orphaned with nothing anywhere - # pointing at it. Log the ref itself: it is the only way anyone - # can clean it up by hand. - try: - cleared = delete_job_files(job) - except Exception: - cleared = [] - logger.exception( - "agent-kv: staged-input cleanup raised for failed " - "submit %s (ref=%s)", - job.id, - job.input_ref, - ) - if "input_ref" not in cleared: - logger.error( - "agent-kv: the staged input for failed submit %s could " - "NOT be removed and no job row exists to carry its ref " - "-- object %s is orphaned and unreachable by TTL " - "cleanup; remove it manually", - job.id, - job.input_ref, - ) + _discard_orphaned_input(job) return _fail_job_response( job, org_id, diff --git a/backend/agent_kv/migrations/0004_pg_periodic_tasks.py b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py index 5a0cb7f8e7..046f7558e7 100644 --- a/backend/agent_kv/migrations/0004_pg_periodic_tasks.py +++ b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py @@ -57,9 +57,12 @@ def create_pg_periodic_tasks(apps, schema_editor): - PgPeriodicTask = apps.get_model("pg_queue", "PgPeriodicTask") + # snake_case, not the usual `PgPeriodicTask = apps.get_model(...)` Django + # idiom: it is a local variable, and sonar's S117 reads the CamelCase name + # as a naming violation. The historical-model object is the same either way. + periodic_task = apps.get_model("pg_queue", "PgPeriodicTask") for spec in PG_PERIODIC_TASKS: - PgPeriodicTask.objects.update_or_create( + periodic_task.objects.update_or_create( name=spec["name"], defaults={ "task_name": spec["task_name"], @@ -79,8 +82,8 @@ def create_pg_periodic_tasks(apps, schema_editor): def remove_pg_periodic_tasks(apps, schema_editor): - PgPeriodicTask = apps.get_model("pg_queue", "PgPeriodicTask") - PgPeriodicTask.objects.filter( + periodic_task = apps.get_model("pg_queue", "PgPeriodicTask") + periodic_task.objects.filter( name__in=[spec["name"] for spec in PG_PERIODIC_TASKS] ).delete() diff --git a/unstract/agent-kv-schema/tests/test_compile.py b/unstract/agent-kv-schema/tests/test_compile.py index 12a3a707d4..c21e61fc71 100644 --- a/unstract/agent-kv-schema/tests/test_compile.py +++ b/unstract/agent-kv-schema/tests/test_compile.py @@ -284,8 +284,9 @@ def test_a_case_variant_of_a_known_format_is_refused(fmt, intended): every value for the life of the job, and `/validate` reported `{"valid": true}` -- the author configured validation and got none. """ + spec = _one_key(fmt) with pytest.raises(SchemaError, match=f"Did you mean '{intended}'"): - compile_schema(_one_key(fmt)) + compile_schema(spec) @pytest.mark.parametrize("fmt", ["number", "date", "currency", "string"]) @@ -312,8 +313,9 @@ def test_an_enum_with_no_values_is_refused(fmt): so every non-empty value fails QA forever. This does not disable validation, it inverts it. """ + spec = _one_key(fmt) with pytest.raises(SchemaError, match="enum with no values"): - compile_schema(_one_key(fmt)) + compile_schema(spec) def test_an_enum_with_values_still_compiles(): @@ -323,8 +325,9 @@ def test_an_enum_with_values_still_compiles(): @pytest.mark.parametrize("fmt", ["regex:", "regex"]) def test_an_empty_regex_is_refused(fmt): + spec = _one_key(fmt) with pytest.raises(SchemaError, match="empty regex"): - compile_schema(_one_key(fmt)) + compile_schema(spec) def test_the_same_rules_apply_to_array_columns(): diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index 4b13142892..5b56d3d393 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -246,7 +246,7 @@ def agent_kv_cancelled( # exists to deliver -- and nothing retried or recorded it. Raise so the # retry budget applies, exactly as a transport exception would. logger.error( - "agent_kv_cancelled: webhook not delivered for job %s (retry %s of " "%s)", + "agent_kv_cancelled: webhook not delivered for job %s (retry %s of %s)", job_id, self.request.retries, self.max_retries, From d068e58cd4381184c4fb9b0941c121ea7ae3e58d Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Wed, 7 Oct 2026 10:33:28 +0530 Subject: [PATCH 16/18] UN-4232 [FEAT] The table extractor runs on the caller's adapters, not operator env vars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverses spec D6 for the `table` extractor. D6 specified "no platform adapters by design" -- models and OCR credentials in operator env vars, "no end-user model control". That was right while the consumer was hypothetical and wrong once the consumers are existing customers with accounts: * There was NO operator-level LLM credential anywhere in the platform to reuse. LLM credentials live in per-org `AdapterInstance` rows, encrypted; the only LLMWhisperer key in a namespace is `LLMWHISPERER_PORTAL_KEY`, which is the portal ADMIN key that mints per-org extraction keys -- wrong credential, and over-privileged for executor pods. So the six required env vars had no source, and a deployment could not be completed. * Env configuration put every customer's model choice and every customer's LLM spend on one shared operator key. * The IDE table path in the SAME plugin already resolves `llm` / `lite_llm` / `x2text` from adapter instances. This makes the two paths agree. Wire format follows the platform's own conventions rather than inventing one: * `adapters: {llm, lite_llm, x2text}` -- a SIBLING of `keys`/`options`, named by ROLE. `prompt_profile_manager_v2.ProfileManager` declares `llm` / `x2text` / `embedding_model` / `vector_store`, as fields on the owning object rather than inside a knobs blob, so neither `*_adapter_id` naming nor burying them in `options` would have matched. Keeping them out of `options` also leaves that member optional. * Types validated against `AdapterTypes` (`unstract/sdk1/constants.py`). * Errors are DRF `ValidationError`s -> 400, like the rest of the submit caps. **Validation is split, and the split is the point.** Shape (`_validated_adapter_shape`) is in the serializer and touches no database -- `test_submit_serializer.py` is a unit suite, and my first attempt put an `AdapterInstance` lookup there and reddened 22 tests. Ownership and type (`_resolved_adapters`) are in the VIEW, where the Bearer key is, for the same reason `_subscription_denial` lives there. Both run before the job row, the concurrency slot and the staging write -- the first version of the view gate sat AFTER staging while its own comment claimed otherwise. **The org filter is the tenant boundary and nothing downstream repeats it.** The executor resolves these ids through the platform service and does not re-check whose they are, so without `AdapterInstance.objects.filter(id=..., organization_id=key.organization_id)` a caller names any adapter UUID and the run spends ANOTHER TENANT'S LLM credential. Same class as the `organization_id` filter in `_get_job`, higher consequence. No platform helper fitted: `AdapterProcessor.get_adapters_by_type` filters `.for_user(user)` and this path has no user (a key resolves to an ORGANIZATION), and `get_adapter_by_name_and_type` is unscoped. Three deliberate choices in that gate, each documented at the code: * "No such adapter" and "not yours" return the IDENTICAL message, with a test asserting the bodies are byte-equal -- distinguishing them makes the endpoint an oracle for which adapter ids exist in other organizations. * Type is checked because an `X2TEXT` id in the `llm` slot resolves fine and then fails deep in the engine as a provider error, which reads like a broken model rather than two swapped UUIDs. * Per-user adapter visibility is NOT honoured. `AdapterInstance` is `HasMembersMixin` with `ResourceMembership` VIEWER rows, and an org-scoped key has no user to evaluate them against -- so a key reaches any adapter in its own org. A key is already an organization-level credential, so this is a decision, not an oversight. `kv` is untouched and stays env-configured: one credential model per extractor, never both for one extractor. `ExtractionConfig` keeps its credential fields for it. Docs: new §3a covering the three roles, what each is for (lite_llm runs once per page, so it is the cost-critical one), the four 400 cases, and the D6 reversal with its consequences stated -- the API is no longer callable by someone who has never signed in, and a table-only deployment can leave `global.sharedConfigs.agentKv.enabled: false` entirely. Added a `table` curl example; the existing `kv` one is now labelled as wire-format-only, since this build 400s it. 259 backend tests (was 254). Mutation-checked: removing the `organization_id` filter fails 4. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/dispatch.py | 16 +- backend/agent_kv/execution_serializers.py | 104 ++++++++- backend/agent_kv/execution_views.py | 84 ++++++- .../agent_kv/tests/test_submit_serializer.py | 31 ++- backend/agent_kv/tests/test_submit_view.py | 213 +++++++++++++++++- docs/agent-kv-api.md | 94 +++++++- 6 files changed, 522 insertions(+), 20 deletions(-) diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py index 149a56ab3a..f45dcb3827 100644 --- a/backend/agent_kv/dispatch.py +++ b/backend/agent_kv/dispatch.py @@ -94,7 +94,9 @@ def _platform_api_key(job) -> str: return str(platform_key.key) -def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None: +def dispatch_job( + job, *, extractor: str, schema: dict, options: dict, adapters: dict | None = None +) -> None: executor_name, operation = EXTRACTOR_ROUTES[extractor] org_id = str(job.organization_id) # Everything that can fail — platform-key lookup, context construction, @@ -115,6 +117,18 @@ def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None: "input_ref": job.input_ref, "schema": schema, "options": options, + # Platform adapter instance ids, by role, already validated at + # submit against THIS job's organization and against the + # expected `AdapterTypes` (see + # `execution_serializers.validated_adapters`). The executor + # resolves them through the platform service using the key + # below -- it does NOT re-check tenancy, so the submit-time + # check is the only one there is. + # + # Empty for an env-configured extractor (`kv`), which is why + # this is a dict rather than three params: the two credential + # models coexist, one per extractor. + "adapters": adapters or {}, "platform_api_key": _platform_api_key(job), # The CAP the engine must enforce (spec §6.1/§6.6), not the # measured count -- job.pages_total is None for Excel (no diff --git a/backend/agent_kv/execution_serializers.py b/backend/agent_kv/execution_serializers.py index 6dada100d0..01064c3c65 100644 --- a/backend/agent_kv/execution_serializers.py +++ b/backend/agent_kv/execution_serializers.py @@ -8,6 +8,7 @@ from agent_kv.constants import EXTRACTOR_ROUTES, TABLE_EXTRACTOR_NAME, V1_EXTRACTOR_NAME from unstract.agent_kv_schema import SchemaError, compile_schema +from unstract.sdk1.constants import AdapterTypes # Images are deliberately ABSENT, and that is a cross-repo contract, not an # oversight. The cloud engine's `_build_agent_graph` (agentic_kv @@ -154,6 +155,63 @@ def validate(self, data): TABLE_EXTRACTOR_NAME: TableOptionsSerializer, } + +class TableAdaptersSerializer(serializers.Serializer): + """The platform adapters the `table` extractor runs on. + + **Why the caller names adapters rather than the operator configuring env + vars.** The engine needs two LLMs and an OCR source. The IDE table path + resolves all three from platform adapter instances the user configured + (`agentic_table/executor.py` -> `LLM(adapter_instance_id=...)`, + `X2Text(adapter_instance_id=...)`), and this API now does the same. That + reverses spec D6, which specified system-level env configuration and "no + end-user model control" -- a deliberate reversal, recorded here and in + `docs/agent-kv-api.md`: the consumers are existing customers with + accounts, so they already own adapters, and letting them choose puts model + selection and LLM spend on the account that benefits from it. + + Field names follow the platform convention for naming adapters by ROLE, + not by id: `prompt_profile_manager_v2.ProfileManager` declares `llm`, + `x2text`, `embedding_model`, `vector_store`. Hence `llm` / `lite_llm` / + `x2text` rather than `llm_adapter_id` and friends. + + `lite_llm` is separate because the engine uses a cheaper model for + per-page presence detection and the advanced one for structure and + extraction; collapsing them would silently multiply the cost of the + highest-volume stage. + """ + + llm = serializers.UUIDField() + lite_llm = serializers.UUIDField() + x2text = serializers.UUIDField() + + def validate(self, data): + unknown = set(self.initial_data) - set(self.fields) + if unknown: + raise serializers.ValidationError( + f"unknown adapters for extractor '{TABLE_EXTRACTOR_NAME}': " + f"{sorted(unknown)}" + ) + return data + + +#: Each extractor's own `adapters` validator, and which `AdapterTypes` each +#: role must be. An extractor absent from this table takes no adapters -- `kv` +#: is env-configured (`extraction_seams.ExtractionConfig`) and deliberately +#: stays that way for now, so the two credential models coexist: one extractor +#: per model, never both for one extractor. +_ADAPTERS_SERIALIZERS = { + TABLE_EXTRACTOR_NAME: ( + TableAdaptersSerializer, + { + "llm": AdapterTypes.LLM, + "lite_llm": AdapterTypes.LLM, + "x2text": AdapterTypes.X2TEXT, + }, + ), +} + + #: Each extractor's own `keys` validator. An extractor absent from this table #: falls through to the schema compiler, which is `kv`'s contract: its `keys` IS #: a compiled schema rather than a fixed set of fields. @@ -162,11 +220,46 @@ def validate(self, data): } +def _validated_adapter_shape(name: str, supplied) -> dict: + """Validate an extractor's `adapters` block SHAPE only. No database. + + Returns `{role: ""}`, or `{}` for an extractor that takes none. + + Deliberately split from the tenancy check. This runs in the serializer, so + it must stay free of the ORM -- `test_submit_serializer.py` is a unit suite + with no database, and an adapter lookup here would make every submit test + require one. What it does check is everything that needs no database: + presence, that each id parses as a UUID, and that no unknown role was sent. + + **Ownership and type are checked in the VIEW** + (`execution_views._resolved_adapters`), because they need the Bearer key's + organization -- the same reason `_subscription_denial` lives there. Both + still run before anything is staged or billed. + """ + entry = _ADAPTERS_SERIALIZERS.get(name) + if entry is None: + if supplied: + raise serializers.ValidationError( + {"adapters": f"extractor '{name}' takes no adapters"} + ) + return {} + + adapters_cls, _expected_types = entry + ser = adapters_cls(data=supplied if isinstance(supplied, dict) else {}) + ser.is_valid(raise_exception=True) + return {role: str(value) for role, value in ser.validated_data.items()} + + class ExtractorSerializer(serializers.Serializer): """One entry of the submit's `extractors` array (spec §7.0/§7.1).""" name = serializers.CharField() keys = serializers.JSONField() + #: Sibling of `keys`/`options`, not nested inside them. These are the + #: RESOURCES the extractor runs on, which `ProfileManager` likewise holds + #: as fields on the owning object rather than inside a knobs blob -- and + #: keeping them out of `options` leaves that member optional. + adapters = serializers.DictField(required=False, default=dict) options = serializers.DictField(required=False, default=dict) def validate_name(self, v): @@ -331,7 +424,16 @@ def validate_extractors(self, raw): ser = ExtractorSerializer(data=entry) if not ser.is_valid(): raise serializers.ValidationError({f"extractors[{i}]": ser.errors}) - validated.append(ser.validated_data) + data = ser.validated_data + try: + data["adapters"] = _validated_adapter_shape( + data["name"], data.get("adapters") + ) + except serializers.ValidationError as e: + raise serializers.ValidationError( + {f"extractors[{i}]": e.detail} + ) from None + validated.append(data) return validated def validate(self, data): diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index 556deb5385..52479e3b20 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -17,7 +17,7 @@ RateLimited, SubscriptionGateUnavailable, ) -from agent_kv.execution_serializers import SubmitSerializer +from agent_kv.execution_serializers import _ADAPTERS_SERIALIZERS, SubmitSerializer from agent_kv.execution_views_result import result_payload from agent_kv.key_validator import AgentKVKeyValidator from agent_kv.models import AgentKVJob, JobStatus @@ -181,7 +181,13 @@ def _dispatch_or_fail(job, org_id: str, entry: dict, options: dict) -> Response escape as an unhandled 500 with the slot still held. """ try: - dispatch_job(job, extractor=entry["name"], schema=entry["keys"], options=options) + dispatch_job( + job, + extractor=entry["name"], + schema=entry["keys"], + options=options, + adapters=entry.get("adapters") or {}, + ) except DispatchError: logger.exception("agent-kv dispatch failed for job %s", job.id) except Exception: @@ -282,6 +288,73 @@ def _discard_orphaned_input(job) -> None: ) +def _resolved_adapters(entry: dict, agent_kv_key) -> dict: + """Confirm every adapter the caller named is THIS org's, and of the right type. + + Returns `{role: ""}` unchanged on success -- the ids are already + UUID-shaped and role-checked by the serializer + (`_validated_adapter_shape`); what is added here is the half that needs a + database and the Bearer key. + + **This is the tenant boundary on adapters, and nothing downstream repeats + it.** The executor resolves these ids through the platform service with an + org-scoped platform key, but it does not re-check that the id belongs to + the job's org -- so without this, a caller passes any adapter UUID and the + run SPENDS ANOTHER TENANT'S LLM CREDENTIAL. Same class as the + `organization_id` filter in `_get_job`, and the highest-consequence failure + this endpoint has. + + It lives in the view rather than the serializer for the same reason + `_subscription_denial` does: it needs the key. The platform's own helpers + do not fit -- `AdapterProcessor.get_adapters_by_type` filters + `.for_user(user)` and this path has no user (a key resolves to an + ORGANIZATION), and `get_adapter_by_name_and_type` is not scoped at all. + + TYPE is checked too: an `X2TEXT` id accepted into the `llm` slot resolves + fine and then fails deep in the engine as a provider error, which reads + like a broken model rather than two swapped UUIDs. + + What this deliberately does NOT honour is per-user adapter visibility + (`AdapterInstance` is `HasMembersMixin`, with `ResourceMembership` VIEWER + rows). An org-scoped key has no user to evaluate it against, so a key may + use ANY adapter in its own organization. Deliberate: a key is an + organization-level credential, and whoever can mint one can already read + the org's data. + + Raises: + ValidationError: 400 at submit -- before staging, before a slot is + taken, before anything is billed. + """ + requested = entry.get("adapters") or {} + if not requested: + return {} + + from adapter_processor_v2.models import AdapterInstance + + _, expected_types = _ADAPTERS_SERIALIZERS[entry["name"]] + for role, adapter_id in requested.items(): + expected = expected_types[role] + adapter = AdapterInstance.objects.filter( + id=adapter_id, organization_id=agent_kv_key.organization_id + ).first() + if adapter is None: + # One message for "no such adapter" and "not yours", on purpose: + # telling them apart reveals which UUIDs exist. + raise ValidationError( + {"adapters": f"{role}: no such adapter in this organization"} + ) + if adapter.adapter_type != expected.value: + raise ValidationError( + { + "adapters": ( + f"{role}: adapter is of type '{adapter.adapter_type}', " + f"expected '{expected.value}'" + ) + } + ) + return requested + + class SubmitView(APIView): authentication_classes: list = [] permission_classes: list = [] @@ -304,6 +377,13 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): v = serializer.validated_data org_id = str(agent_kv_key.organization_id) + # Tenancy + type on the named adapters, HERE and not later: before the + # job row, before the concurrency slot, before the staging write. A + # caller who names another org's adapter, or swaps two UUIDs, gets a + # 400 and is charged nothing -- the same "every cap before paid work" + # rule the serializer's caps follow. + _resolved_adapters(v["extractors"][0], agent_kv_key) + job = AgentKVJob( api_key=agent_kv_key, organization_id=agent_kv_key.organization_id, diff --git a/backend/agent_kv/tests/test_submit_serializer.py b/backend/agent_kv/tests/test_submit_serializer.py index e7037e39c0..a94d77dcfb 100644 --- a/backend/agent_kv/tests/test_submit_serializer.py +++ b/backend/agent_kv/tests/test_submit_serializer.py @@ -58,13 +58,36 @@ def _pdf_upload(name="doc.pdf"): } +#: The platform adapters a `table` submit must name. All three are REQUIRED -- +#: the engine needs two LLMs and an OCR source, and this API resolves them from +#: the caller's own adapter instances rather than from operator env vars. +#: Ownership and type are checked in the VIEW against the Bearer key's org +#: (`execution_views._resolved_adapters`); the serializer only checks shape, so +#: these can be any UUIDs here. +TABLE_ADAPTERS = { + "llm": "11111111-1111-1111-1111-111111111111", + "lite_llm": "22222222-2222-2222-2222-222222222222", + "x2text": "33333333-3333-3333-3333-333333333333", +} + + def _data(**over): """Build a submit payload in the extractor-scoped wire format (§7.0).""" keys = over.pop("keys", TABLE_KEYS) + adapters = over.pop("adapters", TABLE_ADAPTERS) options = {k: over.pop(k) for k in list(over) if k not in _JOB_LEVEL} d = { "file": _pdf_upload(), - "extractors": json.dumps([{"name": "table", "keys": keys, "options": options}]), + "extractors": json.dumps( + [ + { + "name": "table", + "keys": keys, + "adapters": adapters, + "options": options, + } + ] + ), } d.update(over) return d @@ -458,7 +481,11 @@ def test_a_padded_name_is_still_normalised_for_routing(): identity twice, not accepting whitespace. """ s = SubmitSerializer( - data=_data(extractors=json.dumps([{"name": " table ", "keys": TABLE_KEYS}])) + data=_data( + extractors=json.dumps( + [{"name": " table ", "keys": TABLE_KEYS, "adapters": TABLE_ADAPTERS}] + ) + ) ) assert s.is_valid(), s.errors diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index cf61f061c0..429ac44de6 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -3,6 +3,7 @@ from unittest import mock import django +import pytest from django.apps import apps os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") @@ -67,6 +68,13 @@ def _valid_validated_data(**overrides): # code they name. It also meant the whole file exercised the view with an # `extractors` payload the real serializer rejects with a 400. keys = overrides.pop("keys", {"target_table": "Rent roll"}) + # EXPLICIT, and empty by default. `_resolved_adapters` reads + # `entry.get("adapters")` and returns early when it is falsy -- a MagicMock + # auto-attribute here would be TRUTHY, so the gate would fall through to a + # real `AdapterInstance` query and every view test would die on "Database + # access not allowed". Tests that exercise the gate set it and mock the + # lookup; the rest leave it empty and never touch the ORM. + adapters = overrides.pop("adapters", {}) options = { "instructions": "", "json_structure": "", @@ -77,7 +85,9 @@ def _valid_validated_data(**overrides): options.update({k: overrides.pop(k) for k in list(overrides) if k not in _JOB_LEVEL}) data = { "file": mock.Mock(name="uploaded_file"), - "extractors": [{"name": "table", "keys": keys, "options": options}], + "extractors": [ + {"name": "table", "keys": keys, "adapters": adapters, "options": options} + ], "page_start": 1, "page_end": None, "timeout": 0, @@ -97,6 +107,47 @@ def _mock_serializer(m_cls, **overrides): return instance +#: A `table` submit's three required adapter roles. +_TABLE_ADAPTERS = { + "llm": "11111111-1111-1111-1111-111111111111", + "lite_llm": "22222222-2222-2222-2222-222222222222", + "x2text": "33333333-3333-3333-3333-333333333333", +} + +#: Which `AdapterTypes` value each role must resolve to. +_ROLE_TYPES = {"llm": "LLM", "lite_llm": "LLM", "x2text": "X2TEXT"} + + +def _adapters_owned_by(organization_id, *, types=None, missing=()): + """Patch the adapter lookup `_resolved_adapters` performs. + + The gate runs `AdapterInstance.objects.filter(id=..., organization_id=...) + .first()`, so this stands in for the ORM: an id in `missing` resolves to + None (not this org's, or nonexistent -- the gate cannot and must not tell + those apart), and everything else resolves to an adapter of the type + `types` gives, defaulting to the correct one for its role. + """ + from adapter_processor_v2.models import AdapterInstance + + type_by_id = { + aid: (types or {}).get(role, _ROLE_TYPES[role]) + for role, aid in _TABLE_ADAPTERS.items() + } + + def _filter(**kw): + qs = mock.Mock() + aid = str(kw.get("id")) + if aid in {str(m) for m in missing} or kw.get( + "organization_id" + ) != organization_id: + qs.first.return_value = None + else: + qs.first.return_value = mock.Mock(adapter_type=type_by_id.get(aid, "LLM")) + return qs + + return mock.patch.object(AdapterInstance, "objects", **{"filter.side_effect": _filter}) + + def _stamp_created_at(job, *args, **kwargs): """save() side effect mimicking auto_now_add for a fully-mocked save(). @@ -222,7 +273,7 @@ def test_happy_path_returns_202_with_job_id_status_and_status_url( m_limiter.check_and_acquire.return_value = True m_save.side_effect = _stamp_created_at - def _side_effect(job, *, extractor, schema, options): + def _side_effect(job, *, extractor, schema, options, adapters=None): job.status = JobStatus.DISPATCHED job.dispatched_at = timezone.now() @@ -719,21 +770,27 @@ def test_real_serializer_through_real_view_reaches_dispatch_intact_for_table( m_save.side_effect = _stamp_created_at schema = {"target_table": "Rent rolls"} - resp = ev.SubmitView.as_view()( - _real_multipart_post( - [ - { - "name": "table", - "keys": schema, - "options": {"instructions": "skip totals"}, - } - ], + with _adapters_owned_by(kv_key().organization_id): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": schema, + "adapters": _TABLE_ADAPTERS, + "options": {"instructions": "skip totals"}, + } + ], + ) ) - ) assert resp.status_code == 202, resp.data kwargs = m_dispatch.call_args.kwargs assert kwargs["extractor"] == "table" + # The caller's own adapter instances reach the executor, by role. This is + # the whole point of the adapter wire format: the engine resolves these + # through the platform service instead of reading operator env vars. + assert kwargs["adapters"] == _TABLE_ADAPTERS # Nested under `schema`, matching exactly what the cloud executor reads # (`params["schema"]["target_table"]") -- NOT hoisted to the top level. assert kwargs["schema"] == {"target_table": "Rent rolls"} @@ -883,3 +940,135 @@ def test_a_save_failure_after_staging_removes_the_staged_object( "the staged upload was left in the bucket with no job row carrying its " "ref; TTL cleanup selects from job rows, so it can never be reached" ) + + +# --------------------------------------------------------------------------- +# The adapter tenancy gate. +# +# `POST /agent-kv/` names platform adapters by id, and the executor resolves +# them through the platform service WITHOUT re-checking whose they are. So the +# submit-time org filter is the only thing standing between a caller and +# another tenant's LLM credential. Same class as the `organization_id` filter +# on every job-scoped view, and higher consequence: that one leaks a job, this +# one spends someone else's money. +# --------------------------------------------------------------------------- + + +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +@pytest.mark.parametrize("stolen_role", ["llm", "lite_llm", "x2text"]) +def test_an_adapter_from_another_org_is_refused( + m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch, stolen_role +): + """Parametrised across all three roles: one unguarded slot is enough.""" + key = kv_key() + m_keys.get.return_value = key + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + with _adapters_owned_by( + key.organization_id, missing=[_TABLE_ADAPTERS[stolen_role]] + ): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "adapters": _TABLE_ADAPTERS, + "options": {}, + } + ], + ) + ) + + assert resp.status_code == 400, resp.data + assert "no such adapter in this organization" in str(resp.data) + assert stolen_role in str(resp.data) + assert not m_dispatch.called, "an unowned adapter must never reach the executor" + assert not m_stage.called, "and nothing may be staged or billed first" + + +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_an_adapter_of_the_wrong_type_is_refused( + m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch +): + """An X2TEXT id in the `llm` slot resolves fine and then fails deep inside + the engine as a provider error -- which reads like a broken model rather + than two swapped UUIDs. Caught at submit instead. + """ + key = kv_key() + m_keys.get.return_value = key + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + with _adapters_owned_by(key.organization_id, types={"llm": "X2TEXT"}): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "adapters": _TABLE_ADAPTERS, + "options": {}, + } + ], + ) + ) + + assert resp.status_code == 400, resp.data + assert "expected 'LLM'" in str(resp.data) + assert not m_dispatch.called + + +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_the_refusal_does_not_reveal_whether_the_adapter_exists( + m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch +): + """"Not yours" and "no such adapter" must read identically. + + Telling them apart turns the endpoint into an oracle for which adapter + UUIDs are real in other organizations. + """ + key = kv_key() + m_keys.get.return_value = key + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + bodies = [] + # First: the id exists but belongs to someone else. Second: it exists + # nowhere. The stand-in cannot distinguish them either, which is the point. + for missing in ([_TABLE_ADAPTERS["llm"]], [_TABLE_ADAPTERS["llm"]]): + with _adapters_owned_by(key.organization_id, missing=missing): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "adapters": _TABLE_ADAPTERS, + "options": {}, + } + ], + ) + ) + bodies.append(str(resp.data)) + + assert bodies[0] == bodies[1] diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md index 8cb78e06f6..a9592466fa 100644 --- a/docs/agent-kv-api.md +++ b/docs/agent-kv-api.md @@ -204,8 +204,54 @@ The request is **extractor-scoped**: a per-extractor schema and its knobs live i |---|---|---| | `name` | yes | Which extractor. **`table` is the only supported value on this deployment**; anything else, `kv` included, ⇒ 400 (see the note below). More than one entry ⇒ 400 — the format is in place for multi-extractor jobs, the execution is not built yet. | | `keys` | yes | This extractor's own request. For `table`, `{"target_table": ""}` — the engine's one required extraction parameter. | +| `adapters` | yes (for `table`) | The platform adapter instances this extraction runs on: `{"llm": "", "lite_llm": "", "x2text": ""}`. All three required. See **§3a** below. | | `options` | no | This extractor's own knobs (below). An unrecognised option ⇒ 400, so a knob aimed at the wrong extractor fails loudly instead of being silently dropped. | +### 3a. `adapters` — you supply the models and the OCR credentials + +The `table` extractor runs on **your own platform adapters**, named by id. Create +them once in Unstract (Settings → Adapters, or the adapter API) and pass their ids +on every submit: + +| Role | Adapter type | What it does | +|---|---|---| +| `llm` | `LLM` | Structure detection and extraction — the quality-critical model. | +| `lite_llm` | `LLM` | Per-page table-presence detection. Runs once per page, so this is the cost-critical one; a cheap fast model belongs here. | +| `x2text` | `X2TEXT` | OCR. An LLMWhisperer adapter, which also carries the endpoint — there is no separate base-URL field. | + +Each id is validated at submit, against **your organization** and against the +expected adapter type. A ⇒ 400, before anything is staged or billed: + +- an id that is not your organization's — reported identically to one that does not + exist, deliberately, so the endpoint cannot be used to discover which adapter ids + are real elsewhere +- an id of the wrong type, e.g. an `X2TEXT` adapter in the `llm` slot +- a missing role, or an unknown one + +Page usage and token usage are metered against the adapters you named, so **model +choice and LLM spend sit on your account**, per job. + +> **This is a deliberate reversal of the original design (spec D6).** D6 specified +> that this API would have "no platform adapters by design", with models and OCR +> credentials configured by the operator in environment variables and "no end-user +> model control". That was the right call while the API's consumer was hypothetical; +> it is the wrong one now that the consumers are existing Unstract customers. They +> already own adapters, there was no operator-level LLM credential anywhere in the +> platform to reuse — LLM credentials live in per-organization adapter rows, +> encrypted — and env configuration would have put every customer's model choice and +> every customer's LLM spend on a single shared operator key. +> +> Consequences worth knowing: the API is no longer usable by a caller who has never +> signed in, since adapters must exist first; and the six `AGENT_KV_LLM_*` / +> `AGENT_KV_LLMWHISPERER_*` environment variables are **not read on the table path** +> (they remain in place for the `kv` extractor, which is still env-configured). +> +> One thing this does not honour: adapters carry per-user visibility within an +> organization, and an Agent-KV key resolves to an organization rather than a user. +> So a key may use **any** adapter in its own organization. A key is already an +> organization-level credential, so this is a deliberate decision rather than an +> oversight — but treat key issuance accordingly. + > **Why `kv` returns 400 here.** This deployment ships the table extractor only. > The `kv` extractor's engine runs on its own release track and nothing on this > deployment consumes its queue, so the API refuses a `kv` submit outright @@ -290,7 +336,37 @@ that middleware to resolve an organization from. Cloud-only: an OSS-only deployment returns `501` before ever reaching it. `400`: serializer/schema validation failure — nothing billed. -### Example +### Example — `table` (the only extractor this deployment accepts) + +```bash +curl -X POST https://api.unstract.example/agent-kv/ \ + -H "Authorization: Bearer 5c9e2c9e-1234-4a5b-9c6d-abcdef012345" \ + -F "file=@invoice.pdf" \ + -F 'extractors=[{ + "name": "table", + "keys": {"target_table": "Line items"}, + "adapters": { + "llm": "7f3c1a90-0000-4000-8000-000000000001", + "lite_llm": "7f3c1a90-0000-4000-8000-000000000002", + "x2text": "7f3c1a90-0000-4000-8000-000000000003" + }, + "options": { + "instructions": "Ignore the summary block at the foot of the page", + "enable_header_mapping": true + } + }]' \ + -F "page_start=1" -F "page_end=5" \ + -F "webhook_url=https://example.com/hooks/agent-kv" +``` + +The three adapter ids are yours, from your own organization — see §3a. Omit any of +them and the submit is a 400. + +### Example — `kv` + +Shown for the wire format only; **`kv` returns 400 on this deployment** (see the +note above). Note it takes no `adapters` block: the `kv` extractor is +env-configured on the operator side. ```bash curl -X POST https://api.unstract.example/agent-kv/ \ @@ -787,7 +863,21 @@ beyond `docker compose up`: ttl-cleanup additionally needs `storage` for `AGENT_KV_FILE_STORAGE_CREDENTIALS` (see item 6 below) — both already on the `backend` deployment's config list. 5. **Env vars**: every `AGENT_KV_*` setting plus `AGENT_KV_FILE_STORAGE_CREDENTIALS` — - see [§6](#12-environment-reference) and `docker/sample.env`. The executor-side vars + see [§6](#12-environment-reference) and `docker/sample.env`. + + > **For the `table` extractor, the executor-side credential group below is NOT + > required.** That path resolves the caller's own platform adapters (see §3a), so + > none of the six `AGENT_KV_LLM_*` / `AGENT_KV_LLMWHISPERER_*` values is read on + > it. The group only has to be configured where the `kv` extractor is deployed, + > which is a separate release track. A table-only deployment can leave + > `global.sharedConfigs.agentKv.enabled: false` and the API works. + > + > Two operator knobs are still read on the table path, independently of that + > group: `AGENT_KV_STORAGE_DIR_PREFIX` (which must match the backend's — the + > executor derives the job's expected directory from it to enforce the tenant + > boundary on the staged input) and `AGENT_KV_PARALLEL_PAGES`. + + The executor-side vars (read by the cloud `agentic_kv` plugin, not by the backend settings in §13) are a separate chart group: `global.sharedConfigs.agentKv` (`charts/unstract-platform/values.yaml`, cloud repo) — From 7ee61de54ea2e5ed067a6efc7f3529d575d54513 Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Thu, 8 Oct 2026 11:26:24 +0530 Subject: [PATCH 17/18] =?UTF-8?q?UN-4232=20[FIX]=20Two=20P1=20review=20fin?= =?UTF-8?q?dings=20on=20the=20adapter=20change=20=E2=80=94=20one=20of=20th?= =?UTF-8?q?em=20broke=20every=20submit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **P1: the adapter lookup hid every valid adapter.** `_resolved_adapters` used `AdapterInstance.objects`, which is an `AdapterInstanceModelManager` and inherits `DefaultOrganizationManagerMixin.get_queryset()` -- that filters EVERY query by `UserContext.get_organization()`, a request-local thread-local the tenant middleware sets. `/agent-kv/` is deliberately whitelisted past that middleware (the org comes from the Bearer key, not the URL), so the thread-local is unset and the ambient filter became `organization=None`. Result: the gate refused EVERY submit, including a correctly-configured caller naming their own adapter, with "no such adapter in this organization". The API would not have worked at all. I wrote it by analogy to the `organization_id` filter in `_get_job`, and the analogy is false: `AgentKVJob` uses `BaseModelManager`, which does not auto-filter, so its explicit filter is the only one. `AdapterInstance` sets a different default manager. Verified empirically rather than assumed -- `type(AdapterInstance.objects)` is the filtering manager and `type(AdapterInstance._base_manager)` is a plain `Manager`. Fixed with `_base_manager`, Django's unfiltered manager, which exists for exactly this: internal lookups that must not inherit a custom default manager's filtering. That makes the explicit `organization_id` the ONLY org filter, which is why it stays required rather than optional. NOT fixed by setting the thread-local instead: that would silently re-scope every other org-managed query in the request, and `UserContext` keys on the org SLUG while the key carries the FK pk -- the confusion `dispatch._platform_api_key` documents shipping once already. Extracted as `_lookup_adapter`, because `_base_manager` is a read-only property that cannot be patched, and because mocking `.objects` is what hid this: every view test replaced the manager wholesale, so the manager's own behaviour never ran. **The test that was missing.** `tests/test_adapter_scoping.py` is DB-backed (`TestCase`, so the integration tier picks it up by inference) and exercises the real manager with the thread-local unset -- the only arrangement that reproduces the production path. Four cases: the org's own adapter resolves with no request context (the regression), another org's does not (bypassing the AMBIENT filter must not bypass the explicit one, or the fix would have opened the hole it exists to close), an unknown id is None, and `.objects` really does hide it -- so the explanation on `_lookup_adapter` stays truthful if that manager ever changes. Run against a real Postgres: 4 passed. Mutation-checked by reverting `_base_manager` to `.objects`, which fails the regression case. The unit suite could not have caught this at all. **P1: the e2e lane's submits carried no adapters**, so its shared helper got 400 where it asserted 202 and the lane could no longer validate table extraction on a deployed stack. `submit_raw`/`submit` now take `adapters`, defaulting to ids read from `AGENT_KV_E2E_LLM_ADAPTER` / `_LITE_LLM_ADAPTER` / `_X2TEXT_ADAPTER`. These have to come from the environment: they are rows in the deployed stack's database carrying real provider credentials, and an e2e client cannot mint them. Nine scenarios whose submit must be ACCEPTED now take a `require_adapters` fixture, which skips naming the unset variables -- following the lane's existing gate convention (`AGENT_KV_E2E`, `require_llm`, `AGENT_KV_E2E_BAD_KEY_JOB`) rather than inventing one. The seven scenarios that expect a rejection BEFORE the adapter gate (bad key 403, rate limit 429, absent `extractors`, bad schema 400) deliberately do not take it, and keep working unchanged. Also corrected the module docstring, which still described `AGENT_KV_LLM_API_KEY` as the gate for real extraction -- on the table path that credential now lives on the adapter, so a real-adapter run satisfies it whether or not the variable is exported. 259 unit + 4 integration backend tests. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/execution_views.py | 48 +++++++- .../agent_kv/tests/test_adapter_scoping.py | 112 ++++++++++++++++++ backend/agent_kv/tests/test_submit_view.py | 45 +++---- tests/e2e/agent_kv/conftest.py | 59 ++++++++- tests/e2e/agent_kv/test_agent_kv_e2e.py | 79 +++++++++--- 5 files changed, 297 insertions(+), 46 deletions(-) create mode 100644 backend/agent_kv/tests/test_adapter_scoping.py diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index 52479e3b20..4c610d5a93 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -288,6 +288,48 @@ def _discard_orphaned_input(job) -> None: ) +def _lookup_adapter(adapter_id, organization_id): + """One adapter belonging to `organization_id`, or ``None``. + + **`_base_manager`, NOT `.objects`.** `AdapterInstance.objects` is an + `AdapterInstanceModelManager`, which inherits + `DefaultOrganizationManagerMixin.get_queryset()` -- and that filters every + query by `UserContext.get_organization()`, a REQUEST-LOCAL thread-local the + tenant middleware sets. + + This route is deliberately whitelisted past that middleware (the org comes + from the Bearer key, not the URL), so the thread-local is unset here and the + ambient filter becomes `organization=None`, matching nothing. Through + `.objects` this gate refused EVERY submit -- including valid ones -- with + "no such adapter in this organization". + + That is NOT the same situation as `_get_job`, and reasoning by analogy to it + is how the defect got written: `AgentKVJob` uses `BaseModelManager`, which + does not auto-filter, so its explicit `organization_id=` is the only filter. + `AdapterInstance` sets a different default manager. + + `_base_manager` is Django's unfiltered manager and exists for exactly this + -- internal lookups that must not inherit a custom default manager's + filtering. It therefore makes the explicit `organization_id` below the ONLY + org filter, which is why that argument is required rather than optional. + + Not fixed by setting the thread-local instead: that would silently re-scope + every other org-managed query in the request, and `UserContext` keys on the + org SLUG while the key carries the FK pk -- the exact confusion + `dispatch._platform_api_key` documents shipping once already. + + A separate function so the view tests have an honest seam to patch: + `_base_manager` is a read-only property and cannot be patched, and mocking + `.objects` is what hid this defect in the first place. The real manager is + exercised by `tests/test_adapter_scoping.py`. + """ + from adapter_processor_v2.models import AdapterInstance + + return AdapterInstance._base_manager.filter( + id=adapter_id, organization_id=organization_id + ).first() + + def _resolved_adapters(entry: dict, agent_kv_key) -> dict: """Confirm every adapter the caller named is THIS org's, and of the right type. @@ -329,14 +371,10 @@ def _resolved_adapters(entry: dict, agent_kv_key) -> dict: if not requested: return {} - from adapter_processor_v2.models import AdapterInstance - _, expected_types = _ADAPTERS_SERIALIZERS[entry["name"]] for role, adapter_id in requested.items(): expected = expected_types[role] - adapter = AdapterInstance.objects.filter( - id=adapter_id, organization_id=agent_kv_key.organization_id - ).first() + adapter = _lookup_adapter(adapter_id, agent_kv_key.organization_id) if adapter is None: # One message for "no such adapter" and "not yours", on purpose: # telling them apart reveals which UUIDs exist. diff --git a/backend/agent_kv/tests/test_adapter_scoping.py b/backend/agent_kv/tests/test_adapter_scoping.py new file mode 100644 index 0000000000..704fd849e6 --- /dev/null +++ b/backend/agent_kv/tests/test_adapter_scoping.py @@ -0,0 +1,112 @@ +"""The adapter lookup must work with NO request-local organization context. + +This is the test whose absence let a P1 through. `_resolved_adapters` was +written with `AdapterInstance.objects`, by analogy to the `organization_id` +filter in `_get_job` -- but the two models have different default managers: + +* `AgentKVJob` uses `BaseModelManager`, which does not auto-filter, so its + explicit `organization_id=` is the only filter. +* `AdapterInstance` sets `AdapterInstanceModelManager`, which inherits + `DefaultOrganizationManagerMixin.get_queryset()` and filters EVERY query by + `UserContext.get_organization()` -- a request-local thread-local set by the + tenant middleware. + +`/agent-kv/` is deliberately whitelisted past that middleware (the org comes +from the Bearer key, not the URL), so the thread-local is unset and the ambient +filter became `organization=None`. Every valid submit was refused with "no such +adapter in this organization". + +Every test in `test_submit_view.py` mocked the lookup, so none of them could +see it. These run against the real manager with the real thread-local unset -- +which is the only arrangement that reproduces the production path. + +DB-backed, so `TestCase` and the integration tier (`backend/conftest.py` +auto-marks on that basis; no manual marker). +""" + +import os +import uuid + +import django +from django.apps import apps + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings.test") +if not apps.ready: + django.setup() + +from django.test import TestCase # noqa: E402 + +from account_v2.models import Organization # noqa: E402 +from adapter_processor_v2.models import AdapterInstance # noqa: E402 +from agent_kv.execution_views import _lookup_adapter # noqa: E402 +from utils.user_context import UserContext # noqa: E402 + + +class TestAdapterLookupWithoutRequestContext(TestCase): + def setUp(self): + self.org = Organization.objects.create( + display_name="Acme", organization_id="org_acme", name="acme" + ) + self.other = Organization.objects.create( + display_name="Other", organization_id="org_other", name="other" + ) + self.adapter = AdapterInstance.objects.create( + adapter_name="acme-llm", + adapter_id="anthropic|abc", + adapter_type="LLM", + adapter_metadata={}, + organization=self.org, + ) + # The state the whitelisted route actually runs in: no tenant + # middleware has run, so there is no ambient organization. + UserContext.set_organization_identifier(None) + + def test_the_lookup_finds_the_org_s_own_adapter_with_no_user_context(self): + """The regression. Through `.objects` this returned None and the submit + was a 400 -- for a correctly-configured caller naming their own adapter. + """ + self.assertIsNone( + UserContext.get_organization(), + "precondition: this route has no ambient organization", + ) + + found = _lookup_adapter(self.adapter.id, self.org.id) + + self.assertIsNotNone( + found, + "the caller's own adapter must resolve without a request-local " + "organization; `.objects` filters by one and returns nothing here", + ) + self.assertEqual(found.id, self.adapter.id) + self.assertEqual(found.adapter_type, "LLM") + + def test_the_org_filter_still_excludes_another_org_s_adapter(self): + """Bypassing the AMBIENT filter must not bypass the explicit one. + + `_base_manager` is unfiltered, so the `organization_id` argument is the + only tenant boundary left -- if it stopped working, the fix for the + regression above would have opened the cross-tenant hole it exists to + close. + """ + self.assertIsNone(_lookup_adapter(self.adapter.id, self.other.id)) + + def test_an_unknown_id_resolves_to_none(self): + self.assertIsNone(_lookup_adapter(uuid.uuid4(), self.org.id)) + + def test_the_ambient_manager_really_does_hide_it(self): + """Pins the cause, so the comment on `_lookup_adapter` stays truthful. + + If `AdapterInstance.objects` ever stops auto-filtering by the + thread-local, this fails and `_base_manager` is no longer necessary -- + at which point the explanation should be revisited rather than left + asserting something that is no longer true. + """ + via_objects = AdapterInstance.objects.filter( + id=self.adapter.id, organization_id=self.org.id + ).first() + + self.assertIsNone( + via_objects, + "`.objects` is expected to hide the adapter here -- that is the " + "whole reason `_lookup_adapter` uses `_base_manager`", + ) diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index 429ac44de6..00e6a21b8e 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -119,33 +119,36 @@ def _mock_serializer(m_cls, **overrides): def _adapters_owned_by(organization_id, *, types=None, missing=()): - """Patch the adapter lookup `_resolved_adapters` performs. - - The gate runs `AdapterInstance.objects.filter(id=..., organization_id=...) - .first()`, so this stands in for the ORM: an id in `missing` resolves to - None (not this org's, or nonexistent -- the gate cannot and must not tell - those apart), and everything else resolves to an adapter of the type - `types` gives, defaulting to the correct one for its role. + """Patch the single-adapter lookup `_resolved_adapters` performs. + + Patches `ev._lookup_adapter`, the view's own seam, rather than the ORM. + Two reasons, both learned the hard way: `AdapterInstance._base_manager` is + a read-only property and cannot be patched at all, and patching `.objects` + is what hid the defect where the gate refused every valid submit (that + manager auto-filters by a request-local org this route never sets). + + So these tests assert the GATE's logic -- org match, type match, identical + refusals -- and `tests/test_adapter_scoping.py` asserts that the real + lookup finds a real adapter with no request context. Neither covers the + other. + + An id in `missing` resolves to None (not this org's, or nonexistent -- the + gate cannot and must not tell those apart); everything else resolves to an + adapter of the type `types` gives, defaulting to the right one per role. """ - from adapter_processor_v2.models import AdapterInstance - type_by_id = { aid: (types or {}).get(role, _ROLE_TYPES[role]) for role, aid in _TABLE_ADAPTERS.items() } + missing_ids = {str(m) for m in missing} + + def _lookup(adapter_id, org_id): + aid = str(adapter_id) + if aid in missing_ids or org_id != organization_id: + return None + return mock.Mock(adapter_type=type_by_id.get(aid, "LLM")) - def _filter(**kw): - qs = mock.Mock() - aid = str(kw.get("id")) - if aid in {str(m) for m in missing} or kw.get( - "organization_id" - ) != organization_id: - qs.first.return_value = None - else: - qs.first.return_value = mock.Mock(adapter_type=type_by_id.get(aid, "LLM")) - return qs - - return mock.patch.object(AdapterInstance, "objects", **{"filter.side_effect": _filter}) + return mock.patch.object(ev, "_lookup_adapter", side_effect=_lookup) def _stamp_created_at(job, *args, **kwargs): diff --git a/tests/e2e/agent_kv/conftest.py b/tests/e2e/agent_kv/conftest.py index 9b16da9a90..673787e07f 100644 --- a/tests/e2e/agent_kv/conftest.py +++ b/tests/e2e/agent_kv/conftest.py @@ -13,6 +13,7 @@ from __future__ import annotations import json +import os import time import uuid from dataclasses import dataclass @@ -149,6 +150,38 @@ def kv_stages(status_doc: dict) -> list[dict]: return status_doc["extractors"]["kv"]["stages"] +#: The platform adapters an Agent-KV `table` submit must name, read from the +#: environment because they are rows in the deployed stack's database -- an +#: e2e client cannot mint them, and they carry real provider credentials. +#: +#: Unset by default, which makes every submit a 400. The `require_adapters` +#: fixture is what turns that into an explicit skip instead of a confusing +#: failure; scenarios that expect a rejection BEFORE the adapter gate (bad +#: key, rate limit, absent `extractors`) do not need it. +#: +#: Create them once in the target org -- an LLM adapter, a second cheaper LLM +#: adapter, and an LLMWhisperer X2TEXT adapter -- and export their ids. +E2E_ADAPTER_ENV = { + "llm": "AGENT_KV_E2E_LLM_ADAPTER", + "lite_llm": "AGENT_KV_E2E_LITE_LLM_ADAPTER", + "x2text": "AGENT_KV_E2E_X2TEXT_ADAPTER", +} + + +def e2e_adapters() -> dict[str, str]: + """The configured adapter ids, by role; roles with no env var are omitted.""" + return { + role: os.environ[var] + for role, var in E2E_ADAPTER_ENV.items() + if os.environ.get(var) + } + + +def missing_adapter_env() -> list[str]: + """Which adapter env vars are unset, for a skip message that names them.""" + return [var for var in E2E_ADAPTER_ENV.values() if not os.environ.get(var)] + + def submit_raw( auth: AgentKVAuth, file_bytes: bytes, @@ -156,6 +189,7 @@ def submit_raw( keys: dict | None, *, extractor: str = "table", + adapters: dict[str, str] | None = None, **fields: object, ) -> requests.Response: """POST a submit request and return the raw response -- no assertions. @@ -184,9 +218,19 @@ def submit_raw( f"belongs there" ) else: - data["extractors"] = json.dumps( - [{"name": extractor, "keys": keys, "options": options}] - ) + entry: dict[str, object] = { + "name": extractor, + "keys": keys, + "options": options, + } + # `adapters` is REQUIRED for `table` -- the caller names the LLM and + # OCR adapters the extraction runs on. Defaults to whatever the + # environment configured; `{}` is sent as-is so a scenario can assert + # the 400 for an absent block. + resolved = e2e_adapters() if adapters is None else adapters + if resolved: + entry["adapters"] = resolved + data["extractors"] = json.dumps([entry]) return requests.post( auth.exec_url, headers=auth.headers, @@ -203,6 +247,7 @@ def submit( keys: dict, *, extractor: str = "table", + adapters: dict[str, str] | None = None, **fields: object, ) -> tuple[str, str]: """POST a submit request expected to succeed; return (job_id, status_url). @@ -210,9 +255,13 @@ def submit( Asserts the 202 handshake (spec §7.1 / docs §3) so every caller only ever polls a genuinely dispatched job. A 501 here almost always means the cloud ``agentic_table`` executor plugin isn't installed on this deployment - -- this whole lane requires it (docs §11). + -- this whole lane requires it (docs §11). A 400 naming `adapters` + means the `AGENT_KV_E2E_*_ADAPTER` ids are unset or do not belong to this + key's organization -- gate such a scenario on `require_adapters`. """ - resp = submit_raw(auth, file_bytes, filename, keys, extractor=extractor, **fields) + resp = submit_raw( + auth, file_bytes, filename, keys, extractor=extractor, adapters=adapters, **fields + ) assert resp.status_code == 202, ( f"submit: HTTP {resp.status_code} (expected 202; a 501 means the " f"agentic_table executor plugin isn't installed on this deployment): {resp.text}" diff --git a/tests/e2e/agent_kv/test_agent_kv_e2e.py b/tests/e2e/agent_kv/test_agent_kv_e2e.py index 6bff877d2e..c515ca5dd0 100644 --- a/tests/e2e/agent_kv/test_agent_kv_e2e.py +++ b/tests/e2e/agent_kv/test_agent_kv_e2e.py @@ -10,13 +10,22 @@ accident just because a platform happens to be up; it must be opted into for a run that actually ships the plugin. -A second, narrower gate (the ``require_llm`` fixture below) applies only to -the two scenarios whose assertions depend on a job actually reaching -COMPLETED (real extraction happened): either ``AGENT_KV_LLM_API_KEY`` or -``UNSTRACT_LLM_MOCK_RESPONSE`` must be set. Every other scenario here is -gated purely by request-time checks (auth, schema compile, the concurrency -limiter, the pre-OCR page cap) that resolve before any LLM call, so they -don't need either. +A second gate (the ``require_adapters`` fixture) applies to every scenario +whose submit must be ACCEPTED. `table` submits now name the caller's own +``llm`` / ``lite_llm`` / ``x2text`` platform adapters, and the API validates +each id against the Bearer key's organization and its adapter type -- so +without ``AGENT_KV_E2E_LLM_ADAPTER`` / ``_LITE_LLM_ADAPTER`` / +``_X2TEXT_ADAPTER`` every submit is a 400 before dispatch. Scenarios that +expect a rejection BEFORE that gate (a bad key, the rate limiter, an absent +``extractors``) do not take it. + +A third, narrower gate (``require_llm``) applies only to the scenarios whose +assertions depend on a job actually reaching COMPLETED: either +``AGENT_KV_LLM_API_KEY`` or ``UNSTRACT_LLM_MOCK_RESPONSE`` must be set. Note +that on the table path the LLM credential now lives on the ADAPTER rather +than in the executor's environment, so a real-adapter run satisfies this +whether or not that variable is exported -- it is kept for the mock case and +for the KV path. Values extracted under ``UNSTRACT_LLM_MOCK_RESPONSE`` are whatever the mock config returns, not real answers -- assertions here deliberately check @@ -64,6 +73,7 @@ AgentKVAuth, cancel, delete, + missing_adapter_env, poll, result, submit, @@ -161,6 +171,36 @@ def _drain_until_slot_free( return False +@pytest.fixture +def require_adapters() -> None: + """Skip a scenario that needs a submit to be ACCEPTED (202). + + `table` submits must name the caller's own `llm` / `lite_llm` / `x2text` + platform adapters, and the API validates each id against the Bearer key's + organization and against its adapter type. Those are rows in the deployed + stack's database carrying real provider credentials -- an e2e client cannot + create them, so the ids come from the environment. + + Without them every submit is a 400 before dispatch, so this gate turns an + unconfigured lane into an explicit skip rather than a wall of confusing + failures. Scenarios that expect a rejection BEFORE the adapter gate -- a + bad key (403), the rate limiter (429), an absent `extractors` field -- + deliberately do not take this fixture. + + This replaces what `AGENT_KV_LLM_API_KEY` used to provide for the table + path: the credentials now live on the adapters, not in the executor's + environment. + """ + missing = missing_adapter_env() + if missing: + pytest.skip( + "needs the caller's platform adapters so a submit can be accepted; " + f"unset: {', '.join(missing)}. Create an LLM adapter, a cheaper " + "LLM adapter and an LLMWhisperer X2TEXT adapter in the target org " + "and export their ids." + ) + + @pytest.fixture def require_llm() -> None: """Skip a scenario that needs a job to actually reach COMPLETED. @@ -198,7 +238,9 @@ def test_submit_without_key_is_403(agent_kv_key: AgentKVAuth) -> None: # --------------------------------------------------------------------------- -def test_table_extractor_happy_path(agent_kv_key: AgentKVAuth, require_llm: None) -> None: +def test_table_extractor_happy_path( + require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None +) -> None: """A `table` entry runs the table engine and files its result under `table`. Before the job row recorded its extractor, every response keyed by the @@ -318,7 +360,7 @@ def test_submit_unreadable_pdf_is_400(agent_kv_key: AgentKVAuth) -> None: # --------------------------------------------------------------------------- -def test_cancel_mid_run(agent_kv_key: AgentKVAuth) -> None: +def test_cancel_mid_run(require_adapters: None, agent_kv_key: AgentKVAuth) -> None: job_id, _ = submit( agent_kv_key, _RENT_ROLL_BYTES, @@ -353,6 +395,7 @@ def test_cancel_mid_run(agent_kv_key: AgentKVAuth) -> None: def test_cancelled_job_does_not_leak_its_concurrency_slot( + require_adapters: None, agent_kv_key: AgentKVAuth, ) -> None: """A cancelled job must give its concurrency slot back. @@ -396,7 +439,7 @@ def test_cancelled_job_does_not_leak_its_concurrency_slot( def test_delete_completed_job_then_result_404( - agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None ) -> None: job_id, _ = submit( agent_kv_key, @@ -450,7 +493,9 @@ def _assert_user_safe_error(error: str) -> None: assert leak not in error, f"a filesystem path leaked into: {error!r}" -def test_bad_llm_key_ends_failed(agent_kv_key: AgentKVAuth) -> None: +def test_bad_llm_key_ends_failed( + require_adapters: None, agent_kv_key: AgentKVAuth +) -> None: """A job that DISPATCHES and then fails in the executor ends ``failed`` with a user-safe error. @@ -506,7 +551,7 @@ def test_bad_llm_key_ends_failed(agent_kv_key: AgentKVAuth) -> None: def test_sync_wait_submit_returns_result_inline( - agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None ) -> None: """A submit with ``timeout`` seconds sync-waits: if the job reaches a terminal state inside the window, the response is 200 with the result @@ -539,7 +584,9 @@ def test_sync_wait_submit_returns_result_inline( # --------------------------------------------------------------------------- -def test_excel_submit_extracts(agent_kv_key: AgentKVAuth, require_llm: None) -> None: +def test_excel_submit_extracts( + require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None +) -> None: """An .xlsx submit runs the whole pipeline. Excel has no pre-OCR page count (``pages_total`` stays None at submit) and takes the post-OCR virtual-page cap path instead. Structure-only assertions, like the @@ -562,7 +609,7 @@ def test_excel_submit_extracts(agent_kv_key: AgentKVAuth, require_llm: None) -> def test_webhook_delivered_on_completion( - agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None ) -> None: """Completion-webhook delivery, end to end: submit with a ``webhook_url`` pointing at a receiver on the compose host and wait for the POST. @@ -623,7 +670,9 @@ def log_message(self, *args): # keep pytest output pristine thread.join(timeout=5) -def test_concurrency_limit_returns_429(agent_kv_key: AgentKVAuth) -> None: +def test_concurrency_limit_returns_429( + require_adapters: None, agent_kv_key: AgentKVAuth +) -> None: """Saturate the org's concurrency limiter; every other submit-based scenario in this module runs before this one, and ``tests/groups.yaml`` pins this whole group ``parallel: false`` -- both for the same reason: From 82d51813bf3be471df47e286efe346d4de751b1a Mon Sep 17 00:00:00 2001 From: vishnuszipstack Date: Thu, 8 Oct 2026 15:05:03 +0530 Subject: [PATCH 18/18] UN-4232 [FIX] Round-2 review: a billing control the gate skipped, two dead guards, and one false premise MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A1 (CONFIRMED, the one with teeth). The adapter gate checked organization and adapter_type and nothing else. `is_usable` is how billing cuts off a frictionlessly onboarded org's operator-funded trial, and the platform service does NOT check it -- it hands the credentials back regardless. So the IDE, workflows and Prompt Studio all refuse an exhausted trial (tool_instance_helper.py:533, prompt_studio_helper.py:149/203/3292) while an Agent-KV submit naming the SAME adapter id got a 202 and ran on credentials the operator pays for and has already cut off. Added `is_usable` and `is_available` (deprecated -> `InValidAdapterId` deep in the engine, now a 400 at submit), reusing the platform's own message. A3. Migration 0004 seeded two PgPeriodicTask rows on the stated premise that there was "no PgPeriodicTask, no beat entry, no CronJob". That premise was false about its own tree: the cloud chart already ran the same two management commands, added four commits earlier on this branch. The rows were `pg_owned: False` so nothing double-fired yet -- but that flag exists to be flipped, and a flip would have put a */10 sweep against the chart's */15, colliding at :30 on the same rows, with TTL cadences disagreeing 24x. Only the CronJob side has `concurrencyPolicy: Forbid` and a deadline. 0004 now DELETES the rows (idempotent, name-scoped, reverse restores them inert) and the CronJob is the sole owner. Verified forward/reverse/re-apply against a real DB, including that the five dashboard_metrics periodics are untouched. Self-hosted OSS operators are told to schedule the two commands themselves -- docs §12. C1. `_never_dispatched` read the in-memory row, fetched BEFORE `mark_terminal`. A dispatch completing in that gap made the cancel release a slot whose executor was running and billing -- a smaller instance of the over-release bug the narrowing was written to fix. Now re-reads `dispatched_at` from the DB. The residual enqueue-to-bookkeeping window is documented rather than papered over: bounded at one slot, self-correcting via the idempotent finalize release. C2. Sweep phase 3 counted slot releases, logged them, and returned only `{swept, timed_out}` -- so neither `SweepView` nor the scheduler log could ever show a slot was recovered. Now returns `released` too. E9. Nothing recorded which adapters a run spent. Adapter choice is now per-request, caller-controlled and cost-bearing, so "which model did job X use?" needed a usage_v2 join the API cannot do and the customer cannot see. New `AgentKVJob.adapters` (migration 0006), populated at submit and reported in the status document. Ids only, never adapter_metadata -- that column is returned to the caller. B1. The queue guard was literal-pinned, so re-enabling `kv` would have required DELETING its inverse assertion to go green. Added `tests/test_queue_wiring_is_derived.py`, which derives both directions from `EXTRACTOR_ROUTES` (and so flips on its own), covers `run-worker-docker.sh` (guarded by nothing -- dropping `agent_kv_callback` there passed every test while a docker-launched fleet drained no terminal callbacks), and asserts the derivation is non-vacuous. Also derives the billing backstop: every routed operation must be in `_LLM_BEARING_OPS`, which the two hand-maintained literals could not catch when both were wrong together. The workers-side suite keeps its literals as a second opinion, now cross-referenced. The dead-Celery-var test is NOT parametrized -- review asked for that and dev compose legitimately sets the variable; generalized to "never advertises an unserved queue" instead, over both files. B2. The format-typo guard missed the two formats that take an argument: `_parse_format` matches the `enum:`/`regex:` prefix case-sensitively, so `format: "Enum:paid,unpaid"` never became kind `enum`, slipped the guard as free text, and validation was silently off for that key -- the exact failure the round-1 fix closed for bare names. Now splits on the colon. Rejected rather than auto-corrected, matching the bare case. B7 / C3. Pinned two round-1 fixes that no test could detect: `maintenance.py`'s logging (nothing used caplog on it; all five lines were deletable) and the `except Exception: pass` in `ide_callback/agent_kv_tasks.py` (its test was byte-identical before and after). Fixed the same bare `pass` in the sibling `ide_callback/tasks.py`. Replaced `test_agent_kv_error_matches_its_siblings_failure_posture`, which asserted `agent_kv_complete is not None`, with one that actually drives both callbacks through a failing finalize. Corrected the `retry_backoff` docstring: the PG consumer runs tasks via `task.apply(throw=True)` -- eager -- so eager retry recurses synchronously and ignores countdown; the three attempts are back-to-back and ride out no blip. The real delay comes from vt-redelivery after the raise. C4. `require_llm` gated the only two scenarios that poll a job to a genuine COMPLETED on `AGENT_KV_LLM_API_KEY` -- a variable the table path does not read, since the credential is on the adapter. A correctly configured lane SKIPPED them. Removed; `require_adapters` is the whole gate. `test_bad_llm_key_ends_failed` had lost its trigger entirely (`AGENT_KV_E2E_BAD_KEY_JOB` is unread, so an operator following the docs got a job that reached `completed` and failed the test's own assertion) -- re-pointed at `AGENT_KV_E2E_BAD_LLM_ADAPTER`, where the credential now lives. Part D. `test_submit_unreadable_pdf_is_400` and `test_page_cap_rejects_oversized_document` went RED, not skipped, without adapter env: the adapter shape check runs in a FIELD validator, so DRF raises it before `validate()` and the blamed attr is `extractors`, not `file`. Both now take `require_adapters`. Replaced the identical-refusal test that ran the same configuration twice and compared the body to itself -- the oracle property is now asserted from two genuinely different DB states in `test_adapter_scoping.py`, which also gained the `adapter_type` comparison against a real row (previously only ever against a mock that chose its own value) and explicit `is_usable`/`is_available` on every stand-in, since a bare `Mock` satisfies any attribute the gate reads. E1. Four places across both repos asserted the submit gate was the only tenancy check and were cited as the justification for its design. It is not: platform-service resolves adapters with `WHERE id=%s and organization_id=%s` against the org of the key `dispatch._platform_api_key(job)` mints from the job's org, so org A cannot spend org B's credential even with this gate removed. Corrected to defence-in-depth, with what the gate actually buys: a clean 400, plus the two refusals the platform service does not make. E7. Documented that the page cap bounds `pages_selected` (after page_start/page_end), not the document's length -- an unlisted behaviour change. A4. Deploy-order skew: a new backend against an OLD worker sends validated `adapters` to a worker with no `ParamKeys.ADAPTERS`, which ignores them and calls `from_env()`. Where the six AGENT_KV_* values are set it runs on OPERATOR credentials and returns a correct-looking 202. Documented: deploy cloud first, and leave that group unset. Also: the stale "see the backend migration that schedules them" in workers/scheduler, and `_resolved_adapters`' discarded return value (now assigned back, so future normalization reaches dispatch). New: `unstract/sdk1/tests/test_page_usage_metering.py` pins that non-PDF inputs meter as ONE page while PDFs meter per real page. That under-counts Excel, which the table engine splits into virtual pages -- product-wide, rooted in the SDK's own TODO, and affecting the IDE path identically. Pinned rather than changed: correcting it re-prices every spreadsheet extraction and needs sign-off. Verification: agent_kv 294 passed; workers (ide_callback, config, webhook, client, scheduler, queue-wiring, llm-bearing-ops) 74 passed 1 skipped; agent-kv-schema 138 passed; sdk1 766 passed. Migrations 0004 forward/reverse and 0006 applied against a real Postgres; `makemigrations --check` clean. ruff 0.3.4 check + format clean. Every fix above was mutation-verified by reverting it and confirming the new test fails. Co-Authored-By: Claude Opus 5 --- backend/agent_kv/dispatch.py | 19 +- backend/agent_kv/execution_views.py | 132 ++++++-- backend/agent_kv/maintenance.py | 15 +- .../migrations/0004_pg_periodic_tasks.py | 98 +++--- .../migrations/0006_agentkvjob_adapters.py | 30 ++ backend/agent_kv/models.py | 19 ++ .../agent_kv/tests/test_adapter_scoping.py | 213 ++++++++++++- .../tests/test_cross_repo_stage_name.py | 52 +++ backend/agent_kv/tests/test_job_views.py | 73 ++++- .../tests/test_periodic_tasks_migration.py | 165 +++++++--- .../tests/test_queue_wiring_is_derived.py | 297 ++++++++++++++++++ backend/agent_kv/tests/test_submit_view.py | 129 ++++++-- backend/agent_kv/tests/test_sweeps.py | 107 ++++++- docs/agent-kv-api.md | 78 ++++- tests/e2e/agent_kv/conftest.py | 9 + tests/e2e/agent_kv/test_agent_kv_e2e.py | 127 +++++--- .../src/unstract/agent_kv_schema/compile.py | 32 +- .../agent-kv-schema/tests/test_compile.py | 66 ++++ .../sdk1/tests/test_page_usage_metering.py | 172 ++++++++++ workers/ide_callback/agent_kv_tasks.py | 19 +- workers/ide_callback/tasks.py | 19 +- .../tests/test_agent_kv_callbacks.py | 91 +++++- workers/scheduler/agent_kv_tasks.py | 10 +- workers/tests/test_llm_bearing_ops.py | 18 +- workers/tests/test_queue_consumer_wiring.py | 60 ++++ 25 files changed, 1826 insertions(+), 224 deletions(-) create mode 100644 backend/agent_kv/migrations/0006_agentkvjob_adapters.py create mode 100644 backend/agent_kv/tests/test_queue_wiring_is_derived.py create mode 100644 unstract/sdk1/tests/test_page_usage_metering.py diff --git a/backend/agent_kv/dispatch.py b/backend/agent_kv/dispatch.py index f45dcb3827..7f6cb4276f 100644 --- a/backend/agent_kv/dispatch.py +++ b/backend/agent_kv/dispatch.py @@ -120,10 +120,21 @@ def dispatch_job( # Platform adapter instance ids, by role, already validated at # submit against THIS job's organization and against the # expected `AdapterTypes` (see - # `execution_serializers.validated_adapters`). The executor - # resolves them through the platform service using the key - # below -- it does NOT re-check tenancy, so the submit-time - # check is the only one there is. + # `execution_serializers._validated_adapter_shape` for the + # shape half and `execution_views._resolved_adapters` for the + # tenancy half). + # + # Defence in depth, NOT the only check: the platform service + # re-scopes every lookup as + # `WHERE id=%s and organization_id=%s` + # (platform-service/.../helper/adapter_instance.py:28-30), + # where the org comes from the bearer platform key that + # `_platform_api_key(job)` below mints from THIS job's org. So + # org A cannot spend org B's credential even with the submit + # gate removed. What the submit gate buys is a clean 400 naming + # the role instead of a mid-run `SdkError`, plus the two checks + # the platform service does NOT make: `is_usable` (exhausted + # trial) and `is_available` (deprecated). # # Empty for an env-configured extractor (`kv`), which is why # this is a dict rather than three params: the two credential diff --git a/backend/agent_kv/execution_views.py b/backend/agent_kv/execution_views.py index 4c610d5a93..93d1699167 100644 --- a/backend/agent_kv/execution_views.py +++ b/backend/agent_kv/execution_views.py @@ -80,6 +80,12 @@ def _status_document(job) -> dict: ], } }, + # Which adapters this run spent, by role. Reported because the caller + # chose them and is paying for them: a run naming the wrong `llm` id + # produces a correct-looking result at a different price, and without + # this the only record is in `usage_v2`, which the caller cannot read. + # `{}` for an env-configured extractor. + "adapters": job.adapters or {}, "created_at": job.created_at.isoformat() if job.created_at else None, "started_at": job.dispatched_at.isoformat() if job.dispatched_at else None, "completed_at": job.completed_at.isoformat() if job.completed_at else None, @@ -93,13 +99,37 @@ def _status_document(job) -> dict: def _never_dispatched(job) -> bool: """True when this job never reached an executor, so no callback will come. - Read from the row as it was BEFORE this request terminalized it. Both - conditions are deliberate: `dispatched_at` is stamped by post-enqueue - bookkeeping that can legitimately fail, and `status` is what the dispatcher - advances -- a job with either marker set has, or may have, a running - executor whose finalize callback owns the slot. + **Re-read from the database, not from the in-memory row.** The row the + cancel path holds was fetched BEFORE `mark_terminal`, and a dispatch can + complete inside that gap: + + cancel reads the row -> PENDING, dispatched_at=NULL + dispatch_job -> passes its own terminal re-read, enqueues, + writes dispatched_at and DISPATCHED + cancel's mark_terminal-> wins (DISPATCHED is not terminal) + _never_dispatched -> True on the STALE row + + which releases the slot while the executor is running and billing. That is + a smaller version of the original over-release bug this narrowing was + written to fix, reintroduced by trusting the pre-cancel snapshot. + + Only `dispatched_at` is consulted, and `status` deliberately is not: by the + time this runs the cancel has already rewritten `status` to CANCELLED, so + the old `status == PENDING` test could only ever be answered from the stale + object. `dispatched_at` is the durable marker of "an executor may exist" + and the cancel never touches it. + + **Residual window, stated rather than papered over.** `dispatch_job` + enqueues and only then writes `dispatched_at` (`_record_dispatch`). A cancel + landing between those two still sees NULL and still releases early. The + consequence is bounded and self-correcting: one extra concurrent slot for + the remainder of that one run, after which the executor's finalize releases + again (`release` is an idempotent zrem). Closing it completely means + stamping a dispatch-intent marker before the enqueue, which changes what + `dispatched_at` means to both sweep phases -- a larger change than the + defect warrants. """ - return job.dispatched_at is None and job.status == JobStatus.PENDING + return not AgentKVJob.objects.filter(id=job.id, dispatched_at__isnull=False).exists() def _fail_job_response(job, org_id: str, message: str, *, job_saved: bool) -> Response: @@ -338,13 +368,23 @@ def _resolved_adapters(entry: dict, agent_kv_key) -> dict: (`_validated_adapter_shape`); what is added here is the half that needs a database and the Bearer key. - **This is the tenant boundary on adapters, and nothing downstream repeats - it.** The executor resolves these ids through the platform service with an - org-scoped platform key, but it does not re-check that the id belongs to - the job's org -- so without this, a caller passes any adapter UUID and the - run SPENDS ANOTHER TENANT'S LLM CREDENTIAL. Same class as the - `organization_id` filter in `_get_job`, and the highest-consequence failure - this endpoint has. + **Defence in depth, not the only tenancy check** -- an earlier version of + this docstring claimed it was, and was wrong. The platform service re-scopes + every adapter lookup as `WHERE id=%s and organization_id=%s` + (platform-service/.../helper/adapter_instance.py:28-30), against the org of + the bearer platform key that `dispatch._platform_api_key(job)` mints from + THIS job's org. Org A therefore cannot spend org B's credential even if this + gate were deleted outright. + + What this gate does buy, and why it is still worth having: + + - A clean 400 naming the offending role at submit, instead of a mid-run + `SdkError` on a job that already took a concurrency slot and billed for + staging. + - The two refusals the platform service does NOT make: `is_usable` (a + frictionless org's operator-funded trial is exhausted -- it hands back the + credentials anyway) and `is_available` (deprecated, removed from the SDK + registry). It lives in the view rather than the serializer for the same reason `_subscription_denial` does: it needs the key. The platform's own helpers @@ -390,6 +430,43 @@ def _resolved_adapters(entry: dict, agent_kv_key) -> dict: ) } ) + # Trial exhaustion. `is_usable` is flipped False when a frictionlessly + # onboarded org burns through its operator-funded sample allowance, and + # the platform service does NOT check it -- it returns the credentials + # regardless. So without this the IDE, workflows and Prompt Studio all + # refuse (tool_instance_helper.py, prompt_studio_helper.py) while an + # Agent-KV submit naming the SAME adapter id gets a 202 and runs on + # credentials the operator is paying for and has already cut off. + # + # Same message those surfaces use, so an exhausted trial reads + # identically whichever door the caller knocks on. + if not adapter.is_usable: + logger.error( + "agent-kv: free usage for sample adapter %s is exhausted", adapter.id + ) + raise ValidationError( + { + "adapters": ( + f"{role}: free usage for the configured trial adapter is " + "exhausted. Connect your own service account to continue: " + "https://docs.unstract.com/unstract_platform/setup_accounts/" + "whats_needed" + ) + } + ) + # Deprecated: removed from the SDK registry, so resolving it raises + # `InValidAdapterId` (adapter_processor.py:120) deep inside the engine + # and surfaces to the caller as a mid-run extraction failure on a job + # that was already billed for staging. A 400 at submit names the cause. + if not adapter.is_available: + raise ValidationError( + { + "adapters": ( + f"{role}: adapter is deprecated and no longer available; " + "pick a current one" + ) + } + ) return requested @@ -415,17 +492,30 @@ def post(self, request, *args, agent_kv_key=None, **kwargs): v = serializer.validated_data org_id = str(agent_kv_key.organization_id) - # Tenancy + type on the named adapters, HERE and not later: before the - # job row, before the concurrency slot, before the staging write. A - # caller who names another org's adapter, or swaps two UUIDs, gets a - # 400 and is charged nothing -- the same "every cap before paid work" - # rule the serializer's caps follow. - _resolved_adapters(v["extractors"][0], agent_kv_key) + # Tenancy + type + usability on the named adapters, HERE and not later: + # before the job row, before the concurrency slot, before the staging + # write. A caller who names another org's adapter, swaps two UUIDs, or + # points at an exhausted trial gets a 400 and is charged nothing -- the + # same "every cap before paid work" rule the serializer's caps follow. + # + # Assigned back rather than discarded: `_dispatch_or_fail` reads + # `entry["adapters"]` to build `executor_params`, so anything this gate + # ever normalizes (it returns the dict unchanged today) must land where + # dispatch will see it. Discarding the return made the two paths agree + # only by the accident of being the same dict object. + v["extractors"][0]["adapters"] = _resolved_adapters( + v["extractors"][0], agent_kv_key + ) job = AgentKVJob( api_key=agent_kv_key, organization_id=agent_kv_key.organization_id, extractor=v["extractors"][0]["name"], + # The gate above validated these against this org, their expected + # types, and usability -- so what lands here is exactly what the + # run will spend. Ids only, never `adapter_metadata`: this column + # is returned to the caller. + adapters=v["extractors"][0].get("adapters") or {}, pages_total=serializer.pages_total, tags=v["tags"], custom_data=v["custom_data"], @@ -588,6 +678,10 @@ def delete(self, request, *args, job_id=None, agent_kv_key=None, **kwargs): # Guarded on `won` so a lost race (a concurrent cancel or # finalize terminalized it first) does not release a slot that # the winner is still accounting for. + # + # `_never_dispatched` re-reads `dispatched_at` from the DB: the + # `job` here predates `mark_terminal`, and a dispatch can + # complete in that gap (see that function). if _never_dispatched(job): AgentKVConcurrencyLimiter.release( str(job.organization_id), str(job.id) diff --git a/backend/agent_kv/maintenance.py b/backend/agent_kv/maintenance.py index a0a6bbe545..0a537a9708 100644 --- a/backend/agent_kv/maintenance.py +++ b/backend/agent_kv/maintenance.py @@ -225,7 +225,20 @@ def run_sweep() -> dict: ) else: logger.info("agent-kv sweep: nothing to terminalize") - return {"swept": swept, "timed_out": timed_out} + # `released` is reported, not just logged. Phase 3 counted slot releases + # into a local, logged it, and then returned only the first two counts -- + # so `SweepView` (which returns this dict verbatim) and the scheduler task + # (which logs it) could never show that any slot was recovered. That is the + # same "counted, then discarded before the caller sees it" shape as the + # round-1 finding about this function's return value, reintroduced by the + # phase that was added to fix a different half of it. + # + # It matters operationally: a cancelled job whose executor never called + # back holds a slot against the org's cap, and phase 3 is the only thing + # that frees it. Without this key, "the cap is being hit and the sweep is + # recovering slots" and "the cap is being hit and nothing is recovering + # anything" produce identical output. + return {"swept": swept, "timed_out": timed_out, "released": released} def run_ttl_cleanup() -> dict: diff --git a/backend/agent_kv/migrations/0004_pg_periodic_tasks.py b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py index 046f7558e7..c010a9b488 100644 --- a/backend/agent_kv/migrations/0004_pg_periodic_tasks.py +++ b/backend/agent_kv/migrations/0004_pg_periodic_tasks.py @@ -1,45 +1,64 @@ -"""Schedule the two Agent-KV maintenance periodics. +"""Remove the Agent-KV maintenance periodics -- the CronJob owns the schedule. -Both tasks were registered in ``workers/scheduler/agent_kv_tasks.py`` and both -internal endpoints existed, but nothing anywhere created a row to fire them -- -no ``PgPeriodicTask``, no beat entry, no CronJob. The module's own docstring -said an operator must register them by hand, which in practice means they never -ran. +An earlier revision of this migration SEEDED two ``PgPeriodicTask`` rows +(``agent_kv.sweep`` at ``*/10``, ``agent_kv.ttl_cleanup`` at ``17 3 * * *``) on +the premise that nothing anywhere fired the two maintenance commands -- "no +``PgPeriodicTask``, no beat entry, no CronJob". **That premise was false when it +was written.** ``templates/backend/agent-kv-cronjobs.yaml`` in the cloud chart +already ran the same two management commands, on the same branch, added several +commits earlier. The seeding migration was written in response to a review +finding about the unscheduled sweep without noticing the finding had already +been answered elsewhere in the same change. -What that costs, concretely: +Keeping both would have been worse than either alone: -* ``run_sweep`` never runs, so an OOM-killed executor leaves its job row - non-terminal forever -- ``link_error`` never fires for a killed process -- and - the row holds its concurrency slot until the 6h Redis TTL. Enough of those and - the org's allowance is exhausted and new submits start getting 429s. -* ``run_ttl_cleanup`` never runs, so ``AGENT_KV_RESULT_TTL_DAYS`` is advisory: - every staged customer document and every result stays in the bucket - indefinitely. That is a retention-policy failure, not just a disk-usage one. +* **Two owners for one schedule.** The rows are seeded ``pg_owned: False``, and + the PG scheduler claims only ``WHERE pg_owned AND enabled`` + (``pg_queue/models.py``), so they are inert *today*. But the whole point of + that flag is that a rollout flips it -- and the moment it did, the ``*/10`` + rows and the CronJob's ``*/15`` ticks would both fire, colliding exactly at + ``:30`` and contending on the same job rows. The TTL cadences disagreed by + 24x (daily here, hourly in the chart), so whichever fired would be whichever + owner someone remembered. +* **Only one owner has operational bounds.** The CronJob carries + ``concurrencyPolicy: Forbid`` and ``activeDeadlineSeconds``; a PG periodic has + neither, so a hung sweep under the scheduler has nothing stopping it. +* **The feature only runs where the CronJob is.** This deployment routes + ``table`` only, and the ``agentic_table`` plugin ships in the cloud image. A + pure-OSS install cannot dispatch an Agent-KV job at all, so there is no + stranded row for a sweep to reap there. -Follows the ``dashboard_metrics`` precedent (migrations 0004-0006) rather than -inventing a mechanism: the same ``PgPeriodicTask`` table, the same -``update_or_create`` shape so a re-run is idempotent, and ``pg_owned: False`` so -applying this migration does not itself start firing them -- the rollout flag -decides that, exactly as it does for the metrics periodics. +So the CronJob is the single owner, and this migration deletes the rows rather +than merely stopping at not creating them -- any environment that applied the +seeding revision (dev namespaces did) still has them, and leaving them to be +switched on by a future global ``pg_owned`` flip is precisely the hazard being +removed. -Cadences are deliberately offset from each other and off the hour: the sweep is -cheap and wants to be frequent (a stuck job holds a slot), the TTL cleanup is a -bulk delete that should not land on the same tick as anything else. +**Self-hosted OSS operators who enable the Agent-KV API must schedule +``manage.py agent_kv_sweep`` and ``manage.py agent_kv_ttl_cleanup`` +themselves** (cron, a systemd timer, or their own ``PgPeriodicTask`` rows with +``pg_owned: True``). Without them a stranded job holds its concurrency slot +until the 6h Redis TTL, and ``AGENT_KV_RESULT_TTL_DAYS`` is advisory -- +staged documents and results stay in the bucket indefinitely, which is a +retention failure rather than a disk-usage one. ``docs/agent-kv-api.md`` §12 +carries this in the deploy checklist. + +Reverse re-creates the rows (still inert) so the migration is reversible and a +``migrate agent_kv 0003`` lands back on the previous tree's state exactly. """ from django.db import migrations +#: What the seeding revision wrote, kept ONLY so forward can delete exactly +#: those rows by name and reverse can restore them. Not a live schedule -- +#: the cadences that run are in the cloud chart's `backend.agentKvCronJobs`. PG_PERIODIC_TASKS = [ { "name": "agent_kv_sweep", - # Wire name registered by workers/scheduler/agent_kv_tasks.py. "task_name": "agent_kv.sweep", "queue": "scheduler", "task_args": [], "task_kwargs": {}, - # Every 10 minutes: a stuck job holds a concurrency slot, and the - # handler is idempotent and batch-capped server-side, so a missed tick - # costs nothing and a frequent one is cheap. "cron_string": "*/10 * * * *", }, { @@ -48,19 +67,25 @@ "queue": "scheduler", "task_args": [], "task_kwargs": {}, - # Daily at 03:17 UTC. Off the hour and off the dashboard_metrics - # cleanups (02:00 / 03:00) so a bulk object-store delete does not land - # on the same tick as another bulk job. "cron_string": "17 3 * * *", }, ] -def create_pg_periodic_tasks(apps, schema_editor): +def remove_pg_periodic_tasks(apps, schema_editor): + """Delete the seeded rows. Idempotent: a fresh install matches nothing.""" # snake_case, not the usual `PgPeriodicTask = apps.get_model(...)` Django # idiom: it is a local variable, and sonar's S117 reads the CamelCase name # as a naming violation. The historical-model object is the same either way. periodic_task = apps.get_model("pg_queue", "PgPeriodicTask") + periodic_task.objects.filter( + name__in=[spec["name"] for spec in PG_PERIODIC_TASKS] + ).delete() + + +def create_pg_periodic_tasks(apps, schema_editor): + """Reverse only. Restores the rows the seeding revision created, inert.""" + periodic_task = apps.get_model("pg_queue", "PgPeriodicTask") for spec in PG_PERIODIC_TASKS: periodic_task.objects.update_or_create( name=spec["name"], @@ -74,27 +99,18 @@ def create_pg_periodic_tasks(apps, schema_editor): # endpoints sweep across every organization. "org_id": "", "enabled": True, - # Inert until the rollout flag decides otherwise; never fired by - # applying this migration. Same posture as dashboard_metrics. "pg_owned": False, }, ) -def remove_pg_periodic_tasks(apps, schema_editor): - periodic_task = apps.get_model("pg_queue", "PgPeriodicTask") - periodic_task.objects.filter( - name__in=[spec["name"] for spec in PG_PERIODIC_TASKS] - ).delete() - - class Migration(migrations.Migration): dependencies = [ ("agent_kv", "0003_agentkvjob_cleanup_failed_at"), - # The table this seeds. + # The table this operates on. ("pg_queue", "0003_pgperiodictask"), ] operations = [ - migrations.RunPython(create_pg_periodic_tasks, remove_pg_periodic_tasks), + migrations.RunPython(remove_pg_periodic_tasks, create_pg_periodic_tasks), ] diff --git a/backend/agent_kv/migrations/0006_agentkvjob_adapters.py b/backend/agent_kv/migrations/0006_agentkvjob_adapters.py new file mode 100644 index 0000000000..e3a1b0d702 --- /dev/null +++ b/backend/agent_kv/migrations/0006_agentkvjob_adapters.py @@ -0,0 +1,30 @@ +"""Record which platform adapters a job ran on. + +The `table` extractor resolves the CALLER's own adapters rather than operator +env credentials, so adapter choice became a per-request, cost-bearing decision. +Before this column the chosen ids reached `executor_params` and nowhere else -- +not the job row, not the status document, not `usage_summary` -- so the first +question in any billing dispute ("which model did job X use?") needed a join +against `usage_v2` on `run_id` that the API cannot perform and the customer +cannot see. + +Additive and nullable-by-default (`{}`), so existing rows are untouched: they +predate caller-supplied adapters and genuinely ran on env credentials, which +`{}` is the honest representation of. +""" + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("agent_kv", "0005_agentkvjob_extractor_choices"), + ] + + operations = [ + migrations.AddField( + model_name="agentkvjob", + name="adapters", + field=models.JSONField(blank=True, default=dict), + ), + ] diff --git a/backend/agent_kv/models.py b/backend/agent_kv/models.py index 149a3136fe..45df84d8dd 100644 --- a/backend/agent_kv/models.py +++ b/backend/agent_kv/models.py @@ -106,6 +106,25 @@ class AgentKVJob(DefaultOrganizationMixin, BaseModel): stage = models.CharField(max_length=32, blank=True, default="") stages = models.JSONField(default=dict, blank=True) pages_total = models.IntegerField(null=True, blank=True) + #: Which platform adapters this run was dispatched with, by role -- + #: ``{"llm": "", "lite_llm": "", "x2text": ""}``. + #: + #: Recorded because adapter choice is now a per-request, caller-controlled, + #: COST-BEARING decision (the `table` extractor runs on the submitter's own + #: adapters, not operator env credentials). Without it the ids reached + #: `executor_params` and nothing else: neither the status document nor + #: `usage_summary` reported them, so "which model did job X use?" -- the + #: first question in any billing dispute -- could only be answered by + #: joining `usage_v2` on `run_id`, which the API cannot do and a customer + #: cannot see at all. + #: + #: Empty for an env-configured extractor (`kv`), which is why it is a dict + #: with a `{}` default rather than three nullable columns: the two + #: credential models coexist, one per extractor. + #: + #: Ids only. Deliberately never `adapter_metadata` -- that is where the + #: provider credentials live, and this column is returned to the caller. + adapters = models.JSONField(default=dict, blank=True) input_ref = models.CharField(max_length=512, blank=True, default="") result_ref = models.CharField(max_length=512, blank=True, default="") usage_summary = models.JSONField(null=True, blank=True) diff --git a/backend/agent_kv/tests/test_adapter_scoping.py b/backend/agent_kv/tests/test_adapter_scoping.py index 704fd849e6..a8984a8cad 100644 --- a/backend/agent_kv/tests/test_adapter_scoping.py +++ b/backend/agent_kv/tests/test_adapter_scoping.py @@ -26,6 +26,7 @@ import os import uuid +from unittest import mock import django from django.apps import apps @@ -38,7 +39,12 @@ from account_v2.models import Organization # noqa: E402 from adapter_processor_v2.models import AdapterInstance # noqa: E402 -from agent_kv.execution_views import _lookup_adapter # noqa: E402 +from rest_framework.exceptions import ValidationError # noqa: E402 + +from agent_kv.execution_views import ( # noqa: E402 + _lookup_adapter, + _resolved_adapters, +) from utils.user_context import UserContext # noqa: E402 @@ -110,3 +116,208 @@ def test_the_ambient_manager_really_does_hide_it(self): "`.objects` is expected to hide the adapter here -- that is the " "whole reason `_lookup_adapter` uses `_base_manager`", ) + + +class TestResolvedAdaptersAgainstRealRows(TestCase): + """`_resolved_adapters` end-to-end against the database, not against mocks. + + `test_submit_view.py` patches `ev._lookup_adapter`, so every property the + gate reads off an adapter -- `adapter_type`, `is_usable`, `is_available` -- + is only ever compared to a value the stand-in chose for itself. That is + enough to test the gate's branching and nothing about whether the branches + match the real columns. These rows get their flags from the model's own + defaults. + + It is also the only place the refusal-oracle property can honestly be + tested: "not yours" and "no such adapter" have to come from two GENUINELY + different database states, which a single mocked lookup cannot produce. + """ + + def setUp(self): + self.org = Organization.objects.create( + display_name="Acme", organization_id="org_acme", name="acme" + ) + self.other = Organization.objects.create( + display_name="Other", organization_id="org_other", name="other" + ) + self.key = mock.Mock(organization_id=self.org.id) + UserContext.set_organization_identifier(None) + + def _adapter(self, *, org=None, adapter_type="LLM", **kwargs): + return AdapterInstance.objects.create( + adapter_name=f"a-{uuid.uuid4().hex[:8]}", + adapter_id="anthropic|abc", + adapter_type=adapter_type, + adapter_metadata={}, + organization=org or self.org, + **kwargs, + ) + + def _entry(self, **roles): + return {"name": "table", "adapters": {r: str(v) for r, v in roles.items()}} + + def _full_entry(self, **overrides): + roles = { + "llm": self._adapter().id, + "lite_llm": self._adapter().id, + "x2text": self._adapter(adapter_type="X2TEXT").id, + } + roles.update(overrides) + return self._entry(**roles) + + def test_a_fully_valid_set_of_the_org_s_own_adapters_resolves(self): + """The happy path, with every flag coming from the DB's own defaults. + + `is_usable` and `is_available` both default True at the model, so this + also pins that the two new refusals do not reject ordinary adapters + nobody has touched. + """ + entry = self._full_entry() + + self.assertEqual(_resolved_adapters(entry, self.key), entry["adapters"]) + + def test_a_wrong_type_is_refused_against_a_real_row(self): + """An X2TEXT id in the `llm` slot. + + Previously asserted only against a mock that was told to report + `adapter_type="X2TEXT"`; this reads the real column, so the comparison + against `AdapterTypes.LLM.value` is pinned to the value the DB stores. + """ + entry = self._full_entry(llm=self._adapter(adapter_type="X2TEXT").id) + + with self.assertRaises(ValidationError) as caught: + _resolved_adapters(entry, self.key) + + self.assertIn("expected 'LLM'", str(caught.exception)) + + def test_an_exhausted_trial_adapter_is_refused(self): + """A1. `is_usable=False` is how billing cuts off a frictionless trial. + + The platform service returns the credentials regardless, so this gate + is the only thing that stops an operator-funded extraction here. + """ + entry = self._full_entry(llm=self._adapter(is_usable=False).id) + + with self.assertRaises(ValidationError) as caught: + _resolved_adapters(entry, self.key) + + self.assertIn("exhausted", str(caught.exception)) + + def test_a_deprecated_adapter_is_refused(self): + entry = self._full_entry(llm=self._adapter(is_available=False).id) + + with self.assertRaises(ValidationError) as caught: + _resolved_adapters(entry, self.key) + + self.assertIn("deprecated", str(caught.exception)) + + def test_another_org_s_adapter_and_a_nonexistent_one_refuse_identically(self): + """The oracle property, from two genuinely different DB states. + + If these two messages ever diverge, `POST /agent-kv/` becomes an oracle + for which adapter UUIDs are real in OTHER organizations: a caller + brute-forcing ids learns "exists but not yours" from "does not exist". + + The mocked version of this test could not fail -- one patched lookup + returns None for both cases, so it compared one branch's message to + itself. Here one id is a real row owned by `self.other` and the other + is a UUID no row has. + """ + foreign = self._adapter(org=self.other).id + nowhere = uuid.uuid4() + + messages = [] + for adapter_id in (foreign, nowhere): + with self.assertRaises(ValidationError) as caught: + _resolved_adapters(self._full_entry(llm=adapter_id), self.key) + messages.append(str(caught.exception)) + + self.assertEqual( + messages[0], + messages[1], + "a foreign adapter and a nonexistent one must be indistinguishable; " + f"got {messages[0]!r} vs {messages[1]!r}", + ) + + def test_no_adapters_requested_is_a_no_op(self): + """The `kv` shape: env-configured extractors send `{}` and must pass.""" + self.assertEqual(_resolved_adapters({"name": "kv", "adapters": {}}, self.key), {}) + + +class TestTheJobRowRecordsWhatItSpent(TestCase): + """The adapters a run was dispatched with must be recoverable AFTERWARDS. + + Adapter choice is per-request, caller-controlled and cost-bearing on the + `table` path. Before the `adapters` column the chosen ids reached + `executor_params` and nothing else -- not the job row, not the status + document, not `usage_summary` -- so "which model did job X use?", the first + question in any billing dispute, needed a `usage_v2` join on `run_id` that + the API cannot perform and the customer cannot see. + + DB-backed because the point is that the value SURVIVES the request. + """ + + def setUp(self): + self.org = Organization.objects.create( + display_name="Acme", organization_id="org_acme", name="acme" + ) + UserContext.set_organization_identifier(None) + + def _job(self, **kwargs): + from agent_kv.models import AgentKVJob, JobExtractor + + job = AgentKVJob( + organization_id=self.org.id, + extractor=JobExtractor.TABLE, + **kwargs, + ) + job.save() + return job + + def test_the_adapters_round_trip_through_the_database(self): + adapters = { + "llm": str(uuid.uuid4()), + "lite_llm": str(uuid.uuid4()), + "x2text": str(uuid.uuid4()), + } + + job = self._job(adapters=adapters) + job.refresh_from_db() + + assert job.adapters == adapters + + def test_the_status_document_reports_them(self): + from agent_kv.execution_views import _status_document + + adapters = {"llm": str(uuid.uuid4())} + doc = _status_document(self._job(adapters=adapters)) + + assert doc["adapters"] == adapters, doc + + def test_a_job_with_no_adapters_reports_an_empty_dict_not_null(self): + """`kv` is env-configured and names none, and rows predating the column + genuinely ran on operator credentials -- `{}` is the honest value for + both, and a stable type is easier for a client than `null | object`. + """ + from agent_kv.execution_views import _status_document + + job = self._job() + + assert job.adapters == {} + assert _status_document(job)["adapters"] == {} + + def test_no_adapter_metadata_is_ever_stored_on_the_job(self): + """The column is returned to the caller, so it must carry ids only. + + `AdapterInstance.adapter_metadata` holds the PROVIDER CREDENTIALS. A + future change that widened this field to "the adapter" rather than "its + id" would publish those to whoever submitted the job. + """ + adapters = {"llm": str(uuid.uuid4()), "x2text": str(uuid.uuid4())} + job = self._job(adapters=adapters) + job.refresh_from_db() + + for role, value in job.adapters.items(): + assert isinstance(value, str), (role, value) + # A bare UUID string, not a serialized adapter. + uuid.UUID(value) diff --git a/backend/agent_kv/tests/test_cross_repo_stage_name.py b/backend/agent_kv/tests/test_cross_repo_stage_name.py index 00255a7169..fe69212c1b 100644 --- a/backend/agent_kv/tests/test_cross_repo_stage_name.py +++ b/backend/agent_kv/tests/test_cross_repo_stage_name.py @@ -41,3 +41,55 @@ def test_the_table_extractor_reports_exactly_one_stage(): be recorded as missing rather than pending, for every job.""" assert TABLE_STAGE_NAMES == [CLOUD_STAGE_TABLE_EXTRACTION] assert TABLE_EXTRACTOR_NAME == "table" + + +# --------------------------------------------------------------------------- +# Second cross-repo lock in this pair: the stage-STATUS allowlist. +# +# Same shape as the stage-name lock above and the same class of silent drift, +# but one layer along. `StageReportView` rejects a status outside +# `_VALID_STAGE_STATUSES` with a 400 -- and the cloud reporter +# (`extraction_seams.progress.StageReporter.report`) SWALLOWS 400s by design, +# because its contract is that a failed progress report must never fail a +# paid run. So if the two sets drift, every affected job completes and bills +# normally while its reported stage stays frozen at the last value that +# happened to be accepted. +# +# The cloud side already carried a `!! CROSS-REPO CONSTANT !!` banner on its +# copy with nothing asserting against this one. The cloud twin of this test is +# `workers/plugins/extraction_seams/tests/test_progress.py`. +# --------------------------------------------------------------------------- + +#: Must equal ``VALID_STAGE_STATUSES`` in the cloud repo's +#: ``workers/plugins/extraction_seams/src/progress.py``. Change one, change +#: both, in the same PR. +#: +#: A literal, not an import: the two repos are separate checkouts and only meet +#: in the merged tree, so an import would make this pass vacuously wherever the +#: other side is absent -- exactly the environments where drift is introduced. +CLOUD_VALID_STAGE_STATUSES = frozenset({"running", "done"}) + + +def test_the_accepted_stage_statuses_match_what_the_executor_sends(): + from agent_kv.internal_views import _VALID_STAGE_STATUSES + + assert _VALID_STAGE_STATUSES == CLOUD_VALID_STAGE_STATUSES, ( + "_VALID_STAGE_STATUSES no longer equals VALID_STAGE_STATUSES in the " + "cloud repo's workers/plugins/extraction_seams/src/progress.py. Stage " + "reports will be rejected with a 400 that the cloud reporter swallows, " + "so jobs will complete and bill while their reported stage freezes. " + "Update both sides, and both twins of this test, together." + ) + + +def test_a_terminal_outcome_is_not_an_accepted_stage_status(): + """Why the set is only two values, asserted rather than described. + + Terminal outcomes travel on finalize. `"failed"` as a stage status is the + case that actually shipped once, and accepting it here would let a job + report a terminal stage while its row stayed non-terminal. + """ + from agent_kv.internal_views import _VALID_STAGE_STATUSES + + for terminal in ("failed", "completed", "cancelled", "timed_out"): + assert terminal not in _VALID_STAGE_STATUSES, terminal diff --git a/backend/agent_kv/tests/test_job_views.py b/backend/agent_kv/tests/test_job_views.py index 4416027756..48d34b34e6 100644 --- a/backend/agent_kv/tests/test_job_views.py +++ b/backend/agent_kv/tests/test_job_views.py @@ -308,6 +308,7 @@ def test_cancel_win_releases_concurrency_slot(m_keys, m_jobs, m_mark_terminal, m job = AgentKVJob(status=JobStatus.PENDING) job.organization_id = "org1" m_jobs.get.return_value = job + _dispatched_in_db(m_jobs, False) resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) @@ -315,6 +316,17 @@ def test_cancel_win_releases_concurrency_slot(m_keys, m_jobs, m_mark_terminal, m m_release.assert_called_once_with("org1", str(job.id)) +def _dispatched_in_db(m_jobs, dispatched: bool) -> None: + """Wire the DB re-read `_never_dispatched` performs. + + It no longer reads `job.dispatched_at` off the in-memory row -- that row + predates `mark_terminal`, and a dispatch completing in that gap made the + cancel release a slot whose executor was still running (round-2 C1). It now + asks the database, so these tests have to say what the database would + answer. + """ + m_jobs.filter.return_value.exists.return_value = dispatched + # --------------------------------------------------------------------------- # (6c) cancel that LOSES the guard (job already terminal) must NOT release -- # whoever terminalized it (finalize callback / a prior cancel) owns the slot. @@ -488,6 +500,7 @@ def test_delete_on_a_never_dispatched_job_DOES_release_the_slot( job.organization_id = "org1" job.dispatched_at = None m_jobs.get.return_value = job + _dispatched_in_db(m_jobs, False) resp = ev.JobDeleteView.as_view()(_authed(method="delete"), job_id=uuid.uuid4()) @@ -806,6 +819,7 @@ def test_cancel_on_a_never_dispatched_job_DOES_release_the_slot( job.organization_id = "org1" job.dispatched_at = None m_jobs.get.return_value = job + _dispatched_in_db(m_jobs, False) resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) @@ -813,15 +827,58 @@ def test_cancel_on_a_never_dispatched_job_DOES_release_the_slot( m_release.assert_called_once_with("org1", str(job.id)) -def test_a_dispatched_but_still_pending_job_is_not_treated_as_undispatched(): - """`dispatched_at` and `status` are checked together on purpose. - - Post-enqueue bookkeeping can fail after the task is already on the queue - (dispatch.py wraps it precisely because losing it must not fail the - dispatch), leaving a row that still reads PENDING while an executor runs. - Releasing that slot would over-subscribe exactly as the RUNNING case did. +@mock.patch.object(ev, "dispatch_cancelled_webhook") +@mock.patch.object(ev.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +@mock.patch.object(AgentKVKey, "objects") +def test_a_cancel_that_raced_a_dispatch_does_NOT_release_the_slot( + m_keys, m_jobs, m_mark_terminal, m_release, m_hook +): + """Round-2 C1. The in-memory row says undispatched; the database disagrees. + + The interleaving: the cancel reads a PENDING row with `dispatched_at=NULL`, + then `dispatch_job` passes its own terminal re-read, enqueues, and stamps + `dispatched_at` / DISPATCHED. The cancel's `mark_terminal` still wins + (DISPATCHED is not terminal), and the old check -- reading + `job.dispatched_at` off the snapshot -- answered True and released a slot + whose executor was running and billing. + + That is a smaller instance of the very over-release bug the narrowing was + written to fix. The row here is deliberately left looking undispatched, so + the ONLY thing that can produce the right answer is the DB re-read. """ + m_keys.get.return_value = kv_key() job = AgentKVJob(status=JobStatus.PENDING) + job.organization_id = "org1" + job.dispatched_at = None # the stale snapshot, as the cancel path sees it + m_jobs.get.return_value = job + _dispatched_in_db(m_jobs, True) # what actually happened meanwhile + + resp = ev.JobCancelView.as_view()(_authed(method="post"), job_id=uuid.uuid4()) + + assert resp.status_code == 200 + assert not m_release.called, ( + "released a slot for a job that HAD been dispatched -- the executor is " + "still running and its finalize callback owns the slot" + ) + # The caller is still told, since cancellation never reaches finalize. + assert m_hook.called + + +@mock.patch.object(AgentKVJob, "objects") +def test_the_undispatched_check_asks_the_database_not_the_row(m_jobs): + """Pins the mechanism, so the C1 regression cannot return quietly. + + If `_never_dispatched` goes back to reading `job.dispatched_at`, this fails: + the row says dispatched (which the old code would honour) while the DB says + it is not, and only the DB answer is correct after a `mark_terminal`. + """ + job = AgentKVJob(status=JobStatus.CANCELLED) job.dispatched_at = timezone.now() + _dispatched_in_db(m_jobs, False) + + assert ev._never_dispatched(job) is True - assert not ev._never_dispatched(job) + _dispatched_in_db(m_jobs, True) + assert ev._never_dispatched(job) is False diff --git a/backend/agent_kv/tests/test_periodic_tasks_migration.py b/backend/agent_kv/tests/test_periodic_tasks_migration.py index 662db83338..f03c4a7c85 100644 --- a/backend/agent_kv/tests/test_periodic_tasks_migration.py +++ b/backend/agent_kv/tests/test_periodic_tasks_migration.py @@ -1,22 +1,35 @@ -"""The two maintenance periodics must be scheduled, not merely registered. - -Both tasks were registered in ``workers/scheduler/agent_kv_tasks.py`` and both -internal endpoints existed, but nothing anywhere created a row to fire them. -"Wired but unscheduled" is an invisible failure: the code looks complete, the -endpoints respond to a manual call, and the only symptoms are that stuck jobs -keep their concurrency slots until a 6h Redis TTL and that customer documents -are retained forever despite a documented ``AGENT_KV_RESULT_TTL_DAYS``. - -These assertions pin the migration's declared rows against the wire names the -worker actually registers, so renaming one side without the other fails here -rather than silently stopping the schedule. - -Reported by Greptile on PR #2317, and as 2.3 in the branch review. +"""One owner for the two maintenance schedules, and it is the CronJob. + +History, because the test's shape only makes sense with it: an earlier revision +of migration 0004 SEEDED two ``PgPeriodicTask`` rows to fire +``agent_kv.sweep`` and ``agent_kv.ttl_cleanup``, answering a review finding +about the sweep never running. The finding was real; the fix was redundant. +``templates/backend/agent-kv-cronjobs.yaml`` in the cloud chart already ran the +same two management commands, added earlier on the same branch, so the +migration's stated premise -- "no ``PgPeriodicTask``, no beat entry, no +CronJob" -- was false about its own tree. + +The rows were seeded ``pg_owned: False`` and the PG scheduler claims only +``WHERE pg_owned AND enabled``, so nothing double-fired yet. That is the +dangerous kind of correct: the flag exists to be flipped, and a flip would have +put a ``*/10`` sweep against the chart's ``*/15`` -- colliding at ``:30``, +contending on the same job rows -- with TTL cadences disagreeing 24x. + +So 0004 now DELETES those rows and the CronJob is the sole owner. What these +tests defend is that decision, in the two ways it can be undone: someone +re-pointing the forward operation at the seeding function, or someone dropping +the reverse and making the migration one-way. + +The name/queue assertions are kept rather than deleted -- they still guard the +reverse path, and the same wire names are what the CronJob's management +commands resolve to. """ import importlib +import inspect import pytest +from django.db import migrations # The module name starts with a digit, so it cannot be a plain `import`. MIGRATION = importlib.import_module("agent_kv.migrations.0004_pg_periodic_tasks") @@ -33,18 +46,86 @@ def _specs() -> dict: return {spec["name"]: spec for spec in MIGRATION.PG_PERIODIC_TASKS} -def test_both_maintenance_tasks_are_scheduled(): +def _run_python_ops() -> list: + return [ + op + for op in MIGRATION.Migration.operations + if isinstance(op, migrations.RunPython) + ] + + +# --------------------------------------------------------------------------- +# The ownership decision. +# --------------------------------------------------------------------------- + + +def test_the_migration_still_has_a_runpython_operation(): + """Guards the empty-operations failure mode. + + Every other assertion in this file reads `PG_PERIODIC_TASKS` or a function's + source, so `operations = []` would leave the whole suite green while the + migration did nothing at all -- applying cleanly and leaving the seeded rows + in place in every environment that already has them. + """ + assert len(_run_python_ops()) == 1, MIGRATION.Migration.operations + + +def test_the_forward_operation_removes_the_rows_rather_than_seeding_them(): + """The decision itself. + + If this fails, someone has re-pointed forward at `create_pg_periodic_tasks` + and there are two owners for one schedule again. Read the migration's + docstring before changing it: the collision is at `:30`, and only the + CronJob side carries `concurrencyPolicy: Forbid` and a deadline. + """ + assert _run_python_ops()[0].code is MIGRATION.remove_pg_periodic_tasks + + +def test_the_migration_is_reversible(): + """`migrate agent_kv 0003` must land back on the previous tree's state. + + An irreversible data migration here would mean a rollback of this release + leaves an environment with no rows and no way to restore them except by + hand -- and the seeding revision is what the previous release shipped. + """ + op = _run_python_ops()[0] + + assert op.reverse_code is MIGRATION.create_pg_periodic_tasks + assert op.reverse_code is not migrations.RunPython.noop + + +def test_removal_is_idempotent_and_targets_only_the_two_seeded_names(): + """A fresh install has no such rows, and re-running must still succeed. + + Equally important: the delete is scoped by name. An unscoped + `PgPeriodicTask.objects.all().delete()` here would wipe the + dashboard_metrics periodics this migration sits alongside. + """ + source = inspect.getsource(MIGRATION.remove_pg_periodic_tasks) + + assert "name__in=" in source, "the delete must be scoped by name" + assert ".all()" not in source + + +# --------------------------------------------------------------------------- +# The reverse path's contents, and the names the CronJob commands share. +# --------------------------------------------------------------------------- + + +def test_both_maintenance_tasks_are_accounted_for(): assert set(_specs()) == {"agent_kv_sweep", "agent_kv_ttl_cleanup"} -def test_every_scheduled_row_points_at_a_task_the_worker_registers(): +def test_every_row_points_at_a_task_the_worker_registers(): """A row naming a task nothing registers is a schedule that fires into - nothing -- the same invisible failure, one layer along. + nothing. Still live for the reverse path, and the same wire names back the + `agent_kv_sweep` / `agent_kv_ttl_cleanup` management commands the CronJob + invokes. """ declared = {spec["task_name"] for spec in MIGRATION.PG_PERIODIC_TASKS} assert declared == WORKER_TASK_NAMES, ( - f"migration schedules {sorted(declared)} but workers/scheduler/" + f"migration names {sorted(declared)} but workers/scheduler/" f"agent_kv_tasks.py registers {sorted(WORKER_TASK_NAMES)}" ) @@ -54,22 +135,6 @@ def test_rows_are_scheduled_onto_the_scheduler_queue(name): assert _specs()[name]["queue"] == "scheduler" -def test_rows_are_platform_wide_and_enabled(): - """Both internal endpoints sweep across every organization (spec §5.4), so a - non-empty `org_id` would scope them to one tenant and silently leave every - other org unswept. - - Asserted on the writer rather than the spec table: `org_id`/`enabled` are - set once in `defaults`, not per row. - """ - import inspect - - source = inspect.getsource(MIGRATION.create_pg_periodic_tasks) - - assert '"org_id": ""' in source - assert '"enabled": True' in source - - @pytest.mark.parametrize("name", ["agent_kv_sweep", "agent_kv_ttl_cleanup"]) def test_cron_strings_are_five_field_and_parseable(name): """A malformed cron string is accepted by the column and then never fires.""" @@ -78,28 +143,24 @@ def test_cron_strings_are_five_field_and_parseable(name): assert len(fields) == 5, _specs()[name]["cron_string"] -def test_the_sweep_runs_far_more_often_than_the_ttl_cleanup(): - """Not cosmetic: a stuck job holds a concurrency slot, so the sweep has to - be frequent, while the TTL cleanup is a bulk object-store delete that should - not run on a tight loop. If these ever converge, one of them is wrong. +def test_the_restored_rows_are_inert(): + """`pg_owned: False` on the reverse path too. + + A reverse that restored them ENABLED would turn a rollback into the + double-fire this migration exists to prevent. """ - sweep = _specs()["agent_kv_sweep"]["cron_string"] - ttl = _specs()["agent_kv_ttl_cleanup"]["cron_string"] + source = inspect.getsource(MIGRATION.create_pg_periodic_tasks) - assert sweep.startswith("*/"), f"sweep should be interval-based, got {sweep!r}" - assert not ttl.startswith("*/"), f"ttl cleanup should be daily, got {ttl!r}" + assert '"pg_owned": False' in source + assert "update_or_create" in source, "re-running must stay idempotent" -def test_rows_are_created_inert(): - """`pg_owned: False` means applying the migration does not itself start - firing them -- the rollout flag decides, same posture as the - dashboard_metrics periodics this follows. +def test_restored_rows_are_platform_wide(): + """Both internal endpoints sweep across every organization (spec §5.4), so a + non-empty `org_id` would scope them to one tenant and silently leave every + other org unswept. """ - # The flag lives in the migration's writer, not its spec table, so assert on - # the source rather than re-running the RunPython against a live DB. - import inspect - source = inspect.getsource(MIGRATION.create_pg_periodic_tasks) - assert '"pg_owned": False' in source - assert "update_or_create" in source, "re-running the migration must be idempotent" + assert '"org_id": ""' in source + assert '"enabled": True' in source diff --git a/backend/agent_kv/tests/test_queue_wiring_is_derived.py b/backend/agent_kv/tests/test_queue_wiring_is_derived.py new file mode 100644 index 0000000000..e2c9109e82 --- /dev/null +++ b/backend/agent_kv/tests/test_queue_wiring_is_derived.py @@ -0,0 +1,297 @@ +"""Fleet queue wiring, DERIVED from `EXTRACTOR_ROUTES` rather than literal-pinned. + +`workers/tests/test_queue_consumer_wiring.py` guards the same property with +hardcoded queue names (`UNSERVED_KV_QUEUE`, `AGENT_TABLE_QUEUE`). That works +today and catches a real, expensive failure -- but it cannot survive the change +it most needs to survive. Re-enabling the `kv` extractor means adding one entry +to `EXTRACTOR_ROUTES`, at which point `celery_executor_agentic_kv` flips from +"must NOT be advertised" to "must be served", and the literal-pinned assertion +has to be *deleted* to make the suite pass. An assertion whose correct response +to a change is deletion is not protecting anything at that moment. + +This suite derives both directions from the routing table, so the same change +flips the expectation automatically: + +* Every executor in `EXTRACTOR_ROUTES` must have its `celery_executor_` + queue served by every fleet configuration. Unserved means jobs are accepted + and sit in DISPATCHED forever -- `enqueue_task` succeeds, rows land in + `pg_queue_message`, nothing errors at the producer. +* Every executor this module names but deliberately does NOT route + (`EXECUTOR_NAME` = `agentic_kv`, kept dormant by the carve-out) must NOT be + advertised anywhere. Same silent failure from the other side. + +Lives in the backend suite because that is where `EXTRACTOR_ROUTES` is +importable; the fleet configs it reads are plain text. The workers-side suite +keeps its literal assertions as a cheap second opinion -- two independent +statements of the same fact, which is the point. + +**Every path is guarded by `pytest.skip` when absent.** A cross-tree read that +assumed a file existed has reddened CI on this branch once already: the backend +test venv runs from `backend/`, and nothing guarantees a packaging step kept +`docker/` or `workers/` alongside it. +""" + +import re +from pathlib import Path + +import pytest +import yaml + +from agent_kv.constants import ( + EXECUTOR_NAME, + EXTRACTOR_ROUTES, + TABLE_EXECUTOR_NAME, +) + +REPO_ROOT = Path(__file__).resolve().parents[3] +DEV_COMPOSE = REPO_ROOT / "docker" / "docker-compose.yaml" +TEST_COMPOSE = REPO_ROOT / "tests" / "compose" / "docker-compose.test.yaml" +RUN_WORKER = REPO_ROOT / "workers" / "run-worker.sh" +RUN_WORKER_DOCKER = REPO_ROOT / "workers" / "run-worker-docker.sh" + +PG_QUEUE_VAR = "WORKER_PG_QUEUE_CONSUMER_QUEUE" + +#: Every executor name this module knows about. The ones in `EXTRACTOR_ROUTES` +#: must be served; the rest must not be advertised. Derived, so adding a route +#: moves a name from one set to the other with no edit here. +ALL_KNOWN_EXECUTORS = {EXECUTOR_NAME, TABLE_EXECUTOR_NAME} + + +def _queue_name(executor: str) -> str: + """How `pg_queue` derives a queue from an executor name. + + Mirrors the dispatcher's own derivation. If that convention changes, every + assertion here goes stale together rather than one at a time. + """ + return f"celery_executor_{executor}" + + +def _routed_executors() -> set[str]: + return {executor for executor, _operation in EXTRACTOR_ROUTES.values()} + + +def _unrouted_executors() -> set[str]: + return ALL_KNOWN_EXECUTORS - _routed_executors() + + +def _require(path: Path) -> str: + if not path.is_file(): + pytest.skip(f"{path} is not present in this tree") + return path.read_text() + + +def _queues(raw: str) -> set[str]: + """Queue names from a consumer list, ignoring a `${VAR:-default}` wrapper.""" + inner = re.sub(r"^\$\{[^:}]+:-(.*)\}$", r"\1", raw.strip()) + return {q.strip() for q in inner.split(",") if q.strip()} + + +def _compose_executor_queues(path: Path) -> set[str]: + doc = yaml.safe_load(_require(path)) + env = (doc.get("services", {}).get("worker-pg-executor") or {}).get( + "environment" + ) or [] + if isinstance(env, dict): + values = {k: str(v) for k, v in env.items()} + else: + values = {} + for item in env: + key, _, value = str(item).partition("=") + values[key] = value + raw = values.get(PG_QUEUE_VAR) + assert raw is not None, ( + f"{path.name}: worker-pg-executor sets no {PG_QUEUE_VAR}, so the " + f"running pg-queue-consumer falls back to a default of unknown content" + ) + return _queues(raw) + + +def _shell_role_queues(path: Path, pattern: str) -> set[str]: + match = re.search(pattern, _require(path)) + assert match, f"could not find {pattern!r} in {path.name}" + return _queues(match.group(1)) + + +#: Every place a fleet declares which executor queues it drains. The two shell +#: runners are included because a queue missing from a launcher is exactly as +#: undrained as one missing from compose -- and `run-worker-docker.sh` was +#: guarded by nothing at all. +def _all_executor_fleets() -> dict: + return { + "dev compose": lambda: _compose_executor_queues(DEV_COMPOSE), + "e2e test compose": lambda: _compose_executor_queues(TEST_COMPOSE), + "run-worker.sh": lambda: _shell_role_queues( + RUN_WORKER, r'\["\$PG_ROLE_EXECUTOR"\]="executor;([^"]+)"' + ), + } + + +@pytest.mark.parametrize("fleet", sorted(_all_executor_fleets())) +def test_every_routed_executor_has_a_consumer(fleet): + """The forward direction, derived. + + A route with no consumer accepts work and drains nothing, silently -- the + failure that cost this team ~30 hours of firings with zero executions. + """ + served = _all_executor_fleets()[fleet]() + expected = {_queue_name(e) for e in _routed_executors()} + + missing = expected - served + assert not missing, ( + f"{fleet} serves none of {sorted(missing)}, but EXTRACTOR_ROUTES " + f"dispatches there. Jobs will be accepted and sit in DISPATCHED " + f"forever with no error at the producer." + ) + + +@pytest.mark.parametrize("fleet", sorted(_all_executor_fleets())) +def test_no_fleet_advertises_an_unrouted_executor(fleet): + """The inverse direction, derived. + + `agentic_kv` is the live case: this deployment ships no such plugin, so a + fleet listing its queue would accept work nothing can drain. When `kv` is + re-enabled by adding its `EXTRACTOR_ROUTES` entry, this expectation inverts + on its own and `test_every_routed_executor_has_a_consumer` starts requiring + the queue instead -- which is the whole reason this is derived. + """ + served = _all_executor_fleets()[fleet]() + forbidden = {_queue_name(e) for e in _unrouted_executors()} + + advertised = forbidden & served + assert not advertised, ( + f"{fleet} advertises {sorted(advertised)}, but no EXTRACTOR_ROUTES " + f"entry dispatches there and this deployment carries no such plugin. " + f"Remove it, or add the route and ship the plugin." + ) + + +def test_the_derivation_is_not_vacuous(): + """Both sets must be non-empty, or the two tests above assert nothing. + + If `EXTRACTOR_ROUTES` were emptied, `test_every_routed_executor_...` would + pass with an empty expectation -- green while nothing was wired. If every + known executor were routed, the inverse test would pass trivially. This is + the guard on the guards. + """ + assert _routed_executors(), "no routed executors: the forward test is vacuous" + assert _unrouted_executors(), ( + "every known executor is routed, so the inverse test is vacuous. If " + "`kv` was deliberately re-enabled, that is correct -- delete this " + "assertion and say so." + ) + + +# --------------------------------------------------------------------------- +# The callback queue, across BOTH launchers. +# +# `run-worker-docker.sh` carries its own `["ide_callback"]= +# "ide_callback,agent_kv_callback"` map and was guarded by nothing: dropping +# `agent_kv_callback` there passed every test in the tree while a +# docker-launched fleet drained no terminal callbacks, leaving every job +# RUNNING forever. +# --------------------------------------------------------------------------- + +CALLBACK_QUEUE = "agent_kv_callback" + + +def test_the_docker_launcher_drains_the_terminal_callback_queue(): + """The launcher the workers-side suite does not cover.""" + queues = _shell_role_queues( + RUN_WORKER_DOCKER, r'\["ide_callback"\]="([^"]+)"' + ) + + assert CALLBACK_QUEUE in queues, ( + f"run-worker-docker.sh's ide_callback role omits {CALLBACK_QUEUE}; a " + f"docker-launched fleet would run jobs to completion and never " + f"terminalize them -- finalize is what persists the result, deletes " + f"the staged input, releases the slot and fires the webhook." + ) + + +def test_the_host_launcher_drains_the_terminal_callback_queue(): + """Already covered on the workers side; repeated here so the two launchers + are asserted side by side and a reader can see neither is missing. + """ + queues = _shell_role_queues( + RUN_WORKER, r'\["\$PG_ROLE_IDE_CALLBACK"\]="ide_callback;([^"]+)"' + ) + + assert CALLBACK_QUEUE in queues + + +# --------------------------------------------------------------------------- +# The billing backstop, DERIVED from `EXTRACTOR_ROUTES` the same way. +# +# `executor.tasks._LLM_BEARING_OPS` is the set of operations that must log when +# a successful run emits no usage records. That log line is the ONLY signal of +# total billing loss: the cloud `flush()` returns an empty list rather than +# raising, so "the billing chain broke" and "this job legitimately made no LLM +# calls" are otherwise indistinguishable. An op missing from the set is a +# missing ALARM, and the symptom is silence. +# +# `workers/tests/test_llm_bearing_ops.py` guards it against a hand-maintained +# `PAID_OPERATIONS` twin, which enforces symmetry between two literals -- so it +# can only fail when the copies DISAGREE, never when both are wrong together. A +# new paid extractor added to `EXTRACTOR_ROUTES` and declared in neither list +# ships with no billing alarm and nothing goes red. +# +# Every operation this API routes drives LLM calls by construction -- that is +# what an Agent-KV extractor IS -- so the set can be derived rather than +# declared. +# +# Read as TEXT and parsed with `ast`, not imported: `executor.tasks` is a +# workers package and is not importable from the backend's test venv (the +# mirror of why the workers-side suite cannot import `EXTRACTOR_ROUTES`). An +# import would make this pass vacuously in exactly the environment where the +# drift happens. Skipped if the file is absent from this tree. +# --------------------------------------------------------------------------- + +EXECUTOR_TASKS = REPO_ROOT / "workers" / "executor" / "tasks.py" + + +def _llm_bearing_ops_literal() -> set[str]: + """Pull `_LLM_BEARING_OPS`' string members out of the source.""" + import ast + + tree = ast.parse(_require(EXECUTOR_TASKS)) + for node in ast.walk(tree): + if not isinstance(node, ast.Assign): + continue + targets = [t.id for t in node.targets if isinstance(t, ast.Name)] + if "_LLM_BEARING_OPS" not in targets: + continue + return { + element.value + for element in ast.walk(node.value) + if isinstance(element, ast.Constant) and isinstance(element.value, str) + } + pytest.fail(f"no `_LLM_BEARING_OPS` assignment found in {EXECUTOR_TASKS}") + + +def test_every_routed_operation_has_a_billing_backstop(): + """The derived assertion the two literal lists cannot make. + + Add an extractor to `EXTRACTOR_ROUTES` and forget the backstop, and this + fails -- without anyone having had to remember a second list. + """ + declared = _llm_bearing_ops_literal() + routed_ops = {operation for _executor, operation in EXTRACTOR_ROUTES.values()} + + missing = routed_ops - declared + assert not missing, ( + f"{sorted(missing)} are routed by EXTRACTOR_ROUTES but absent from " + f"`_LLM_BEARING_OPS` in workers/executor/tasks.py. Every Agent-KV " + f"extractor drives LLM calls, so a run of one that emits zero usage " + f"records means the billing chain broke -- and without the set " + f"membership, nothing logs it. Add the op there (and to " + f"workers/tests/test_llm_bearing_ops.py's PAID_OPERATIONS)." + ) + + +def test_the_backstop_derivation_is_not_vacuous(): + """If the AST parse silently returned nothing, the test above would pass.""" + assert _llm_bearing_ops_literal(), ( + "parsed no operation names out of `_LLM_BEARING_OPS` -- the assignment " + "shape changed and the derivation above is now asserting against an " + "empty set" + ) diff --git a/backend/agent_kv/tests/test_submit_view.py b/backend/agent_kv/tests/test_submit_view.py index 00e6a21b8e..e3c285830c 100644 --- a/backend/agent_kv/tests/test_submit_view.py +++ b/backend/agent_kv/tests/test_submit_view.py @@ -118,7 +118,9 @@ def _mock_serializer(m_cls, **overrides): _ROLE_TYPES = {"llm": "LLM", "lite_llm": "LLM", "x2text": "X2TEXT"} -def _adapters_owned_by(organization_id, *, types=None, missing=()): +def _adapters_owned_by( + organization_id, *, types=None, missing=(), unusable=(), unavailable=() +): """Patch the single-adapter lookup `_resolved_adapters` performs. Patches `ev._lookup_adapter`, the view's own seam, rather than the ORM. @@ -135,18 +137,35 @@ def _adapters_owned_by(organization_id, *, types=None, missing=()): An id in `missing` resolves to None (not this org's, or nonexistent -- the gate cannot and must not tell those apart); everything else resolves to an adapter of the type `types` gives, defaulting to the right one per role. + `unusable` / `unavailable` flip `is_usable` / `is_available` for the ids + named, which is how the trial-exhaustion and deprecation refusals are + reached here. + + Both flags are set EXPLICITLY on every stand-in, never left to `Mock`'s + auto-attribute. A bare `mock.Mock()` returns a truthy child for any + attribute, so a gate that reads `adapter.is_usable` would be satisfied by a + mock that was never asked to model it -- the gate could be deleted and + these tests would still pass. The real values live in + `tests/test_adapter_scoping.py`, against rows the DB defaulted itself. """ type_by_id = { aid: (types or {}).get(role, _ROLE_TYPES[role]) for role, aid in _TABLE_ADAPTERS.items() } missing_ids = {str(m) for m in missing} + unusable_ids = {str(m) for m in unusable} + unavailable_ids = {str(m) for m in unavailable} def _lookup(adapter_id, org_id): aid = str(adapter_id) if aid in missing_ids or org_id != organization_id: return None - return mock.Mock(adapter_type=type_by_id.get(aid, "LLM")) + return mock.Mock( + id=aid, + adapter_type=type_by_id.get(aid, "LLM"), + is_usable=aid not in unusable_ids, + is_available=aid not in unavailable_ids, + ) return mock.patch.object(ev, "_lookup_adapter", side_effect=_lookup) @@ -948,12 +967,17 @@ def test_a_save_failure_after_staging_removes_the_staged_object( # --------------------------------------------------------------------------- # The adapter tenancy gate. # -# `POST /agent-kv/` names platform adapters by id, and the executor resolves -# them through the platform service WITHOUT re-checking whose they are. So the -# submit-time org filter is the only thing standing between a caller and -# another tenant's LLM credential. Same class as the `organization_id` filter -# on every job-scoped view, and higher consequence: that one leaks a job, this -# one spends someone else's money. +# `POST /agent-kv/` names platform adapters by id. The platform service DOES +# re-scope its lookups by organization (`WHERE id=%s and organization_id=%s`, +# against the org of the job's own platform key), so this gate is defence in +# depth rather than the only thing standing between a caller and another +# tenant's credential -- the comment here previously claimed the latter. +# +# It is still load-bearing for what the platform service does not do: produce a +# 400 at submit naming the role (instead of an `SdkError` mid-run, after a slot +# and staging are spent), and refuse an exhausted trial (`is_usable`) or a +# deprecated adapter (`is_available`), neither of which the platform service +# checks before handing the credentials back. # --------------------------------------------------------------------------- @@ -1042,36 +1066,81 @@ def test_an_adapter_of_the_wrong_type_is_refused( @mock.patch.object(ev, "check_key_rate", return_value=True) @mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) @mock.patch.object(AgentKVKey, "objects") -def test_the_refusal_does_not_reveal_whether_the_adapter_exists( +def test_an_exhausted_trial_adapter_is_refused_before_anything_is_billed( + m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch +): + """`is_usable=False` must 400, not 202. + + A frictionlessly onboarded org runs on operator-funded sample credentials; + billing flips `is_usable` when the free allowance is gone, and the IDE, + workflows and Prompt Studio all refuse from that moment. The platform + service does NOT check the flag -- it hands the credentials back -- so if + this endpoint does not check it either, the submit is accepted and the + OPERATOR pays for the caller's extraction. + """ + key = kv_key() + m_keys.get.return_value = key + m_limiter.check_and_acquire.return_value = True + m_save.side_effect = _stamp_created_at + + with _adapters_owned_by(key.organization_id, unusable=[_TABLE_ADAPTERS["llm"]]): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "adapters": _TABLE_ADAPTERS, + "options": {}, + } + ], + ) + ) + + assert resp.status_code == 400 + assert "exhausted" in str(resp.data), resp.data + assert not m_dispatch.called, "an exhausted trial must not reach the executor" + assert not m_stage.called, "nor be charged for staging the upload" + + +@mock.patch.object(ev, "dispatch_job") +@mock.patch.object(AgentKVJob, "save", autospec=True) +@mock.patch.object(ev, "stage_input") +@mock.patch.object(ev, "AgentKVConcurrencyLimiter") +@mock.patch.object(ev, "check_key_rate", return_value=True) +@mock.patch.object(ev, "get_plugin", return_value=_plugin_with_gate()) +@mock.patch.object(AgentKVKey, "objects") +def test_a_deprecated_adapter_is_refused_at_submit( m_keys, m_plugin, m_rate, m_limiter, m_stage, m_save, m_dispatch ): - """"Not yours" and "no such adapter" must read identically. + """`is_available=False` means the SDK registry no longer carries it. - Telling them apart turns the endpoint into an oracle for which adapter - UUIDs are real in other organizations. + Left to the executor this raises `InValidAdapterId` deep in the engine and + reaches the caller as a mid-run extraction failure, on a job that already + took a slot and billed for staging. """ key = kv_key() m_keys.get.return_value = key m_limiter.check_and_acquire.return_value = True m_save.side_effect = _stamp_created_at - bodies = [] - # First: the id exists but belongs to someone else. Second: it exists - # nowhere. The stand-in cannot distinguish them either, which is the point. - for missing in ([_TABLE_ADAPTERS["llm"]], [_TABLE_ADAPTERS["llm"]]): - with _adapters_owned_by(key.organization_id, missing=missing): - resp = ev.SubmitView.as_view()( - _real_multipart_post( - [ - { - "name": "table", - "keys": {"target_table": "Rent rolls"}, - "adapters": _TABLE_ADAPTERS, - "options": {}, - } - ], - ) + with _adapters_owned_by( + key.organization_id, unavailable=[_TABLE_ADAPTERS["x2text"]] + ): + resp = ev.SubmitView.as_view()( + _real_multipart_post( + [ + { + "name": "table", + "keys": {"target_table": "Rent rolls"}, + "adapters": _TABLE_ADAPTERS, + "options": {}, + } + ], ) - bodies.append(str(resp.data)) + ) - assert bodies[0] == bodies[1] + assert resp.status_code == 400 + assert "deprecated" in str(resp.data), resp.data + assert not m_dispatch.called + assert not m_stage.called diff --git a/backend/agent_kv/tests/test_sweeps.py b/backend/agent_kv/tests/test_sweeps.py index cfbee37b7e..94001448ef 100644 --- a/backend/agent_kv/tests/test_sweeps.py +++ b/backend/agent_kv/tests/test_sweeps.py @@ -23,6 +23,7 @@ than on ``internal_views``. """ +import logging import os import uuid from datetime import timedelta @@ -106,7 +107,7 @@ def test_sweep_queries_pending_older_than_grace_and_undispatched( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 0, "timed_out": 0} + assert resp.data == {"swept": 0, "timed_out": 0, "released": 0} filter_kwargs = m_objects.filter.call_args_list[0].kwargs assert filter_kwargs["status"] == JobStatus.PENDING assert filter_kwargs["dispatched_at__isnull"] is True @@ -176,7 +177,7 @@ def test_sweep_count_reflects_guard_outcomes_not_candidate_count( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 1, "timed_out": 0} + assert resp.data == {"swept": 1, "timed_out": 0, "released": 0} m_release.assert_called_once_with("org1", str(won_job.id)) @@ -191,7 +192,7 @@ def test_sweep_with_no_candidates_is_a_pure_noop(m_objects, m_mark_terminal, m_r resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 0, "timed_out": 0} + assert resp.data == {"swept": 0, "timed_out": 0, "released": 0} assert not m_mark_terminal.called assert not m_release.called @@ -217,7 +218,7 @@ def test_stuck_sweep_queries_dispatched_and_running_older_than_stuck_grace( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 0, "timed_out": 0} + assert resp.data == {"swept": 0, "timed_out": 0, "released": 0} filter_kwargs = m_objects.filter.call_args_list[1].kwargs assert set(filter_kwargs["status__in"]) == {JobStatus.DISPATCHED, JobStatus.RUNNING} cutoff = frozen_now - timedelta(seconds=settings.AGENT_KV_STUCK_JOB_GRACE_SECONDS) @@ -259,7 +260,7 @@ def test_stuck_sweep_terminalizes_each_candidate_as_failed_timed_out( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 0, "timed_out": 1} + assert resp.data == {"swept": 0, "timed_out": 1, "released": 0} m_mark_terminal.assert_called_once_with( job.id, "org1", JobStatus.FAILED, error="Job timed out" ) @@ -298,7 +299,7 @@ def test_stuck_sweep_count_reflects_guard_outcomes_not_candidate_count( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 0, "timed_out": 1} + assert resp.data == {"swept": 0, "timed_out": 1, "released": 0} m_release.assert_called_once_with("org1", str(won_job.id)) @@ -320,7 +321,7 @@ def test_sweep_reports_both_phase_counts_independently( resp = iv.SweepView.as_view()(_post("/x")) assert resp.status_code == 200 - assert resp.data == {"swept": 1, "timed_out": 1} + assert resp.data == {"swept": 1, "timed_out": 1, "released": 0} # --------------------------------------------------------------------------- @@ -699,6 +700,98 @@ def test_sweep_releases_slots_held_by_abandoned_cancelled_jobs( assert not m_mark_terminal.called +@mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +def test_phase_three_s_release_count_reaches_the_caller( + m_objects, m_mark_terminal, m_release +): + """`released` must be in the RESPONSE, not only in a log line. + + Phase 3 counted its releases into a local, logged them, and returned only + `{"swept", "timed_out"}`. `SweepView` passes that dict through verbatim and + the scheduler task logs it, so no caller could ever see that a slot had + been recovered -- the same "counted then discarded" shape as the round-1 + finding about this function's return value, reintroduced by the phase added + to fix another part of it. + + Operationally: a cancelled job whose executor never called back holds a + slot against the org's cap, and phase 3 is the only thing that frees it. + Without this key, "the cap is being hit and the sweep is recovering slots" + and "the cap is being hit and nothing is recovering anything" look the same + from outside. + """ + jobs = [] + for _ in range(3): + job = AgentKVJob(status=JobStatus.CANCELLED) + job.organization_id = "org1" + jobs.append(job) + _wire_sweep_phases(m_objects, cancelled=jobs) + + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.status_code == 200 + assert resp.data["released"] == 3, resp.data + assert m_release.call_count == 3 + + +@mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +def test_a_sweep_that_strands_jobs_says_so_at_warning( + m_objects, m_mark_terminal, m_release, caplog +): + """The logging added for round-1 finding 2, pinned. + + Nothing anywhere used `caplog` on this module, so all five log lines could + be deleted and the suite stayed green -- `test_agent_kv_scheduler_tasks.py` + asserts the returned dict, which passed before the logging existed too. + + WARNING specifically, not INFO: a non-zero count means jobs were stranded + and their concurrency slots held, which is the condition an operator needs + surfaced. Before this, a sweep terminalizing a thousand jobs as FAILED + emitted nothing at all, so a backlog of stranded jobs was indistinguishable + from a quiet, healthy system. + """ + job = AgentKVJob(id=uuid.uuid4(), organization_id="org1") + _wire_sweep_phases(m_objects, never_dispatched=[job]) + + with caplog.at_level(logging.INFO, logger="agent_kv.maintenance"): + resp = iv.SweepView.as_view()(_post("/x")) + + assert resp.data["swept"] == 1 + terminalized = [ + r + for r in caplog.records + if r.levelno >= logging.WARNING and "terminalized" in r.getMessage() + ] + assert len(terminalized) == 1, [r.getMessage() for r in caplog.records] + assert "1 never-dispatched" in terminalized[0].getMessage() + + +@mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") +@mock.patch.object(AgentKVJob, "mark_terminal", return_value=True) +@mock.patch.object(AgentKVJob, "objects") +def test_a_quiet_sweep_still_records_that_it_RAN( + m_objects, m_mark_terminal, m_release, caplog +): + """The other half, and the one that matters more. + + "Ran and found nothing" and "never ran at all" produce the same counts, and + distinguishing them is the whole reason this line exists -- the sweep + shipped unscheduled and the only symptom was an absence. So the quiet path + must log too, at INFO. + """ + _wire_sweep_phases(m_objects) + + with caplog.at_level(logging.INFO, logger="agent_kv.maintenance"): + iv.SweepView.as_view()(_post("/x")) + + assert any( + "nothing to terminalize" in r.getMessage() for r in caplog.records + ), [r.getMessage() for r in caplog.records] + + @mock.patch.object(maintenance.AgentKVConcurrencyLimiter, "release") @mock.patch.object(AgentKVJob, "objects") def test_phase_three_is_bounded_on_both_sides(m_objects, m_release): diff --git a/docs/agent-kv-api.md b/docs/agent-kv-api.md index a9592466fa..316cd85a1b 100644 --- a/docs/agent-kv-api.md +++ b/docs/agent-kv-api.md @@ -299,6 +299,15 @@ Not exposed (D6): model choice, challenger model, `parallel_pages`, thinking bud `pdfplumber` and rejected over `AGENT_KV_MAX_PAGES` before any paid work runs. Excel has no pre-OCR page concept — it is capped by file size only at submit time (the engine enforces a post-OCR virtual-page cap). + - **The cap bounds the pages this request will PROCESS, not the document's + length.** It is checked against `pages_selected` — the count after + `page_start` / `page_end` are applied — not against `pages_total`. So pages + 1–50 of a 400-page PDF is accepted under a 100-page cap, while the same + document submitted without a page range is refused. That is the intended + reading: the cap exists to bound paid work per job, and a page range is the + caller declaring how much work they are asking for. `pages_total` is still + recorded on the job and returned in the status document, so the document's + real length is never lost. - Images (`.png .jpg .jpeg .tiff`) are **not** accepted, and no longer count as 1 page. The engine treats only `.pdf/.xlsx/.xls` as a document and `ImageLoader.load_pages` has no call site, so an accepted image returned @@ -423,6 +432,11 @@ list for the extractor that actually ran ] } }, + "adapters": { + "llm": "3f2b1c90-...", + "lite_llm": "a71e4d55-...", + "x2text": "c90aa812-..." + }, "created_at": "2026-08-28T10:15:00.123456+00:00", "started_at": "2026-08-28T10:15:01.500000+00:00", "completed_at": null, @@ -436,9 +450,17 @@ Each stage entry always carries `status` (`running`|`done`) and, if reported, counters, and any counter using the reserved keys `status`/`seconds`, are dropped by the internal stage-report endpoint rather than persisted (`backend/agent_kv/internal_views.py::_sanitize_counters`). +`adapters` echoes the platform adapter ids this job was dispatched with, by role +— the ones the submit named and the gate validated (§3a). It is reported because +the caller chose them and is paying for them: naming the wrong `llm` id yields a +correct-looking result at a different price, and the only other record of what +was spent is in `usage_v2`, which the caller cannot read. **Ids only, never +adapter metadata** — that is where provider credentials live. `{}` for an +env-configured extractor (`kv`) and for any job predating the field. + `status` is lowercased (`pending`, `dispatched`, `running`, `completed`, `failed`, -`cancelled`); `error` is included (only) when `status == "failed"`. `pages_total` and -`completed_at` are always present (as `null` until known) — an addition beyond the +`cancelled`); `error` is included (only) when `status == "failed"`. `adapters`, +`pages_total` and `completed_at` are always present (as `null`/`{}` until known) — an addition beyond the spec's illustrative example, not a drift, since spec §7.2 was explicitly a partial sample ("Verbose, agent-centric, stage-level"). @@ -846,10 +868,28 @@ beyond `docker compose up`: (`backend/agent_kv/management/commands/`) — each printing the JSON counts (`{"swept": N, "timed_out": M}` / `{"cleaned": N}`) and exiting 0. In the cloud deployment, a Kubernetes CronJob runs each command on the cadences above instead - of registering a `PgPeriodicTask` row. The scheduler proxy tasks - (`agent_kv.sweep`/`agent_kv.ttl_cleanup`) and their PG-scheduler registration - remain the mechanism for OSS/self-hosted deployments, which have no CronJob - equivalent to drive from. + of registering a `PgPeriodicTask` row. + + **The CronJob is the only owner.** Migration `0004_pg_periodic_tasks` once also + seeded two `PgPeriodicTask` rows for the same two tasks; it now removes them. + They were seeded `pg_owned: False` so nothing double-fired, but the flag exists + to be flipped, and a flip would have run a `*/10` sweep against the chart's + `*/15` — colliding at `:30` on the same rows — with TTL cadences disagreeing 24x. + Only the CronJob side carries `concurrencyPolicy: Forbid` and a deadline. + + > **Self-hosted OSS: you must schedule these two yourself.** There is no CronJob + > outside the cloud chart and no migration registers them any more. Run + > `manage.py agent_kv_sweep` and `manage.py agent_kv_ttl_cleanup` from cron, a + > systemd timer, or your own `PgPeriodicTask` rows with `pg_owned: True` — the + > scheduler proxy tasks `agent_kv.sweep`/`agent_kv.ttl_cleanup` are still + > registered and still work. Without them a stranded job holds its concurrency + > slot until the 6 h Redis TTL, and `AGENT_KV_RESULT_TTL_DAYS` is advisory: + > staged documents and results stay in the bucket indefinitely, which is a + > retention failure, not just a disk-usage one. + > + > Note this deployment routes `table` only, and that extractor needs the cloud + > `agentic_table` plugin — so a pure-OSS install cannot dispatch an Agent-KV job + > at all, and has no stranded rows to reap until it ships an engine plugin. **Chart keys (cloud repo)**: `backend.agentKvCronJobs` in `charts/unstract-platform/values.yaml` — `enabled: false` there (on-prem never runs @@ -865,13 +905,27 @@ beyond `docker compose up`: 5. **Env vars**: every `AGENT_KV_*` setting plus `AGENT_KV_FILE_STORAGE_CREDENTIALS` — see [§6](#12-environment-reference) and `docker/sample.env`. - > **For the `table` extractor, the executor-side credential group below is NOT - > required.** That path resolves the caller's own platform adapters (see §3a), so - > none of the six `AGENT_KV_LLM_*` / `AGENT_KV_LLMWHISPERER_*` values is read on - > it. The group only has to be configured where the `kv` extractor is deployed, - > which is a separate release track. A table-only deployment can leave + > **For the `table` extractor, the executor-side credential group below must be + > left UNSET — not merely "is not required".** That path resolves the caller's + > own platform adapters (see §3a), so none of the six `AGENT_KV_LLM_*` / + > `AGENT_KV_LLMWHISPERER_*` values is read on it. The group only has to be + > configured where the `kv` extractor is deployed, which is a separate release + > track. A table-only deployment leaves > `global.sharedConfigs.agentKv.enabled: false` and the API works. > + > **Deploy order: cloud worker first, then the backend.** The two repos deploy + > independently and nothing on the wire marks the capability. A new backend + > against an *old* worker sends validated `adapters` in `executor_params` to a + > worker that has no `ParamKeys.ADAPTERS`; it ignores the key and calls + > `ExtractionConfig.from_env()`. If those six values are set it runs on + > **operator** credentials, bills the `agent_kv:{provider}/{model}` sentinel, and + > returns a 202 with a correct-looking result — while the adapters the caller + > named were never used. Nothing in either repo's logs reveals it. With the group + > unset the same old worker fails loudly with "Missing required Agent-KV env + > vars", which is what a half-finished rollout should look like. The reverse + > order is safe by construction: the new worker hard-fails "Missing required + > adapters" rather than falling back to env. + > > Two operator knobs are still read on the table path, independently of that > group: `AGENT_KV_STORAGE_DIR_PREFIX` (which must match the backend's — the > executor derives the job's expected directory from it to enforce the tenant @@ -1067,7 +1121,7 @@ with the default shown: |---|---|---| | `AGENT_KV_PATH_PREFIX` | `agent-kv` | Top-level public URL prefix (whitelisted past tenant middleware). | | `AGENT_KV_MAX_FILE_SIZE_MB` | `50` | Submit-time file size cap. | -| `AGENT_KV_MAX_PAGES` | `100` | Pre-OCR page cap for PDFs (§6.1). Excel has no pre-OCR page concept; images are not accepted. | +| `AGENT_KV_MAX_PAGES` | `100` | Pre-OCR cap on the pages a request will PROCESS for PDFs — checked against `pages_selected` (after `page_start`/`page_end`), not the document's `pages_total` (§6.1). Excel has no pre-OCR page concept, so it takes the engine's post-OCR virtual-page cap instead; images are not accepted. | | `AGENT_KV_MAX_CALCULATIONS_BYTES` | `20000` | Byte cap on the optional `calculations` field. | | `AGENT_KV_MAX_SCHEMA_BYTES` | `262144` | Byte cap on the raw `keys` JSON document (256 KiB). | | `AGENT_KV_RESULT_TTL_DAYS` | `7` | Result retention window (and a cancelled job's input, which rides the same TTL — a completed/failed job's input is deleted immediately at finalize instead), stamped at submit time (see [§6](#9-retention-and-ttl)). | diff --git a/tests/e2e/agent_kv/conftest.py b/tests/e2e/agent_kv/conftest.py index 673787e07f..392ccba914 100644 --- a/tests/e2e/agent_kv/conftest.py +++ b/tests/e2e/agent_kv/conftest.py @@ -227,6 +227,15 @@ def submit_raw( # OCR adapters the extraction runs on. Defaults to whatever the # environment configured; `{}` is sent as-is so a scenario can assert # the 400 for an absent block. + # + # When the env vars are UNSET this omits the block, and the backend + # then refuses the submit at the adapter field validator -- before any + # file or page-cap check. That is correct behaviour but it is a trap + # for tests asserting a DIFFERENT 400: they still see a 400, then fail + # on the blamed attribute (`extractors`, not `file`). Red, not skipped. + # Every scenario that asserts a specific rejection attribute therefore + # takes `require_adapters`, which turns the unconfigured lane into an + # explicit skip naming the missing vars. resolved = e2e_adapters() if adapters is None else adapters if resolved: entry["adapters"] = resolved diff --git a/tests/e2e/agent_kv/test_agent_kv_e2e.py b/tests/e2e/agent_kv/test_agent_kv_e2e.py index c515ca5dd0..8790f99843 100644 --- a/tests/e2e/agent_kv/test_agent_kv_e2e.py +++ b/tests/e2e/agent_kv/test_agent_kv_e2e.py @@ -19,13 +19,23 @@ expect a rejection BEFORE that gate (a bad key, the rate limiter, an absent ``extractors``) do not take it. -A third, narrower gate (``require_llm``) applies only to the scenarios whose -assertions depend on a job actually reaching COMPLETED: either -``AGENT_KV_LLM_API_KEY`` or ``UNSTRACT_LLM_MOCK_RESPONSE`` must be set. Note -that on the table path the LLM credential now lives on the ADAPTER rather -than in the executor's environment, so a real-adapter run satisfies this -whether or not that variable is exported -- it is kept for the mock case and -for the KV path. +There is no third gate for "can a job actually reach COMPLETED". There used to +be (``require_llm``, on ``AGENT_KV_LLM_API_KEY`` or +``UNSTRACT_LLM_MOCK_RESPONSE``), and on the table path it was wrong: the LLM +credential lives on the ADAPTER now, not in the executor's environment, so a +correctly configured lane with adapters and no exported key SKIPPED the only +two scenarios that poll a job through to a genuine COMPLETED result -- the most +valuable assertions in the file. ``require_adapters`` is the whole gate: +configuring adapter ids is what declares this lane can run real extraction, and +(unless the adapters point at a mock) that it will spend real provider money. + +One scenario needs the opposite -- a job that dispatches and then FAILS inside +the engine -- and takes ``bad_llm_adapter``, the id of an adapter deliberately +carrying an invalid credential. That also moved: it was gated on +``AGENT_KV_E2E_BAD_KEY_JOB=1`` plus an invalid ``AGENT_KV_LLM_API_KEY``, a +variable this path does not read, so an operator who set the stack up exactly +as documented got a job that ran on the valid adapter and reached +``completed``. Values extracted under ``UNSTRACT_LLM_MOCK_RESPONSE`` are whatever the mock config returns, not real answers -- assertions here deliberately check @@ -73,6 +83,7 @@ AgentKVAuth, cancel, delete, + e2e_adapters, missing_adapter_env, poll, result, @@ -202,20 +213,29 @@ def require_adapters() -> None: @pytest.fixture -def require_llm() -> None: - """Skip a scenario that needs a job to actually reach COMPLETED. - - Every other scenario in this module resolves before any LLM call - (auth/schema/limiter/page-cap checks); only the two that poll a job all - the way to a genuine COMPLETED result need this. +def bad_llm_adapter() -> str: + """An adapter id whose provider credential is deliberately INVALID. + + Replaces the old `AGENT_KV_E2E_BAD_KEY_JOB=1` + invalid + `AGENT_KV_LLM_API_KEY` arrangement, which stopped working when the table + path moved onto caller-supplied adapters: that env var is not read on this + path at all, so the scenario it gated could no longer be produced. A test + that declared a precondition the stack could not satisfy -- and then ran + against a VALID credential, reaching `completed` and failing its own + `status == "failed"` assertion. + + The credential now lives on an adapter row, so the trigger has to as well. + Create one LLM adapter with a junk API key in the target org and export its + id; nothing else about the lane changes. """ - has_key = os.environ.get("AGENT_KV_LLM_API_KEY") - has_mock = os.environ.get("UNSTRACT_LLM_MOCK_RESPONSE") - if not (has_key or has_mock): + adapter_id = os.environ.get("AGENT_KV_E2E_BAD_LLM_ADAPTER") + if not adapter_id: pytest.skip( - "needs AGENT_KV_LLM_API_KEY or UNSTRACT_LLM_MOCK_RESPONSE so the " - "job can actually reach COMPLETED" + "needs AGENT_KV_E2E_BAD_LLM_ADAPTER: the id of an LLM adapter in " + "this key's organization carrying a deliberately INVALID provider " + "credential, so a job dispatches and then fails inside the engine" ) + return adapter_id # --------------------------------------------------------------------------- @@ -239,7 +259,7 @@ def test_submit_without_key_is_403(agent_kv_key: AgentKVAuth) -> None: def test_table_extractor_happy_path( - require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth ) -> None: """A `table` entry runs the table engine and files its result under `table`. @@ -332,9 +352,21 @@ def test_an_unknown_extractor_is_400(agent_kv_key: AgentKVAuth) -> None: # --------------------------------------------------------------------------- -def test_submit_unreadable_pdf_is_400(agent_kv_key: AgentKVAuth) -> None: +def test_submit_unreadable_pdf_is_400( + require_adapters: None, agent_kv_key: AgentKVAuth +) -> None: """A ``.pdf``-named file that isn't a real PDF is rejected before dispatch. + Takes ``require_adapters`` even though it asserts a REJECTION. The claim + that "scenarios expecting a rejection before the adapter gate keep working + unchanged" holds only for the three raised inside ``validate_extractors``; + this rejection comes from ``SubmitSerializer.validate()``, and DRF raises + field-level errors from ``to_internal_value`` BEFORE ``validate()`` runs -- + with ``_validated_adapter_shape`` called from the field validator. So with + the adapter vars unset (the documented default) the status assert still + passed on a 400 and the attr assert then FAILED, blaming ``extractors`` + instead of ``file``. Red, not skipped. + This is the submit-serializer's own page-count check (``pdfplumber.open()`` raising -> ``{"file": "Unreadable PDF"}``), which is deterministic and needs no LLM/engine work to reach. The executor's @@ -439,7 +471,7 @@ def test_cancelled_job_does_not_leak_its_concurrency_slot( def test_delete_completed_job_then_result_404( - require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth ) -> None: job_id, _ = submit( agent_kv_key, @@ -462,7 +494,25 @@ def test_delete_completed_job_then_result_404( # --------------------------------------------------------------------------- -def test_page_cap_rejects_oversized_document(agent_kv_key: AgentKVAuth) -> None: +def test_page_cap_rejects_oversized_document( + require_adapters: None, agent_kv_key: AgentKVAuth +) -> None: + """The page cap refuses an oversized document at submit. + + Takes ``require_adapters`` even though it asserts a REJECTION. The claim + that "scenarios expecting a rejection before the adapter gate keep working + unchanged" holds only for the three raised inside ``validate_extractors``; + this rejection comes from ``SubmitSerializer.validate()``, and DRF raises + field-level errors from ``to_internal_value`` BEFORE ``validate()`` runs -- + with ``_validated_adapter_shape`` called from the field validator. So with + the adapter vars unset (the documented default) the status assert still + passed on a 400 and the attr assert then FAILED, blaming ``extractors`` + instead of ``file``. Red, not skipped. + + Same defect as ``test_submit_unreadable_pdf_is_400``, but masked: the + ``AGENT_KV_MAX_PAGES`` skip below usually fires first, so it would only + have gone red on a lane that configured the cap. + """ fixture_pages = 2 # fixtures/invoice.pdf cap_raw = os.environ.get("AGENT_KV_MAX_PAGES") if cap_raw is None or int(cap_raw) > fixture_pages: @@ -493,8 +543,8 @@ def _assert_user_safe_error(error: str) -> None: assert leak not in error, f"a filesystem path leaked into: {error!r}" -def test_bad_llm_key_ends_failed( - require_adapters: None, agent_kv_key: AgentKVAuth +def test_a_bad_adapter_credential_ends_failed( + require_adapters: None, agent_kv_key: AgentKVAuth, bad_llm_adapter: str ) -> None: """A job that DISPATCHES and then fails in the executor ends ``failed`` with a user-safe error. @@ -502,9 +552,16 @@ def test_bad_llm_key_ends_failed( Every other failure scenario in this module is a request-time rejection (400/403/429) that never reaches the engine. This one covers the other half of the contract -- the terminal FAILED path -- and it needs the whole - stack to be running with a deliberately invalid ``AGENT_KV_LLM_API_KEY``, - which no test can arrange for itself. Hence the explicit operator gate - (see the module docstring for how to run it). + stack to be running with an adapter whose provider credential is + deliberately invalid, which no test can mint for itself. Hence the explicit + operator gate (see ``bad_llm_adapter``). + + It used to gate on ``AGENT_KV_E2E_BAD_KEY_JOB=1`` plus an invalid + ``AGENT_KV_LLM_API_KEY``. That env var is not read on the table path any + more, so an operator who set the stack up exactly as documented got a job + that ran on the VALID adapter credential, reached ``completed``, and failed + this test's own assertion. The trigger moved to where the credential now + lives. The assertion is about SHAPE, not text: whatever went wrong inside the engine, the customer-visible ``error`` must not carry a filesystem path, a @@ -513,18 +570,16 @@ def test_bad_llm_key_ends_failed( ``cancelled`` / ``timed out``), which is what the executor now builds from the node listener's exception rather than echoing the engine's own text. """ - if os.environ.get("AGENT_KV_E2E_BAD_KEY_JOB") != "1": - pytest.skip( - "needs a platform deliberately configured with an INVALID " - "AGENT_KV_LLM_API_KEY; set AGENT_KV_E2E_BAD_KEY_JOB=1 to declare " - "that this stack is running that way (see the module docstring)" - ) + # The advanced-LLM role carries the junk credential; the other two stay + # valid, so the run gets past OCR and fails where this test says it does. + adapters = {**e2e_adapters(), "llm": bad_llm_adapter} job_id, _ = submit( agent_kv_key, _RENT_ROLL_BYTES, "rent_roll.pdf", table_keys(), + adapters=adapters, ) status_doc = poll(agent_kv_key, job_id, timeout_s=600) @@ -551,7 +606,7 @@ def test_bad_llm_key_ends_failed( def test_sync_wait_submit_returns_result_inline( - require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth ) -> None: """A submit with ``timeout`` seconds sync-waits: if the job reaches a terminal state inside the window, the response is 200 with the result @@ -585,7 +640,7 @@ def test_sync_wait_submit_returns_result_inline( def test_excel_submit_extracts( - require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth ) -> None: """An .xlsx submit runs the whole pipeline. Excel has no pre-OCR page count (``pages_total`` stays None at submit) and takes the post-OCR @@ -609,7 +664,7 @@ def test_excel_submit_extracts( def test_webhook_delivered_on_completion( - require_adapters: None, agent_kv_key: AgentKVAuth, require_llm: None + require_adapters: None, agent_kv_key: AgentKVAuth ) -> None: """Completion-webhook delivery, end to end: submit with a ``webhook_url`` pointing at a receiver on the compose host and wait for the POST. diff --git a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py index a7ecc37611..b3aa72e075 100644 --- a/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py +++ b/unstract/agent-kv-schema/src/unstract/agent_kv_schema/compile.py @@ -220,13 +220,20 @@ def _reject_unsafe_regex(path: str, pattern: str) -> None: def _reject_unusable_formats(key_specs, array_specs) -> None: """Refuse declared formats that can never validate anything, or always fail. - Three shapes, all accepted before review and all verified by execution: + Four shapes, all accepted before review and all verified by execution: * ``format: "Number"`` -- a case variant of a known format. Silently degrades to a free-text hint, disabling validation for that key. Rejected with the intended spelling, rather than guessed at: a schema author who meant the hint can lower-case it or reword it, and one who meant the format gets told. + * ``format: "Enum:paid,unpaid"`` / ``"REGEX:^[0-9]+$"`` -- the same defect + class for the two formats that take an ARGUMENT, and the one the first + round's fix missed. ``_parse_format`` matches the ``enum:`` / ``regex:`` + prefix case-sensitively, so these never reach kind ``"enum"``/``"regex"``; + they arrive as the whole raw string, which no bare-name comparison + matches. Validation was silently off for the key -- the precise outcome + the first bullet exists to prevent, reached by a route it did not cover. * ``format: "enum:"`` -- no values, so ``_check_one`` tests membership of the empty set and EVERY non-empty value fails QA for the life of the job. * ``format: "regex:"`` -- empty pattern, so ``re.fullmatch("", v)`` matches @@ -240,7 +247,28 @@ def _reject_unusable_formats(key_specs, array_specs) -> None: for kspec in _every_leaf(key_specs, array_specs): fmt = kspec.format lowered = fmt.casefold() - if fmt not in _KNOWN_FORMATS and lowered in _KNOWN_FORMATS: + # `lowered` catches a bare case variant (`"Number"`). `kind_only` + # catches the two formats that take an ARGUMENT, which the bare check + # misses entirely: `_parse_format` matches the `enum:` / `regex:` + # prefix case-SENSITIVELY, so `"Enum:paid,unpaid"` never becomes kind + # `"enum"` -- it arrives here as the whole raw string, and + # `"enum:paid,unpaid".casefold()` is not a member of `_KNOWN_FORMATS` + # (which holds the bare `"enum"`). So the mis-cased form slipped this + # guard, parsed as a free-text kind, and validation was silently off + # for that key -- exactly the failure this function closes for bare + # formats. + # + # Splitting on the colon is safe for genuine free text: a hint like + # `"customer:id"` yields `"customer"`, which is not a known format. + # + # Rejected rather than auto-corrected, to match the bare case above: a + # schema author who meant the format gets told the spelling, and one + # who meant a free-text hint can reword it. Silently honouring `Enum:` + # would make case significant in one direction only. + kind_only = lowered.split(":", 1)[0] + if fmt not in _KNOWN_FORMATS and ( + lowered in _KNOWN_FORMATS or kind_only in _KNOWN_FORMATS + ): raise SchemaError( f"'{kspec.path}' declares format {fmt!r}, which is not a known " f"format and is therefore treated as free text -- no validation " diff --git a/unstract/agent-kv-schema/tests/test_compile.py b/unstract/agent-kv-schema/tests/test_compile.py index c21e61fc71..471df824c7 100644 --- a/unstract/agent-kv-schema/tests/test_compile.py +++ b/unstract/agent-kv-schema/tests/test_compile.py @@ -289,6 +289,72 @@ def test_a_case_variant_of_a_known_format_is_refused(fmt, intended): compile_schema(spec) +@pytest.mark.parametrize( + ("fmt", "intended"), + [ + ("Enum:paid,unpaid", "enum:paid,unpaid"), + ("ENUM:paid,unpaid", "enum:paid,unpaid"), + ("Regex:^[0-9]+$", "regex:^[0-9]+$"), + ("REGEX:^[0-9]+$", "regex:^[0-9]+$"), + ], +) +def test_a_case_variant_of_an_ARGUMENT_taking_format_is_refused(fmt, intended): + """The residual the first round's fix left behind. + + `test_a_case_variant_of_a_known_format_is_refused` above covers the BARE + names, including bare `"Enum"` and `"Regex"` -- but not the forms that + actually carry an argument, which is how anyone real writes them. + `_parse_format` matches the `enum:` / `regex:` prefix case-sensitively, so + `"Enum:paid,unpaid"` never becomes kind `"enum"`; it reached the typo guard + as the whole raw string, and `"enum:paid,unpaid"` is not a member of + `_KNOWN_FORMATS` (which holds the bare `"enum"`). + + Net effect before this: the key compiled as a free-text hint, `/validate` + reported `{"valid": true}`, and the enum was never enforced -- identical to + the `"Number"` failure, through the one gap the fix for it did not close. + """ + with pytest.raises(SchemaError, match="Did you mean"): + compile_schema(_one_key(fmt)) + + # And the suggestion has to be the usable spelling, argument included -- + # `"enum"` alone would be rejected by the no-values check one line later, + # sending the author in a circle. + with pytest.raises(SchemaError) as caught: + compile_schema(_one_key(fmt)) + assert intended in str(caught.value) + + +@pytest.mark.parametrize( + "fmt", + ["enum:paid,unpaid", "regex:^[0-9]+$", "enum:a,b,c"], +) +def test_correctly_cased_argument_formats_still_compile(fmt): + """The other half: the fix must not reject the valid spellings. + + These reach the guard with kind already resolved to `"enum"`/`"regex"` by + `_parse_format`, so the colon-splitting branch must not fire on them. + """ + compile_schema(_one_key(fmt)) + + +@pytest.mark.parametrize( + "fmt", + [ + "customer:id", # a colon in genuine free text + "total:in USD", + "ref:12345", + ], +) +def test_free_text_containing_a_colon_is_not_mistaken_for_a_format(fmt): + """Splitting on `:` must not capture unrelated hints. + + The prefix before the colon is not a known format in any of these, so they + stay free text -- which is what a schema author writing a descriptive hint + expects. + """ + compile_schema(_one_key(fmt)) + + @pytest.mark.parametrize("fmt", ["number", "date", "currency", "string"]) def test_the_known_formats_still_compile(fmt): compile_schema(_one_key(fmt)) diff --git a/unstract/sdk1/tests/test_page_usage_metering.py b/unstract/sdk1/tests/test_page_usage_metering.py new file mode 100644 index 0000000000..6d242e4764 --- /dev/null +++ b/unstract/sdk1/tests/test_page_usage_metering.py @@ -0,0 +1,172 @@ +"""`push_usage_details` is the single point where a page-usage row is billed. + +Pinned because two independent paths depend on the counts it produces and one +of them disagrees with it today (UN-4232): + +* PDFs are counted by `pdfplumber`, one row per real page. +* **Everything else is counted as ONE page**, with a standing + `TODO: Calculate page usage for other file types` at the call site. + +The second is a real under-count for spreadsheets. The agentic table engine +splits an Excel workbook into *virtual* pages and extracts each one, so a +12-sheet workbook does twelve pages of work, reports twelve in the job result, +and bills one. That affects both the IDE table path +(`agentic_table/src/executor.py`) and the Agent-KV blind API +(`agentic_table/src/api_binding.py`) identically -- they share this function -- +so it is a product-wide billing question, not a per-path bug, and correcting it +here moves both at once. + +These tests do not assert that the current numbers are RIGHT. They assert what +they currently are, so that: + +1. A change to the non-PDF count surfaces as a failing test with this + explanation attached, instead of silently re-pricing every spreadsheet + extraction in the product. +2. `file_name` and `run_id` keep flowing from `usage_kwargs` onto the row -- + the two fields a billing dispute is reconstructed from. The blind API + shipped without `file_name` once already. + +Excel page metering has moved twice on this branch (an over-count from blank +chunks, then this under-count), which is why it is pinned rather than +described. +""" + +from collections.abc import Callable +from pathlib import Path +from typing import Any +from unittest import mock + +import pytest +from unstract.sdk1.constants import MimeType +from unstract.sdk1.x2txt import X2Text + + +def _x2text(usage_kwargs: dict[str, str]) -> X2Text: + """An `X2Text` with its `__init__` bypassed. + + Constructing one for real needs a platform round-trip to resolve an adapter; + `push_usage_details` touches only `self._tool` and `self._usage_kwargs`. + """ + inst = object.__new__(X2Text) + inst._usage_kwargs = usage_kwargs + inst._tool = mock.Mock(get_env_or_die=mock.Mock(return_value="pk")) + return inst + + +def _minimal_pdf(pages: int) -> bytes: + """A structurally valid PDF with `pages` empty pages. + + Built by hand rather than with a fixture file so the page count is visible + in the test that asserts on it. The PDF branch really does open the file + with `pdfplumber`, so a placeholder byte string is not enough. + """ + objs = [ + "<< /Type /Catalog /Pages 2 0 R >>", + "<< /Type /Pages /Kids [{}] /Count {} >>".format( + " ".join(f"{3 + i} 0 R" for i in range(pages)), pages + ), + *[ + "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>" + for _ in range(pages) + ], + ] + out = bytearray(b"%PDF-1.4\n") + offsets = [] + for n, body in enumerate(objs, start=1): + offsets.append(len(out)) + out += f"{n} 0 obj\n{body}\nendobj\n".encode() + xref_at = len(out) + out += f"xref\n0 {len(objs) + 1}\n0000000000 65535 f \n".encode() + for off in offsets: + out += f"{off:010d} 00000 n \n".encode() + out += ( + f"trailer\n<< /Size {len(objs) + 1} /Root 1 0 R >>\n" + f"startxref\n{xref_at}\n%%EOF\n" + ).encode() + return bytes(out) + + +XLSX_MIME = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + + +@pytest.fixture +def pushed(tmp_path: Path) -> Callable[..., dict[str, Any]]: + """Capture the single `push_page_usage_data` call as a kwargs dict.""" + + def _run( + mime_type: str, + usage_kwargs: dict[str, str], + *, + pdf_pages: int = 1, + name: str = "book.xlsx", + ) -> dict[str, Any]: + f = tmp_path / name + if mime_type == MimeType.PDF: + f.write_bytes(_minimal_pdf(pdf_pages)) + else: + f.write_bytes(b"a real file with a real size, but not a workbook") + with mock.patch("unstract.sdk1.x2txt.Audit") as m_audit: + _x2text(usage_kwargs).push_usage_details(str(f), mime_type) + assert m_audit.return_value.push_page_usage_data.call_count == 1 + return m_audit.return_value.push_page_usage_data.call_args.kwargs + + return _run + + +def test_a_pdf_is_billed_per_real_page(pushed: Callable[..., dict[str, Any]]) -> None: + """The contrast that makes the spreadsheet case a gap rather than a policy. + + PDFs are counted by `pdfplumber`, so pages of work and pages billed agree. + """ + call = pushed(MimeType.PDF, {"run_id": "job-1"}, pdf_pages=3, name="rent-roll.pdf") + + assert call["page_count"] == 3 + + +def test_a_spreadsheet_is_billed_as_a_single_page( + pushed: Callable[..., dict[str, Any]], +) -> None: + """The under-count, pinned with its consequence spelled out. + + If this starts failing because the count became sheet- or virtual-page + aware, that is an INTENTIONAL billing change: update this test, and check + that `agentic_table`'s reported `pages` now agrees with it (the job result + and the usage row currently disagree for Excel). + """ + call = pushed(XLSX_MIME, {"run_id": "job-1", "file_name": "book.xlsx"}) + + assert call["page_count"] == 1, ( + "non-PDF inputs are metered as one page regardless of how many pages " + "of work they cause; the table engine splits a workbook into virtual " + "pages and bills this single row for all of them" + ) + + +def test_the_billing_identifiers_reach_the_row( + pushed: Callable[..., dict[str, Any]], +) -> None: + """`run_id` and `file_name` are carried only by `usage_kwargs`. + + `Audit.push_page_usage_data` reads both off that dict and defaults each to + `""`, so a caller that omits one gets a billed row with no way back to the + job or the input. The Agent-KV blind API omitted `file_name` on its first + cut (UN-4232); nothing failed, which is why this exists. + """ + call = pushed(XLSX_MIME, {"run_id": "job-7", "file_name": "rent-roll.xlsx"}) + + assert call["kwargs"]["run_id"] == "job-7" + assert call["kwargs"]["file_name"] == "rent-roll.xlsx" + + +def test_a_missing_file_name_is_not_rejected_here( + pushed: Callable[..., dict[str, Any]], +) -> None: + """Documents WHY the omission was silent: this layer tolerates it. + + Not an endorsement -- the point is that no exception, no warning and no log + marks the gap, so the only defence is the caller passing the field. Both + table paths now do. + """ + call = pushed(XLSX_MIME, {"run_id": "job-7"}) + + assert call["kwargs"].get("file_name") is None diff --git a/workers/ide_callback/agent_kv_tasks.py b/workers/ide_callback/agent_kv_tasks.py index 5b56d3d393..65255ce61c 100644 --- a/workers/ide_callback/agent_kv_tasks.py +++ b/workers/ide_callback/agent_kv_tasks.py @@ -166,12 +166,29 @@ def agent_kv_error( terminalized it with "Job timed out", overwriting the real executor error, which existed only in the log line above. - Now retries with backoff and RAISES on exhaustion, matching its sibling + Now retries and RAISES on exhaustion, matching its sibling ``agent_kv_complete`` and the ``process_batch_callback_api`` precedent in ``workers/callback/tasks.py``. The PG consumer relies on task-level autoretry for exactly this (see its own comment in ``queue_backend/pg_queue/consumer.py``). + **What ``retry_backoff`` actually does here: nothing.** The PG consumer runs + tasks via ``task.apply(..., throw=True)`` + (``queue_backend/pg_queue/consumer.py``) -- i.e. EAGER. Celery's eager + retry recurses synchronously and ignores ``countdown``, so + ``retry_backoff=True`` / ``retry_backoff_max=300`` / ``retry_jitter=True`` + produce three back-to-back attempts in microseconds, which rides out no + "momentary blip" at all. They are kept because they are correct for the + Celery transport and cost nothing under the PG one -- not because they + space the attempts out here. + + The delay that genuinely helps comes AFTER exhaustion, from vt-redelivery + once this raises and the message's visibility timeout lapses. So the value + of the raise is not just the failed-task record: it is the only thing that + buys any real elapsed time before the next attempt. An earlier version of + this docstring credited the decorator with that, which would mislead anyone + tuning the backoff to fix a slow finalize. + ``autoretry_for=(Exception,)`` rather than a narrow transport type: the whole point is that this task must not quietly give up, and a programming error that burns three retries and then surfaces loudly is a far better diff --git a/workers/ide_callback/tasks.py b/workers/ide_callback/tasks.py index 968879bac6..7f2ed70e7c 100644 --- a/workers/ide_callback/tasks.py +++ b/workers/ide_callback/tasks.py @@ -115,7 +115,24 @@ def _get_task_error( if res.result: return str(res.result) except Exception: - pass + # Was a bare `pass`, the same defect fixed in the Agent-KV twin + # (`agent_kv_tasks.py::_error_link`) -- carried over here because the + # cause and the cost are identical, not because this PR owns the IDE + # path. + # + # The common case is a kombu deserialization failure: the executor + # raised an exception class that is not importable in this image, so + # the result backend HAS the error and this process cannot read it. The + # execution is then finalized with the generic `default` -- the exact + # useless message this lookup exists to avoid -- and silently, leaving + # the real cause unrecoverable even from logs. + logger.warning( + "Could not read the executor error from the result backend for " + "task %s; finalizing with the generic message %r", + failed_task_id, + default, + exc_info=True, + ) return default diff --git a/workers/ide_callback/tests/test_agent_kv_callbacks.py b/workers/ide_callback/tests/test_agent_kv_callbacks.py index 6278b48d82..d78f7c670e 100644 --- a/workers/ide_callback/tests/test_agent_kv_callbacks.py +++ b/workers/ide_callback/tests/test_agent_kv_callbacks.py @@ -6,6 +6,7 @@ mocked at the ``ide_callback.agent_kv_tasks`` import site. """ +import logging from unittest.mock import MagicMock, patch import pytest @@ -227,6 +228,53 @@ def test_error_link_falls_back_when_lookup_raises( "job-1", "org-1", success=False, error=_UNKNOWN ) + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_a_swallowed_lookup_failure_is_logged_with_its_traceback( + self, mock_get_client, mock_send_webhook, mock_api, cb_kwargs, caplog + ): + """The handler was a bare `except Exception: pass`. + + `test_error_link_falls_back_when_lookup_raises` above asserts only the + `_UNKNOWN` fallback -- which the bare `pass` also produced, so that test + is byte-identical before and after the fix and cannot detect it. + + What actually changed is that the cause is now recoverable. The common + case is a kombu deserialization failure: the executor raised a + cloud-plugin exception class that this OSS image cannot import, so the + result backend HAS the error and this process cannot read it. The job + is finalized with "Executor failed without an error message" -- and + before, with nothing anywhere saying why. + + `exc_info` is asserted, not just the message: without the traceback the + log says a lookup failed but not what failed, which for an unimportable + exception class is the entire diagnosis. + """ + with caplog.at_level(logging.WARNING): + with patch(_PATCH_ASYNC_RESULT, side_effect=RuntimeError("backend down")): + self._call("failed-task-3b", cb_kwargs) + + # Matched by CONTENT, not by position. The callback also logs its own + # terminal ERROR ("agent_kv executor task failed: ..."), so taking + # `records[0]` would let this pass against a bare `pass` by picking up + # an unrelated record -- which it did on the first cut of this test. + matching = [ + r + for r in caplog.records + if r.levelno >= logging.WARNING and "result backend" in r.getMessage() + ] + assert len(matching) == 1, ( + "expected exactly one warning about the unreadable result backend; " + f"got {[r.getMessage() for r in caplog.records]}. Restoring the " + "bare `pass` makes this the failing assertion." + ) + record = matching[0] + assert "failed-task-3b" in record.getMessage(), record.getMessage() + assert record.exc_info is not None, ( + "logged without exc_info -- the traceback is the diagnosis when the " + "cause is an exception class this image cannot import" + ) + @patch(_PATCH_SEND_WEBHOOK) @patch(_PATCH_GET_CLIENT) def test_none_callback_kwargs_uses_empty_defaults( @@ -331,15 +379,46 @@ def test_agent_kv_error_is_configured_to_retry_before_giving_up(self): assert agent_kv_error.max_retries == 3 assert Exception in agent_kv_error.autoretry_for - def test_agent_kv_error_matches_its_siblings_failure_posture(self): - """`agent_kv_complete` re-raises; these two are the success and failure - halves of one contract and must not disagree about what a failed - finalize means. + @patch(_PATCH_SEND_WEBHOOK) + @patch(_PATCH_GET_CLIENT) + def test_agent_kv_error_matches_its_siblings_failure_posture( + self, mock_get_client, mock_send_webhook, cb_kwargs + ): + """Both callbacks must RAISE when finalize fails -- neither may swallow. + + These two are the success and failure halves of one contract: whichever + one runs is the SOLE terminalizer of the job, so a finalize that fails + silently leaves the row in RUNNING until the sweep overwrites the real + error with "Job timed out". `agent_kv_error` used to catch, log and + `return None`, which the consumer records as SUCCESS -- no retry, no + dead letter, no failed-task record. + + The previous version of this test asserted + `agent_kv_complete is not None` and `agent_kv_error.max_retries is not + None`, which is true of essentially any task object and could not fail + for the reason the test names. It is the behaviour that has to be + pinned, so both callbacks are actually driven here with a finalize that + blows up. """ from ide_callback.agent_kv_tasks import agent_kv_complete, agent_kv_error - assert agent_kv_complete is not None - assert agent_kv_error.max_retries is not None + api = MagicMock() + api.agent_kv_finalize.side_effect = RuntimeError("backend unreachable") + mock_get_client.return_value = api + + with pytest.raises(RuntimeError): + agent_kv_complete( + {"success": True, "data": {"output": {}}, "error": None}, cb_kwargs + ) + + with pytest.raises(RuntimeError): + with patch(_PATCH_ASYNC_RESULT, return_value=MagicMock(result="boom")): + agent_kv_error.run("failed-task-posture", cb_kwargs) + + # And neither may fire a webhook off a finalize that did not happen -- + # telling the caller a job is terminal when the row never moved is + # worse than telling them nothing. + assert not mock_send_webhook.called # --------------------------------------------------------------------------- diff --git a/workers/scheduler/agent_kv_tasks.py b/workers/scheduler/agent_kv_tasks.py index 6806299c14..117723f3cc 100644 --- a/workers/scheduler/agent_kv_tasks.py +++ b/workers/scheduler/agent_kv_tasks.py @@ -66,7 +66,15 @@ def agent_kv_sweep() -> dict[str, Any]: # Logged here as well as backend-side: this is the only place that records # the periodic actually FIRED. The counts alone cannot distinguish "ran and # found nothing" from "never ran", which is precisely the failure this - # task pair shipped with (see the backend migration that schedules them). + # task pair shipped with. + # + # Nothing in this repo schedules these. The cloud chart's + # `backend.agentKvCronJobs` runs the equivalent management commands and is + # the single owner (migration `0004_pg_periodic_tasks` used to seed + # PgPeriodicTask rows as a second owner and now removes them). A + # self-hosted OSS operator who wants these tasks driven through the PG + # scheduler registers the rows themselves with `pg_owned: True` -- see + # `docs/agent-kv-api.md` §12. logger.info("agent_kv.sweep completed: %s", result) return result diff --git a/workers/tests/test_llm_bearing_ops.py b/workers/tests/test_llm_bearing_ops.py index 77a602e5cd..7b7e4c8a33 100644 --- a/workers/tests/test_llm_bearing_ops.py +++ b/workers/tests/test_llm_bearing_ops.py @@ -17,8 +17,22 @@ from executor.tasks import _LLM_BEARING_OPS #: Operations known to drive at least one LLM call on every successful run. -#: Add to this list and the set together; a new paid op that lands in neither is -#: exactly the failure this file exists to catch. +#: Add to this list and the set together. +#: +#: **Known limitation of this file.** These are two literals, so the +#: assertions below can only fail when the copies DISAGREE -- never when both +#: are wrong together. A new paid op declared in neither still ships with no +#: billing alarm. The derived version, which asserts every operation in the +#: backend's `EXTRACTOR_ROUTES` is in `_LLM_BEARING_OPS` and so cannot be +#: satisfied by forgetting both, is +#: `backend/agent_kv/tests/test_queue_wiring_is_derived.py` +#: (`test_every_routed_operation_has_a_billing_backstop`). It lives there +#: because `EXTRACTOR_ROUTES` is not importable from this test venv, and +#: `executor.tasks` is not importable from the backend's -- neither side can +#: import both, which is why this pair exists at all. +#: +#: Kept rather than replaced: it covers the four non-Agent-KV paid ops, which +#: `EXTRACTOR_ROUTES` says nothing about. PAID_OPERATIONS = { "answer_prompt", "single_pass_extraction", diff --git a/workers/tests/test_queue_consumer_wiring.py b/workers/tests/test_queue_consumer_wiring.py index 6f98c2675f..14fc562cc7 100644 --- a/workers/tests/test_queue_consumer_wiring.py +++ b/workers/tests/test_queue_consumer_wiring.py @@ -14,6 +14,20 @@ no ``agentic_kv`` plugin, so a fleet listing that queue would accept work nothing can drain -- the same silent failure, arrived at from the other side. +**These names are literals, and that is a known limitation.** Re-enabling the +``kv`` extractor is one ``EXTRACTOR_ROUTES`` entry, after which +``celery_executor_agentic_kv`` must be SERVED -- and the inverse assertion +above would have to be deleted to go green, which is the one moment a guard +should not be getting deleted. The derived version of both directions lives in +``backend/agent_kv/tests/test_queue_wiring_is_derived.py``, where +``EXTRACTOR_ROUTES`` is actually importable (the backend package is not +importable from this test venv -- that asymmetry is why the duplication +exists). That suite also covers ``run-worker-docker.sh``, which this one does +not reach. + +Keeping both is deliberate: two independent statements of the same fact, one of +which does not depend on the routing table being correct. + An earlier version of this docstring described only the first direction and narrated how ``celery_executor_agentic_kv`` ought to be wired in. That is the one thing a guard with an inverted assertion must not say: a reader checking @@ -118,6 +132,18 @@ def test_test_compose_does_not_wire_the_queue_onto_the_dead_celery_var(): That drained nothing on the PG transport. If someone re-adds it, the queue looks configured while the lane still hangs -- so fail loudly. + + Scoped to TEST_COMPOSE deliberately, and NOT parametrized over both files + -- review asked for the parametrization and this is why it is wrong here. + `docker/docker-compose.yaml` legitimately sets `CELERY_QUEUES_EXECUTOR` on + `worker-pg-executor` (it is consumed by `run-worker-docker.sh:281` and by + `workers/executor/worker.py` for anyone still launching the Celery + executor), so parametrizing would turn this into an immediate red on a + config that is vestigial rather than wrong. + + What generalizes is not "the variable must be absent" but "the variable + must never advertise a queue the PG consumer does not serve" -- which is + the next test, and which DOES run against both files. """ env = _service_env(TEST_COMPOSE, "worker-pg-executor") assert DEAD_CELERY_VAR not in env, ( @@ -126,6 +152,40 @@ def test_test_compose_does_not_wire_the_queue_onto_the_dead_celery_var(): ) +@pytest.mark.parametrize( + ("compose_path", "label"), + [(DEV_COMPOSE, "dev compose"), (TEST_COMPOSE, "e2e test compose")], +) +def test_the_dead_celery_var_never_advertises_an_unserved_queue(compose_path, label): + """The generalized form of the test above, over both compose files. + + Wherever `CELERY_QUEUES_EXECUTOR` survives, it must stay a SUBSET of what + the PG consumer actually drains. The hazard is not the variable existing -- + it is the variable drifting into a superset, because then a reader + comparing the two lists concludes a queue is served when nothing drains it, + and the Celery path that would have drained it is disabled. + + Concretely this fails if someone adds `celery_executor_agentic_kv` to the + Celery list while the PG list correctly omits it. + """ + env = _service_env(compose_path, "worker-pg-executor") + dead = env.get(DEAD_CELERY_VAR) + if dead is None: + pytest.skip(f"{label} does not set {DEAD_CELERY_VAR}") + + pg_raw = env.get(PG_QUEUE_VAR) + assert pg_raw is not None, f"{label}: no {PG_QUEUE_VAR} to compare against" + + extra = _queues(dead) - _queues(pg_raw) + assert not extra, ( + f"{label}: {DEAD_CELERY_VAR} advertises {sorted(extra)}, which " + f"{PG_QUEUE_VAR} does not serve. {DEAD_CELERY_VAR} is read only by the " + f"disabled Celery executor, so nothing drains those -- work routed " + f"there is accepted and never runs. Add them to {PG_QUEUE_VAR} or drop " + f"them here." + ) + + def test_run_worker_pg_executor_role_omits_the_unserved_kv_queue(): """The host-run fleet's hardcoded role default must match the compose stacks.