diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f70a33..c5b4fd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v3 @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.10", "3.11", "3.12", "3.13" ] + python-version: [ "3.11", "3.12", "3.13" ] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a6ad8e..f8fb085 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - published env: - PYTHON_VERSION: "3.10" + PYTHON_VERSION: "3.11" jobs: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index a36cf50..5ad54f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +## 0.1.0 + +* **This package now owns the `/invoke` transport for the reserved fields.** + `unstructured_platform_plugins.invocation_settings` holds the `/invoke` binding dependency and + body cap, the `/metadata` capability route, the request-scoped accessors, and `http_status_for` + — the HTTP spelling of the + library's normative `blame` → status rule. It sits on `utic-invocation-settings >=0.5.0`, which + owns the *settings contract* — including the v2 document as the only accepted sealed + `/invoke` shape, independent sealed-field resolution, and what an absent field is allowed to + mean. That split is deliberate: the + absence rule is a security decision and belongs next to the crypto it governs, while request + handling and route registration belong here, where a web framework is already a dependency. + Nothing about the sealed-settings wire format is decided in this repository. +* **The wrapper consumes the ratified `invocation_context` contract locally.** + The field model, reserved key, supported versions, and dimension allow-list are generated from + `https://schemas.u10d.dev/invocation-context/v1.json`. The handwritten + `unstructured_platform_plugins.invocation_context` adapter retains transport error mapping and + the equal-length batch invariant that JSON Schema cannot express. The ratified + `https://schemas.u10d.dev/errors/audience/v1.json` vocabulary also replaces the redundant + top-level `blame` response field: a legacy `UserError` now carries a complete `plugin_error` + metadata object with `audience=user`. +* **Every wrapped app installs it at construction.** The reserved `invocation_settings` / + `invocation_context` fields are handled outside the generated handler schema, the opaque + settings payload is delegated to `utic-invocation-settings`, and only the final resolved mapping + is exposed through `current_invocation_settings()` / `current_invocation_context()`. + An absent field preserves the existing fallback behaviour; under + `FF_INVOCATION_SETTINGS` missing or plaintext settings fail closed. + Repeated installation is safe: the dependency installs once and the last `/metadata` + registration wins. +* **Extraction is a route dependency.** `bind_invocation_envelope` reads the body the framework + buffered and parsed (`request.json()` is Starlette-cached), so the `/invoke` body is held and + decoded exactly once per request. `install_invocation_envelope` contributes that path-aware + dependency through the router's public dependency list before `/invoke` is registered; no + private FastAPI dependency graph is mutated. It also registers the failure response shape and + can optionally install `InvokeBodyLimitMiddleware`, a streaming byte counter that answers 413 + over a host-selected cap without buffering. The cap is disabled by default so a wrapper upgrade + cannot impose an unvalidated fleet-wide request limit. Async-generator plugins explicitly + re-enter the captured request binding inside response iteration, so streaming stays correct + independently of FastAPI's yield-dependency cleanup timing. +* **Sealed settings consumption remains opt-in.** Pass + `invoke_with_sealed_dag_node_settings_v2=True` to `wrap_in_fastapi` / `generate_fast_api` (or + `--sealed-dag-node-settings-v2` on the CLI) only for a plugin that consumes per-invoke settings; + it advertises that the application accepts and acts on the versioned v2 document. + Transport support alone continues to advertise only `invocation_settings` and + `invocation_context`. A + plugin that serves a custom `/metadata` payload must register it via `add_metadata_route` (which + replaces the wrapper's route) — a plain `@app.get("/metadata")` added after construction is + shadowed by the wrapper's earlier registration. +* **Resolution runs off the event loop.** Resolution may perform blocking cryptography for + independently sealed fields and this dependency fronts every invoke on the pod, so it is + dispatched with `asyncio.to_thread` rather than blocking the loop. +* **Failures map through the library's blame taxonomy**, not a flat 500: only a caller-fixable + fault answers 422. Sealing drift, an envelope addressed to another recipient and a broken local + mount are all 5xx, which keeps the controller's blame classification off the customer. Responses + carry the error's class name and never its message, which can embed request-controlled values. +* **Python floor is now 3.11** (required by `utic-invocation-settings`). + ## 0.0.46 * **Carry preflight failure categories through standard `/precheck` responses.** diff --git a/pyproject.toml b/pyproject.toml index 4bb84aa..9da9f37 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "unstructured_platform_plugins" description = "Wrapper to convert arbitrary code into a uvicorn/fastapi implementation for Unstructured Platform" -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", @@ -10,7 +10,6 @@ classifiers = [ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -24,6 +23,7 @@ dependencies = [ "fastapi", "click", "unstructured-ingest", + "utic-invocation-settings>=0.5.0,<1.0.0", "opentelemetry-instrumentation-fastapi", "opentelemetry-exporter-otlp-proto-grpc", "dataclasses-json" diff --git a/scripts/generate_invocation_contracts.py b/scripts/generate_invocation_contracts.py new file mode 100644 index 0000000..f502a6f --- /dev/null +++ b/scripts/generate_invocation_contracts.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Generate local Python bindings from the ratified invocation schema IDs.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import sys +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT_DIR = ROOT / "unstructured_platform_plugins" / "generated" +API_ROOT = "repos/Unstructured-IO/schemas-experimental/contents" + +CONTRACTS = { + "invocation_context_v1.py": { + "schema": f"{API_ROOT}/schemas/ratified-types/invocation-context/v1.json?ref=main", + "typeviz": f"{API_ROOT}/web/public/typeviz/invocation-context-v1.json?ref=main", + "schema_id": "https://schemas.u10d.dev/invocation-context/v1.json", + "root_type": "InvocationContext", + }, + "error_audience_v1.py": { + "schema": f"{API_ROOT}/schemas/ratified-types/errors/audience/v1.json?ref=main", + "typeviz": f"{API_ROOT}/web/public/typeviz/errors-audience-v1.json?ref=main", + "schema_id": "https://schemas.u10d.dev/errors/audience/v1.json", + "root_type": "ErrorAudience", + }, +} + + +def _load_json(api_path: str) -> dict[str, Any]: + completed = subprocess.run( + ["gh", "api", "-H", "Accept: application/vnd.github.raw+json", api_path], + check=True, + capture_output=True, + text=True, + ) + return json.loads(completed.stdout) + + +def _python_binding(sidecar: dict[str, Any]) -> str: + for binding in sidecar["bindings"]: + if binding["key"] == "python": + return binding["code"].rstrip() + "\n" + raise ValueError("typeviz sidecar has no Python binding") + + +def _render(spec: dict[str, str]) -> str: + schema = _load_json(spec["schema"]) + sidecar = _load_json(spec["typeviz"]) + if schema["$id"] != spec["schema_id"]: + raise ValueError(f"unexpected schema id: {schema['$id']}") + if sidecar["root_type_name"] != spec["root_type"]: + raise ValueError(f"unexpected root type: {sidecar['root_type_name']}") + + canonical = json.dumps(schema, sort_keys=True, separators=(",", ":")).encode() + schema_hash = hashlib.sha256(canonical).hexdigest() + generated = _python_binding(sidecar) + if spec["root_type"] == "InvocationContext": + generated = generated.replace( + "from typing import Any, Literal", "from typing import Literal" + ) + else: + generated = generated.replace("\nfrom pydantic import BaseModel, Field\n", "") + provenance = ( + "\n# Generation provenance used by drift tests and reviewers.\n" + f"SCHEMA_ID = {schema['$id']!r}\n" + f"SCHEMA_SHA256 = {schema_hash!r}\n" + ) + if spec["root_type"] == "InvocationContext": + policy = schema["x-unstructured-version-policy"] + provenance += ( + f"RESERVED_CONTEXT_KEY = {schema['x-unstructured-reserved-key']!r}\n" + f"SUPPORTED_CONTEXT_VERSIONS = frozenset({policy['supported']!r})\n" + f"DIMENSION_FIELDS = {tuple(schema['x-unstructured-dimension-fields'])!r}\n" + ) + return ( + "# Generated by scripts/generate_invocation_contracts.py; do not edit by hand.\n" + "# ruff: noqa: E501\n" + f"# Source: {spec['schema_id']}\n" + + generated + + provenance + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + stale: list[str] = [] + for filename, spec in CONTRACTS.items(): + output = OUTPUT_DIR / filename + rendered = _render(spec) + if args.check: + if not output.exists() or output.read_text() != rendered: + stale.append(str(output.relative_to(ROOT))) + else: + output.write_text(rendered) + + if stale: + print("stale generated invocation contracts:", *stale, sep="\n ", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/api/test_api.py b/test/api/test_api.py index 61a6c94..0961c05 100644 --- a/test/api/test_api.py +++ b/test/api/test_api.py @@ -18,14 +18,24 @@ UsageData, wrap_in_fastapi, ) +from unstructured_platform_plugins.generated.error_audience_v1 import ErrorAudience from unstructured_platform_plugins.schema.filedata_meta import FileDataMeta +class PluginErrorMetadata(BaseModel): + error_type: str + error_reason: str + dependency: Optional[str] = None + audience: Optional[ErrorAudience] = None + retryable: bool = False + + class InvokeResponse(BaseModel): usage: list[UsageData] status_code: int filedata_meta: FileDataMeta status_code_text: Optional[str] = None + plugin_error: Optional[PluginErrorMetadata] = None output: Optional[Any] = None file_data: Optional[Union[FileData, BatchFileData]] = None @@ -222,6 +232,67 @@ def test_http_exception_handling(file_data): assert invoke_response.status_code_text == "Not found" +@pytest.mark.parametrize( + "file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data] +) +def test_user_error_declares_canonical_user_audience(file_data): + """Only the UserError family declares a user-actionable plugin error.""" + from test.assets.exception_status_code import function_raises_user_error as test_fn + + client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"file_data": file_data.model_dump()}) + invoke_response = InvokeResponse.model_validate(resp.json()) + + assert invoke_response.status_code >= 400 + assert invoke_response.plugin_error is not None + assert invoke_response.plugin_error.audience is ErrorAudience.USER + assert invoke_response.plugin_error.error_type == "configuration" + assert invoke_response.plugin_error.error_reason == "invalid_input" + assert invoke_response.plugin_error.retryable is False + + +@pytest.mark.parametrize( + "file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data] +) +def test_non_user_failures_declare_no_plugin_error(file_data): + """Anything undeclared is not the customer's: an orchestrator must not infer customer fault + from the status code, which also carries transport semantics.""" + from test.assets.exception_status_code import function_raises_provider_error as test_fn + + client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"file_data": file_data.model_dump()}) + invoke_response = InvokeResponse.model_validate(resp.json()) + + assert invoke_response.plugin_error is None + + +def test_streaming_user_error_declares_user_audience(): + """The streaming error envelope carries the same audience as the non-streaming path.""" + from test.assets.exception_status_code import ( + async_gen_function_raises_user_error_mid_stream as test_fn, + ) + + client = TestClient(wrap_in_fastapi(func=test_fn, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"file_data": mock_file_data[0].model_dump()}) + + assert resp.status_code == 200 + assert resp.headers["content-type"] == "application/x-ndjson" + + import json + + lines = resp.content.decode().strip().split("\n") + assert len(lines) == 2 # One yielded item, then the error envelope + + assert InvokeResponse.model_validate(json.loads(lines[0])).plugin_error is None + error_response = InvokeResponse.model_validate(json.loads(lines[1])) + assert error_response.status_code >= 400 + assert error_response.plugin_error is not None + assert error_response.plugin_error.audience is ErrorAudience.USER + + @pytest.mark.parametrize( "file_data", mock_file_data, ids=[type(fd).__name__ for fd in mock_file_data] ) @@ -608,9 +679,26 @@ def test_precheck_reports_failure_category_from_raised_error(): body = resp.json() assert body["status_code"] == 403 assert body["failure_category"] == "AUTH_PERMISSION_DENIED" + # A plain exception is not the customer's to fix. + assert body["plugin_error"] is None assert "credential rejected" in body["status_code_text"] +def test_precheck_declares_user_audience_like_invoke_does(): + from unstructured_ingest.error import UserError + + def user_fault_precheck() -> None: + raise UserError("bad credentials") + + client = TestClient( + wrap_in_fastapi(func=_no_params, plugin_id="mock_plugin", precheck_func=user_fault_precheck) + ) + + body = client.get("/precheck").json() + + assert body["plugin_error"]["audience"] == "user" + + def test_precheck_success_has_no_failure_category(): client = TestClient( wrap_in_fastapi(func=_no_params, plugin_id="mock_plugin", precheck_func=_passing_precheck) diff --git a/test/api/test_invocation_context.py b/test/api/test_invocation_context.py new file mode 100644 index 0000000..90bdec6 --- /dev/null +++ b/test/api/test_invocation_context.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import pytest +from utic_invocation_settings import ( + Blame, + DecryptionError, + IdentityNotMountedError, + KeyNotFoundError, + MalformedDagNodeSettingsError, + MalformedEnvelopeError, + SealedDagNodeSettingsRequiredError, +) + +from unstructured_platform_plugins.generated import error_audience_v1, invocation_context_v1 +from unstructured_platform_plugins.invocation_context import ( + RESERVED_CONTEXT_KEY, + InvocationContext, + UnsupportedContextVersionError, + dimensions, + extract_context, +) +from unstructured_platform_plugins.invocation_settings import http_status_for + +VALID = { + "schema_version": "1", + "invocation_id": "inv-1", + "job_id": "job-1", + "tenant_id": "tenant-1", + "dag_node_id": "node-1", + "dag_node_type": "chunker", + "record_id": "rec-1", + "attempt": 2, +} + + +def test_local_bindings_name_the_ratified_schema_ids(): + assert ( + invocation_context_v1.SCHEMA_ID + == "https://schemas.u10d.dev/invocation-context/v1.json" + ) + assert error_audience_v1.SCHEMA_ID == "https://schemas.u10d.dev/errors/audience/v1.json" + assert invocation_context_v1.RESERVED_CONTEXT_KEY == RESERVED_CONTEXT_KEY + assert invocation_context_v1.DIMENSION_FIELDS == ( + "invocation_id", + "tenant_id", + "org_id", + "job_id", + "workflow_id", + "attribution_id", + "dag_node_id", + "dag_node_type", + "dag_node_subtype", + "record_id", + "attempt", + ) + + +def test_extracts_identity_fields_from_body(): + context = extract_context({"file_data": {"path": "x"}, RESERVED_CONTEXT_KEY: VALID}) + assert context is not None + assert context.tenant_id == "tenant-1" + assert context.attempt == 2 + + +def test_absent_key_returns_none(): + assert extract_context({"file_data": {"path": "x"}}) is None + + +def test_accepts_already_parsed(): + context = InvocationContext(**VALID) + assert extract_context({RESERVED_CONTEXT_KEY: context}) is context + + +def test_present_but_null_fails_closed(): + # Same rule as the envelope: a context that silently vanishes takes tenant attribution with it. + with pytest.raises(MalformedEnvelopeError): + extract_context({RESERVED_CONTEXT_KEY: None}) + + +def test_present_but_not_an_object_fails_closed(): + with pytest.raises(MalformedEnvelopeError): + extract_context({RESERVED_CONTEXT_KEY: "tenant-1"}) + + +def test_unknown_schema_version_is_rejected_by_its_own_error(): + with pytest.raises(UnsupportedContextVersionError) as exc: + extract_context({RESERVED_CONTEXT_KEY: {**VALID, "schema_version": "2"}}) + assert "'2'" in str(exc.value) + + +@pytest.mark.parametrize("version", [None, 2, ["1"]]) +def test_a_mistyped_schema_version_is_malformed_not_version_skew(version): + # Only a well-typed version string this consumer does not know reads as deployment skew; + # a wrong-typed field is the caller's malformed context like any other field. + with pytest.raises(MalformedEnvelopeError): + extract_context({RESERVED_CONTEXT_KEY: {**VALID, "schema_version": version}}) + + +def test_misaligned_batch_lists_are_rejected(): + # invocation_ids is read positionally against record_ids, so a length mismatch would let + # every id past the gap name the wrong record. + with pytest.raises(MalformedEnvelopeError): + extract_context( + { + RESERVED_CONTEXT_KEY: { + **VALID, + "record_ids": ["r1", "r2"], + "invocation_ids": ["inv-1"], + } + } + ) + + +def test_partial_context_is_accepted(): + # A producer that populates only some identity facets degrades to less telemetry, not a + # failed invoke. + context = extract_context({RESERVED_CONTEXT_KEY: {"schema_version": "1", "job_id": "job-1"}}) + assert context is not None + assert context.job_id == "job-1" + assert context.tenant_id is None + + +def test_unknown_fields_survive_for_forward_compatibility(): + context = extract_context({RESERVED_CONTEXT_KEY: {**VALID, "future_field": "keep me"}}) + assert context is not None + assert context.model_extra["future_field"] == "keep me" + + +def test_batch_fields_are_index_aligned(): + # The controller emits one invocation id per record, using None where a record carried no + # context, so entry i always describes record i. + context = extract_context( + { + RESERVED_CONTEXT_KEY: { + **VALID, + "record_ids": ["rec-1", "rec-2", "rec-3"], + "invocation_ids": ["inv-1", None, "inv-3"], + } + } + ) + assert context is not None + assert len(context.record_ids) == len(context.invocation_ids) + assert dict(zip(context.record_ids, context.invocation_ids))["rec-2"] is None + + +class TestDimensions: + def test_returns_populated_identity_facets(self): + context = InvocationContext(**VALID) + + assert dimensions(context) == { + "invocation_id": "inv-1", + "job_id": "job-1", + "tenant_id": "tenant-1", + "dag_node_id": "node-1", + "dag_node_type": "chunker", + "record_id": "rec-1", + "attempt": 2, + } + + def test_excludes_batch_fields(self): + # These describe the work, not who it belongs to, and would blow up dimension cardinality. + context = InvocationContext.model_validate( + {**VALID, "record_ids": ["a"], "invocation_ids": ["b"]} + ) + + assert not {"record_ids", "invocation_ids"} & set(dimensions(context)) + + def test_unknown_producer_fields_are_not_promoted_to_dimensions(self): + context = InvocationContext.model_validate({**VALID, "future_field": "value"}) + + assert "future_field" not in dimensions(context) + + def test_absent_context_yields_no_dimensions(self): + assert dimensions(None) == {} + + +class TestHttpStatusFor: + """The transport's spelling of the library's normative `blame` -> status rule.""" + + def test_caller_blame_is_the_only_422(self): + assert http_status_for(MalformedEnvelopeError("x")) == 422 + assert MalformedEnvelopeError.blame is Blame.CALLER + + @pytest.mark.parametrize( + "error", + [ + DecryptionError("x"), + KeyNotFoundError("x"), + IdentityNotMountedError("x"), + SealedDagNodeSettingsRequiredError("x"), + MalformedDagNodeSettingsError("x"), + UnsupportedContextVersionError("x"), + ], + ) + def test_everything_else_is_5xx(self, error): + assert http_status_for(error) == 500 + + def test_an_unclassified_exception_is_not_blamed_on_the_caller(self): + assert http_status_for(RuntimeError("boom")) == 500 diff --git a/test/api/test_invocation_envelope.py b/test/api/test_invocation_envelope.py new file mode 100644 index 0000000..0565aa5 --- /dev/null +++ b/test/api/test_invocation_envelope.py @@ -0,0 +1,603 @@ +"""The transport for the reserved /invoke fields: binding, /metadata, the optional body cap. + +The *contract* these exercise — which shapes carry settings, what absence means — is owned and +tested in `utic_invocation_settings`. What is tested here is delivery: that the framework-parsed +body is resolved and bound for the handler, and that a payload which cannot be used fails the +request instead of reaching a handler as absence. +""" + +from __future__ import annotations + +import asyncio +from threading import get_ident +from typing import Optional + +import pytest +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient +from utic_invocation_settings import FIELDS_DOCUMENT_FORMAT, Blame + +import unstructured_platform_plugins.invocation_settings as invocation_settings_transport +from unstructured_platform_plugins.invocation_settings import ( + InvokeBodyLimitMiddleware, + add_metadata_route, + current_invocation_context, + current_invocation_settings, + install_invocation_envelope, +) + +BASE_CAPABILITIES = [ + "invocation_settings", + "invocation_context", +] +ALL_CAPABILITIES = [ + *BASE_CAPABILITIES, + "invoke_with_sealed_dag_node_settings_v2", +] + + +class TestMetadataRoute: + def test_advertises_transport_capabilities_by_default(self): + app = FastAPI() + add_metadata_route(app, identifier="plugin.test") + + with TestClient(app) as client: + payload = client.get("/metadata").json() + + assert payload == { + "api_version": "3", + "identifier": "plugin.test", + "capabilities": BASE_CAPABILITIES, + } + + def test_sealed_consumption_capability_is_opt_in(self): + app = FastAPI() + add_metadata_route(app, invoke_with_sealed_dag_node_settings_v2=True) + + with TestClient(app) as client: + payload = client.get("/metadata").json() + + assert payload["capabilities"] == ALL_CAPABILITIES + + def test_last_call_wins(self): + # A host wrapper registers /metadata at construction; the plugin's later call with its own + # identifier must replace it, not be shadowed by route order. + app = FastAPI() + add_metadata_route(app, identifier="wrapper.default") + add_metadata_route( + app, + identifier="plugin.test", + invoke_with_sealed_dag_node_settings_v2=True, + ) + + with TestClient(app) as client: + payload = client.get("/metadata").json() + + assert payload["identifier"] == "plugin.test" + assert payload["capabilities"] == ALL_CAPABILITIES + + def test_replaces_a_directly_registered_metadata_route(self): + # A route the app registered itself would otherwise win by route order and pin its stale + # payload. + app = FastAPI() + + @app.get("/metadata") + async def stale_metadata() -> dict: + return {"api_version": "3", "identifier": "stale", "capabilities": []} + + add_metadata_route( + app, + identifier="plugin.test", + invoke_with_sealed_dag_node_settings_v2=True, + ) + + with TestClient(app) as client: + payload = client.get("/metadata").json() + + assert payload["identifier"] == "plugin.test" + assert payload["capabilities"] == ALL_CAPABILITIES + + +class _Recorder: + """What the /invoke handler observed: the bound envelope, and whether it ran at all.""" + + def __init__(self): + self.called = False + self.handler_thread = None + self.seen_settings = "unset" + self.seen_context = "unset" + + +def _envelope_app(recorder: _Recorder, max_body_bytes: Optional[int] = None) -> FastAPI: + """A hand-rolled host app: the /invoke route reads the raw request, like a plugin that owns + its own route, so any body shape reaches the handler unless the dependency rejects it.""" + app = FastAPI() + if max_body_bytes is None: + install_invocation_envelope(app) + else: + install_invocation_envelope(app, max_body_bytes=max_body_bytes) + + @app.post("/invoke") + async def invoke(request: Request) -> dict: + recorder.called = True + recorder.handler_thread = get_ident() + recorder.seen_settings = current_invocation_settings() + recorder.seen_context = current_invocation_context() + return {} + + @app.get("/schema") + async def schema() -> dict: + recorder.called = True + return {} + + return app + + +class _SpecCompliantRootShim: + """A rooted deployment as the ASGI spec describes it: ``path`` carries the mount prefix and + ``root_path`` names it. ``TestClient(root_path=...)`` sets only ``root_path`` without + prefixing ``path``, so it cannot produce this shape.""" + + def __init__(self, app, root: str): + self.app = app + self.root = root + + async def __call__(self, scope, receive, send): + if scope["type"] == "http": + scope = {**scope, "path": self.root + scope["path"], "root_path": self.root} + await self.app(scope, receive, send) + + +def _post_invoke(payload, recorder: Optional[_Recorder] = None, **app_kwargs): + recorder = recorder if recorder is not None else _Recorder() + app = _envelope_app(recorder, **app_kwargs) + with TestClient(app, raise_server_exceptions=False) as client: + if isinstance(payload, bytes): + response = client.post("/invoke", content=payload) + else: + response = client.post("/invoke", json=payload) + return recorder, response + + +class TestInvocationEnvelopeBinding: + def test_binds_reserved_fields(self): + recorder, response = _post_invoke( + { + "element_dicts": "/in.json", + "invocation_settings": {"model": "m"}, + "invocation_context": {"schema_version": "1", "job_id": "job-1"}, + } + ) + + assert response.status_code == 200 + assert recorder.seen_settings == {"model": "m"} + assert recorder.seen_context.job_id == "job-1" + + def test_context_dimensions_are_attached_to_the_current_span(self, monkeypatch): + attributes = {} + + class _Span: + def set_attribute(self, key, value): + attributes[key] = value + + monkeypatch.setattr( + invocation_settings_transport.trace, + "get_current_span", + lambda: _Span(), + ) + + _, response = _post_invoke( + { + "invocation_context": { + "schema_version": "1", + "tenant_id": "tenant-1", + "job_id": "job-1", + "attempt": 2, + "record_ids": ["record-1"], + "invocation_ids": ["invocation-1"], + "future_field": "preserved-but-not-a-dimension", + } + } + ) + + assert response.status_code == 200 + assert attributes == { + "tenant_id": "tenant-1", + "job_id": "job-1", + "attempt": 2, + } + + def test_absent_fields_bind_none(self): + recorder, response = _post_invoke({"element_dicts": "/in.json"}) + + assert response.status_code == 200 + assert recorder.seen_settings is None + assert recorder.seen_context is None + + def test_malformed_nonempty_json_is_caller_error_when_settings_are_required(self, monkeypatch): + monkeypatch.setenv("FF_INVOCATION_SETTINGS", "true") + + recorder, response = _post_invoke(b"{") + + assert not recorder.called + assert response.status_code == 422 + assert response.json() == { + "detail": "Invalid JSON body", + "reason": "malformed_envelope", + } + + def test_empty_body_remains_absence_when_settings_are_required(self, monkeypatch): + monkeypatch.setenv("FF_INVOCATION_SETTINGS", "true") + + recorder, response = _post_invoke(b"") + + assert not recorder.called + assert response.status_code == 500 + assert response.json()["reason"] == "sealed_dag_node_settings_required" + + def test_non_dict_reserved_field_is_rejected(self): + recorder, response = _post_invoke({"invocation_settings": "not-a-dict"}) + + assert not recorder.called + assert response.status_code == 422 + body = response.json() + assert "invocation_settings" in body["detail"] + assert body["reason"] == "malformed_envelope" + + def test_context_with_an_unreadable_schema_version_fails_as_platform_error(self): + # An absent context permits boot-state behavior; an unreadable context must not be + # downgraded to absence. It is deployment skew, not a caller fault — a 422 would let an + # upstream blame classifier pin version skew on the customer. + recorder, response = _post_invoke({"invocation_context": {"schema_version": "99"}}) + + assert not recorder.called + assert response.status_code == 500 + body = response.json() + assert "invocation_context" in body["detail"] + assert "UnsupportedContextVersionError" in body["detail"] + assert body["reason"] == "unsupported_context_version" + + def test_malformed_context_is_rejected(self): + recorder, response = _post_invoke({"invocation_context": "not-an-object"}) + + assert not recorder.called + assert response.status_code == 422 + + def test_non_invoke_routes_are_untouched(self): + recorder = _Recorder() + app = _envelope_app(recorder) + + with TestClient(app) as client: + response = client.get("/schema") + + assert response.status_code == 200 + assert recorder.called + + def test_envelope_does_not_leak_between_requests(self): + recorder = _Recorder() + app = _envelope_app(recorder) + + with TestClient(app) as client: + client.post("/invoke", json={"invocation_settings": {"model": "m"}}) + client.post("/invoke", json={"element_dicts": "/in.json"}) + + assert recorder.seen_settings is None + assert recorder.seen_context is None + + def test_install_after_an_invoke_route_fails_loudly(self): + app = FastAPI() + + @app.post("/invoke") + async def invoke() -> dict: + return {} + + with pytest.raises(RuntimeError, match="POST /invoke"): + install_invocation_envelope(app) + + def test_install_after_metadata_but_before_invoke_is_supported(self): + recorder = _Recorder() + app = FastAPI() + add_metadata_route(app, invoke_with_sealed_dag_node_settings_v2=True) + install_invocation_envelope(app) + + @app.post("/invoke") + async def invoke() -> dict: + recorder.seen_settings = current_invocation_settings() + return {} + + with TestClient(app) as client: + response = client.post("/invoke", json={"invocation_settings": {"model": "m"}}) + + assert response.status_code == 200 + assert recorder.seen_settings == {"model": "m"} + + def test_mixed_method_route_does_not_bind_or_parse_get(self): + recorder = _Recorder() + app = FastAPI() + install_invocation_envelope(app) + + @app.api_route("/invoke", methods=["GET", "POST"]) + async def invoke() -> dict: + recorder.seen_settings = current_invocation_settings() + return {} + + with TestClient(app) as client: + response = client.get("/invoke") + + assert response.status_code == 200 + assert recorder.seen_settings is None + + def test_root_path_does_not_prevent_invoke_binding(self): + recorder = _Recorder() + app = _envelope_app(recorder) + + with TestClient(app, root_path="/plugins/chunker") as client: + response = client.post("/invoke", json={"invocation_settings": {"model": "rooted"}}) + + assert response.status_code == 200 + assert recorder.seen_settings == {"model": "rooted"} + + def test_spec_compliant_root_path_scope_still_binds(self): + recorder = _Recorder() + app = _envelope_app(recorder) + + with TestClient(_SpecCompliantRootShim(app, "/plugins/chunker")) as client: + response = client.post("/invoke", json={"invocation_settings": {"model": "rooted"}}) + + assert response.status_code == 200 + assert recorder.seen_settings == {"model": "rooted"} + + def test_oversized_body_is_rejected(self): + recorder, response = _post_invoke( + {"invocation_settings": {"pad": "x" * 64}}, max_body_bytes=16 + ) + + assert not recorder.called + assert response.status_code == 413 + + def test_body_limit_is_disabled_by_default(self): + app = _envelope_app(_Recorder()) + + assert app.state.invocation_envelope_max_body_bytes is None + assert all( + middleware.cls is not InvokeBodyLimitMiddleware for middleware in app.user_middleware + ) + + def test_spec_compliant_root_path_scope_still_caps_body(self): + recorder = _Recorder() + app = _envelope_app(recorder, max_body_bytes=16) + + with TestClient( + _SpecCompliantRootShim(app, "/plugins/chunker"), raise_server_exceptions=False + ) as client: + response = client.post("/invoke", json={"invocation_settings": {"pad": "x" * 64}}) + + assert not recorder.called + assert response.status_code == 413 + + def test_repeat_install_with_the_same_cap_is_a_noop(self): + recorder = _Recorder() + app = _envelope_app(recorder, max_body_bytes=1024) + install_invocation_envelope(app, max_body_bytes=1024) + + with TestClient(app) as client: + response = client.post("/invoke", json={"element_dicts": "/in.json"}) + + assert response.status_code == 200 + assert recorder.called + + def test_repeat_install_with_a_different_cap_fails_loudly(self): + app = FastAPI() + install_invocation_envelope(app, max_body_bytes=16) + + with pytest.raises(ValueError, match="max_body_bytes"): + install_invocation_envelope(app, max_body_bytes=32) + + +class TestInvokeBodyLimitMiddleware: + """ASGI-level behavior of the streaming byte counter: no buffering, disconnect pass-through.""" + + @staticmethod + def _run(middleware, scope, chunks, downstream_app=None) -> tuple[list, list]: + received = [] + sent = [] + + async def receive(): + return chunks.pop(0) + + async def send(message): + sent.append(message) + + async def default_downstream(scope, receive, send): + while True: + message = await receive() + received.append(message) + if message["type"] != "http.request" or not message.get("more_body"): + break + await send({"type": "http.response.start", "status": 200, "headers": []}) + await send({"type": "http.response.body", "body": b"{}"}) + + middleware = middleware(downstream_app or default_downstream) + asyncio.run(middleware(scope, receive, send)) + return received, sent + + def test_chunked_body_over_the_cap_answers_413_and_cuts_downstream(self): + chunks = [ + {"type": "http.request", "body": b"x" * 10, "more_body": True}, + {"type": "http.request", "body": b"x" * 10, "more_body": False}, + ] + received, sent = self._run( + lambda app: InvokeBodyLimitMiddleware(app, max_body_bytes=16), + {"type": "http", "method": "POST", "path": "/invoke"}, + chunks, + ) + + assert received[-1] == {"type": "http.disconnect"} + assert sent[0]["status"] == 413 + + def test_rooted_scope_over_the_cap_answers_413(self): + # Per the ASGI spec, `path` includes the deployment root_path; the cap must key on the + # mount-relative path, the same one the router matches. + chunks = [{"type": "http.request", "body": b"x" * 20, "more_body": False}] + received, sent = self._run( + lambda app: InvokeBodyLimitMiddleware(app, max_body_bytes=16), + { + "type": "http", + "method": "POST", + "path": "/plugins/chunker/invoke", + "root_path": "/plugins/chunker", + }, + chunks, + ) + + assert received[-1] == {"type": "http.disconnect"} + assert sent[0]["status"] == 413 + + def test_client_disconnect_passes_through_uncounted(self): + chunks = [{"type": "http.disconnect"}] + received, sent = self._run( + lambda app: InvokeBodyLimitMiddleware(app, max_body_bytes=16), + {"type": "http", "method": "POST", "path": "/invoke"}, + chunks, + ) + + assert received == [{"type": "http.disconnect"}] + + def test_non_invoke_requests_are_not_capped(self): + chunks = [{"type": "http.request", "body": b"x" * 100, "more_body": False}] + received, sent = self._run( + lambda app: InvokeBodyLimitMiddleware(app, max_body_bytes=16), + {"type": "http", "method": "GET", "path": "/schema"}, + chunks, + ) + + assert sent[0]["status"] == 200 + + def test_coincident_downstream_failure_logs_only_its_type(self, caplog): + class CoincidentDownstreamFailure(Exception): + pass + + async def downstream(_scope, receive, _send): + await receive() + raise CoincidentDownstreamFailure("request-secret-sentinel") + + chunks = [{"type": "http.request", "body": b"x" * 20, "more_body": False}] + with caplog.at_level("DEBUG", logger=invocation_settings_transport.__name__): + _, sent = self._run( + lambda app: InvokeBodyLimitMiddleware(app, max_body_bytes=16), + {"type": "http", "method": "POST", "path": "/invoke"}, + chunks, + downstream_app=downstream, + ) + + assert sent[0]["status"] == 413 + assert "CoincidentDownstreamFailure" in caplog.text + assert "request-secret-sentinel" not in caplog.text + + +class _ResolutionFailure(Exception): + reason = "test_resolution_failure" + + def __init__(self, blame: Blame, secret: str = ""): + super().__init__(secret) + self.blame = blame + + +class TestSettingsResolutionBoundary: + """The library owns settings shape and crypto; this package owns delivery and HTTP mapping.""" + + def test_v2_document_is_delegated_and_final_mapping_is_bound(self, monkeypatch): + opaque_payload = { + "dag_node_settings": { + "format": FIELDS_DOCUMENT_FORMAT, + "settings": { + "api_key": { + "format": "u10d.invocation-settings.field.v1", + "opaque": "member", + } + }, + } + } + resolved = {"api_key": "resolved-secret", "max_characters": 700} + seen = [] + + def resolve(payload): + seen.append(payload) + return resolved + + monkeypatch.setattr(invocation_settings_transport, "resolve_invocation_settings", resolve) + + recorder, response = _post_invoke({"invocation_settings": opaque_payload}) + + assert response.status_code == 200 + assert seen == [opaque_payload] + assert recorder.seen_settings == resolved + + def test_empty_v2_document_is_resolved_and_bound(self): + document_payload = { + "dag_node_settings": { + "format": FIELDS_DOCUMENT_FORMAT, + "settings": {}, + } + } + + recorder, response = _post_invoke({"invocation_settings": document_payload}) + + assert response.status_code == 200 + assert recorder.seen_settings == {} + + def test_resolution_runs_off_the_event_loop(self, monkeypatch): + resolver_threads = [] + + def resolve(_payload): + resolver_threads.append(get_ident()) + return {"model": "m"} + + monkeypatch.setattr(invocation_settings_transport, "resolve_invocation_settings", resolve) + + recorder, response = _post_invoke({"invocation_settings": {"opaque": "payload"}}) + + assert response.status_code == 200 + assert recorder.called + assert len(resolver_threads) == 1 + assert resolver_threads[0] != recorder.handler_thread + + @pytest.mark.parametrize( + ("blame", "expected_status"), + [(Blame.CALLER, 422), (Blame.RECIPIENT, 500)], + ) + def test_resolution_failure_uses_blame_for_http_status( + self, monkeypatch, blame, expected_status + ): + def fail(_payload): + raise _ResolutionFailure(blame) + + monkeypatch.setattr(invocation_settings_transport, "resolve_invocation_settings", fail) + + recorder, response = _post_invoke({"invocation_settings": {"opaque": "payload"}}) + + assert response.status_code == expected_status + assert response.json()["reason"] == _ResolutionFailure.reason + assert not recorder.called + + def test_resolution_failure_never_exposes_exception_message(self, monkeypatch, caplog): + secret = "sealed-settings-sentinel-secret" + + def fail(_payload): + raise _ResolutionFailure(Blame.RECIPIENT, secret) + + monkeypatch.setattr(invocation_settings_transport, "resolve_invocation_settings", fail) + + recorder, response = _post_invoke({"invocation_settings": {"opaque": "payload"}}) + + assert response.status_code == 500 + assert secret not in caplog.text + assert secret not in response.json()["detail"] + assert not recorder.called + + +def test_non_object_bodies_pass_through_when_sealed_settings_not_required(): + recorder, response = _post_invoke(b"") + + assert response.status_code == 200 + assert recorder.seen_settings is None diff --git a/test/api/test_invocation_settings.py b/test/api/test_invocation_settings.py new file mode 100644 index 0000000..37c242d --- /dev/null +++ b/test/api/test_invocation_settings.py @@ -0,0 +1,172 @@ +"""The wrapper-installed invocation-settings surface: /metadata and reserved-field binding.""" + +import json +from typing import Optional + +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import BaseModel, Json + +from unstructured_platform_plugins.etl_uvicorn.api_generator import wrap_in_fastapi +from unstructured_platform_plugins.invocation_settings import ( + current_invocation_settings, + install_invocation_envelope, +) + + +class _Echo(BaseModel): + content: str + settings: Optional[dict] + + +def _echo_settings(content: str) -> _Echo: + return _Echo(content=content, settings=current_invocation_settings()) + + +class _NestedJsonRequest(BaseModel): + payload: Json[dict[str, object]] + + +def test_metadata_route_is_registered_with_transport_capabilities(): + client = TestClient(wrap_in_fastapi(func=_echo_settings, plugin_id="mock_plugin")) + + resp = client.get("/metadata") + + assert resp.status_code == 200 + payload = resp.json() + assert payload["identifier"] == "mock_plugin" + assert payload["capabilities"] == ["invocation_settings", "invocation_context"] + + +def test_sealed_capability_is_opt_in(): + client = TestClient( + wrap_in_fastapi( + func=_echo_settings, + plugin_id="mock_plugin", + invoke_with_sealed_dag_node_settings_v2=True, + ) + ) + + payload = client.get("/metadata").json() + + assert "invoke_with_sealed_dag_node_settings_v2" in payload["capabilities"] + + +def test_reserved_settings_field_binds_without_appearing_in_schema(): + app = wrap_in_fastapi(func=_echo_settings, plugin_id="mock_plugin") + client = TestClient(app) + + resp = client.post( + "/invoke", + json={"content": "hello", "invocation_settings": {"model": "m"}}, + ) + + assert resp.status_code == 200 + output = resp.json()["output"] + assert output == {"content": "hello", "settings": {"model": "m"}} + # The wrapper does not add the reserved field to the generated handler input model. + openapi = app.openapi() + request_schema = openapi["paths"]["/invoke"]["post"]["requestBody"]["content"][ + "application/json" + ]["schema"] + schema_name = request_schema["$ref"].rsplit("/", 1)[-1] + properties = openapi["components"]["schemas"][schema_name]["properties"] + assert "invocation_settings" not in properties + + +def test_generated_invoke_route_classifies_malformed_json_as_caller_error(): + client = TestClient(wrap_in_fastapi(func=_echo_settings, plugin_id="mock_plugin")) + + resp = client.post( + "/invoke", + content=b"{", + headers={"content-type": "application/json"}, + ) + + assert resp.status_code == 422 + assert resp.json() == { + "detail": "Invalid JSON body", + "reason": "malformed_envelope", + } + + +def test_generated_invoke_route_preserves_other_request_validation_errors(): + client = TestClient(wrap_in_fastapi(func=_echo_settings, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={}) + + assert resp.status_code == 422 + payload = resp.json() + assert "reason" not in payload + assert isinstance(payload["detail"], list) + assert any(error["loc"] == ["body", "content"] for error in payload["detail"]) + + +def test_generated_invoke_route_preserves_malformed_nested_json_field_error(): + app = FastAPI() + install_invocation_envelope(app) + + @app.post("/invoke") + async def invoke(_request: _NestedJsonRequest): + return None + + client = TestClient(app) + + # The outer request document is valid; only the string inside its Pydantic Json field is not. + resp = client.post("/invoke", json={"payload": "{"}) + + assert resp.status_code == 422 + payload = resp.json() + assert "reason" not in payload + assert isinstance(payload["detail"], list) + assert any( + error["type"] == "json_invalid" and error["loc"] == ["body", "payload"] + for error in payload["detail"] + ) + + +def test_sync_function_sees_bound_settings_across_the_executor(): + # Sync functions run in an executor thread; the context must be copied there or the + # request-scoped binding would read as absent. + def sync_echo(content: str) -> _Echo: + return _Echo(content=content, settings=current_invocation_settings()) + + client = TestClient(wrap_in_fastapi(func=sync_echo, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"content": "hello", "invocation_settings": {"model": "m"}}) + + assert resp.json()["output"]["settings"] == {"model": "m"} + + +async def _async_echo(content: str) -> _Echo: + return _Echo(content=content, settings=current_invocation_settings()) + + +def test_async_function_sees_bound_settings(): + client = TestClient(wrap_in_fastapi(func=_async_echo, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"content": "hello", "invocation_settings": {"model": "m"}}) + + assert resp.json()["output"]["settings"] == {"model": "m"} + + +async def _stream_echo(content: str) -> _Echo: + yield _Echo(content=content, settings=current_invocation_settings()) + + +def test_async_generator_sees_bound_settings_during_stream_iteration(): + client = TestClient(wrap_in_fastapi(func=_stream_echo, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"content": "hello", "invocation_settings": {"model": "m"}}) + + line = json.loads(resp.text.strip()) + assert line["output"]["settings"] == {"model": "m"} + + +def test_absent_reserved_fields_bind_none(): + client = TestClient(wrap_in_fastapi(func=_echo_settings, plugin_id="mock_plugin")) + + resp = client.post("/invoke", json={"content": "hello"}) + + assert resp.status_code == 200 + assert resp.json()["output"] == {"content": "hello", "settings": None} diff --git a/test/api/test_settings_scoped_cache.py b/test/api/test_settings_scoped_cache.py new file mode 100644 index 0000000..4b725bb --- /dev/null +++ b/test/api/test_settings_scoped_cache.py @@ -0,0 +1,145 @@ +from unittest.mock import MagicMock + +import pytest + +from unstructured_platform_plugins.invocation_settings import ( + SettingsScopedCache, + settings_cache_key, +) + + +class TestSettingsCacheKey: + def test_key_is_insensitive_to_key_order(self): + assert settings_cache_key({"a": 1, "b": 2}) == settings_cache_key({"b": 2, "a": 1}) + + def test_key_is_sensitive_to_values(self): + assert settings_cache_key({"a": 1}) != settings_cache_key({"a": 2}) + + def test_secret_values_do_not_appear_in_the_key(self): + secret = "sk-super-secret-credential" + key = settings_cache_key({"api_key": secret}) + assert secret not in key + + +class TestSettingsScopedCache: + def test_second_lookup_with_same_settings_does_not_rebuild(self): + cache = SettingsScopedCache() + build = MagicMock(return_value="handler") + + first = cache.get_or_build({"model": "a"}, build) + second = cache.get_or_build({"model": "a"}, build) + + assert first == second == "handler" + build.assert_called_once() + + def test_change_in_resolved_field_builds_a_distinct_value(self): + cache = SettingsScopedCache() + + first = cache.get_or_build({"model": "a"}, lambda: object()) + second = cache.get_or_build({"model": "b"}, lambda: object()) + + assert first is not second + + def test_entry_expires_after_ttl(self): + now = [0.0] + cache = SettingsScopedCache(ttl_seconds=10, clock=lambda: now[0]) + build = MagicMock(return_value="handler") + + cache.get_or_build({"model": "a"}, build) + now[0] = 11.0 + cache.get_or_build({"model": "a"}, build) + + assert build.call_count == 2 + + def test_entry_survives_within_ttl(self): + now = [0.0] + cache = SettingsScopedCache(ttl_seconds=10, clock=lambda: now[0]) + build = MagicMock(return_value="handler") + + cache.get_or_build({"model": "a"}, build) + now[0] = 9.0 + cache.get_or_build({"model": "a"}, build) + + build.assert_called_once() + + def test_size_bound_evicts_least_recently_used(self): + cache = SettingsScopedCache(maxsize=2) + builds = {name: MagicMock(return_value=name) for name in ("a", "b", "c")} + + cache.get_or_build({"model": "a"}, builds["a"]) + cache.get_or_build({"model": "b"}, builds["b"]) + # Refresh "a" so "b" is the eviction candidate when "c" lands. + cache.get_or_build({"model": "a"}, builds["a"]) + cache.get_or_build({"model": "c"}, builds["c"]) + + cache.get_or_build({"model": "a"}, builds["a"]) + cache.get_or_build({"model": "b"}, builds["b"]) + + builds["a"].assert_called_once() + assert builds["b"].call_count == 2 + + def test_clear_forces_rebuild(self): + cache = SettingsScopedCache() + build = MagicMock(return_value="handler") + + cache.get_or_build({"model": "a"}, build) + cache.clear() + cache.get_or_build({"model": "a"}, build) + + assert build.call_count == 2 + + @pytest.mark.parametrize("kwargs", [{"ttl_seconds": 0}, {"ttl_seconds": -1}, {"maxsize": 0}]) + def test_degenerate_bounds_are_rejected(self, kwargs): + with pytest.raises(ValueError): + SettingsScopedCache(**kwargs) + + +class TestHandlerFor: + def test_absent_settings_return_the_boot_handler(self): + cache = SettingsScopedCache() + build = MagicMock() + + handler = cache.handler_for(None, boot=lambda: "boot-handler", build=build) + + assert handler == "boot-handler" + build.assert_not_called() + + def test_absent_settings_without_a_boot_handler_fail(self): + cache = SettingsScopedCache() + + with pytest.raises(ValueError): + cache.handler_for(None, boot=lambda: None, build=MagicMock()) + + def test_resolved_settings_build_and_cache_per_distinct_mapping(self): + cache = SettingsScopedCache() + boot = MagicMock() + build = MagicMock(return_value="handler") + + first = cache.handler_for({"model": "a"}, boot=boot, build=build) + second = cache.handler_for({"model": "a"}, boot=boot, build=build) + + assert first == second == "handler" + build.assert_called_once() + boot.assert_not_called() + + +class TestExpirySweep: + def test_an_idle_tenants_expired_handler_is_dropped_by_anothers_insert(self): + clock = MagicMock(side_effect=[0.0, 0.0, 100.0, 100.0]) + cache = SettingsScopedCache(ttl_seconds=50, clock=clock) + + cache.get_or_build({"tenant": "a"}, lambda: "handler-a") + cache.get_or_build({"tenant": "b"}, lambda: "handler-b") + + assert len(cache._entries) == 1 + + def test_an_entry_expiring_while_build_runs_is_swept_by_that_insert(self): + # Lookup for tenant b happens at t=10 (a still live), but its build finishes at t=100 + # (a lapsed); the sweep must judge staleness at insert time, not lookup time. + clock = MagicMock(side_effect=[0.0, 0.0, 10.0, 100.0]) + cache = SettingsScopedCache(ttl_seconds=50, clock=clock) + + cache.get_or_build({"tenant": "a"}, lambda: "handler-a") + cache.get_or_build({"tenant": "b"}, lambda: "handler-b") + + assert len(cache._entries) == 1 diff --git a/test/assets/exception_status_code.py b/test/assets/exception_status_code.py index 6e35997..034f3af 100644 --- a/test/assets/exception_status_code.py +++ b/test/assets/exception_status_code.py @@ -1,6 +1,7 @@ """Test assets for testing exception handling with various status_code scenarios.""" from fastapi import HTTPException +from typing_extensions import TypedDict from unstructured_ingest.error import UnstructuredIngestError @@ -137,3 +138,27 @@ async def async_gen_function_raises_unstructured_ingest_error_with_none_status_c error = UnstructuredIngestError("Async gen test UnstructuredIngestError with None status_code") error.status_code = None raise error + + +class PartialStreamResponse(TypedDict): + partial: str + + +async def async_gen_function_raises_user_error_mid_stream() -> PartialStreamResponse: + """Async generator that yields once, then raises UserError.""" + from unstructured_ingest.error import UserError + + yield PartialStreamResponse(partial="output") + raise UserError("Customer-owned resource rejected the request") + + +def function_raises_user_error() -> None: + from unstructured_ingest.error import UserError + + raise UserError("Customer-owned resource rejected the request") + + +def function_raises_provider_error() -> None: + from unstructured_ingest.error import ProviderError + + raise ProviderError("Upstream provider failed") diff --git a/unstructured_platform_plugins/__version__.py b/unstructured_platform_plugins/__version__.py index 26262e4..2a08aec 100644 --- a/unstructured_platform_plugins/__version__.py +++ b/unstructured_platform_plugins/__version__.py @@ -1 +1 @@ -__version__ = "0.0.46" # pragma: no cover +__version__ = "0.1.0" # pragma: no cover diff --git a/unstructured_platform_plugins/etl_uvicorn/api_generator.py b/unstructured_platform_plugins/etl_uvicorn/api_generator.py index 5534260..767ae3f 100644 --- a/unstructured_platform_plugins/etl_uvicorn/api_generator.py +++ b/unstructured_platform_plugins/etl_uvicorn/api_generator.py @@ -12,7 +12,7 @@ from starlette.responses import RedirectResponse from typing_extensions import deprecated from unstructured_ingest.data_types.file_data import BatchFileData, FileData, file_data_from_dict -from unstructured_ingest.error import UnstructuredIngestError +from unstructured_ingest.error import UnstructuredIngestError, UserError from uvicorn.config import LOG_LEVELS from uvicorn.importer import import_from_string @@ -25,6 +25,14 @@ get_schema_dict, map_inputs, ) +from unstructured_platform_plugins.generated.error_audience_v1 import ErrorAudience +from unstructured_platform_plugins.invocation_settings import ( + add_metadata_route, + current_invocation_context, + current_invocation_settings, + install_invocation_envelope, + invocation_envelope, +) from unstructured_platform_plugins.schema import FileDataMeta, NewRecord, UsageData from unstructured_platform_plugins.schema.json_schema import ( schema_to_base_model, @@ -45,6 +53,16 @@ class MessageChannels(BaseModel): warnings: list[str] = Field(default_factory=list) +class PluginErrorMetadata(BaseModel): + """Canonical metadata nested at ``plugin_error`` on a failed legacy response.""" + + error_type: str + error_reason: str + dependency: Optional[str] = None + audience: Optional[ErrorAudience] = None + retryable: bool = False + + def log_func_and_body(func: Callable, body: Optional[str] = None) -> None: msg = None if logger.level == LOG_LEVELS.get("debug", logging.NOTSET): @@ -93,6 +111,22 @@ def failure_category_of(error: BaseException) -> Optional[str]: return category if isinstance(category, str) else None +def plugin_error_of(error: BaseException) -> Optional[PluginErrorMetadata]: + """Map the legacy UserError family onto the canonical plugin-error envelope. + + The ratified ErrorAudience vocabulary owns the wire spelling. A non-user failure remains + unclassified: orchestrators must not infer actionability from its HTTP status. + """ + if not isinstance(error, UserError): + return None + return PluginErrorMetadata( + error_type="configuration", + error_reason=failure_category_of(error) or "invalid_input", + audience=ErrorAudience.USER, + retryable=False, + ) + + def status_code_of(error: BaseException) -> int: """Return the error's status_code only when it is an int in the HTTP range, else 500.""" status_code = _error_attr(error, "status_code") @@ -169,9 +203,15 @@ def wrap_in_fastapi( func: Callable, plugin_id: str, precheck_func: Optional[Callable] = None, + invoke_with_sealed_dag_node_settings_v2: bool = False, ) -> FastAPI: try: - return _wrap_in_fastapi(func=func, plugin_id=plugin_id, precheck_func=precheck_func) + return _wrap_in_fastapi( + func=func, + plugin_id=plugin_id, + precheck_func=precheck_func, + invoke_with_sealed_dag_node_settings_v2=invoke_with_sealed_dag_node_settings_v2, + ) except Exception as e: logger.error(f"failed to wrap function in FastAPI: {e}", exc_info=True) raise EtlApiException(e) from e @@ -181,6 +221,7 @@ def _wrap_in_fastapi( func: Callable, plugin_id: str, precheck_func: Optional[Callable] = None, + invoke_with_sealed_dag_node_settings_v2: bool = False, ) -> FastAPI: if precheck_func is not None: check_precheck_func(precheck_func=precheck_func) @@ -191,6 +232,8 @@ def _wrap_in_fastapi( logger.warning("usage data not an expected parameter, omitting") fastapi_app = FastAPI() + # Installation contributes a public router dependency, so it must happen before /invoke. + install_invocation_envelope(fastapi_app) response_type = get_output_sig(func) filedata_meta_model = update_filedata_model(response_type) @@ -202,6 +245,7 @@ class InvokeResponse(BaseModel): filedata_meta: Optional[filedata_meta_model] = None status_code_text: Optional[str] = None failure_category: Optional[str] = None + plugin_error: Optional[PluginErrorMetadata] = None output: Optional[response_type] = None message_channels: MessageChannels = Field(default_factory=MessageChannels) @@ -226,10 +270,34 @@ async def wrap_fn(func: Callable, kwargs: Optional[dict[str, Any]] = None) -> Re request_dict["filedata_meta"] = filedata_meta try: if inspect.isasyncgenfunction(func): + bound_settings = current_invocation_settings() + bound_context = current_invocation_context() + # Stream response if function is an async generator async def _stream_response(): - try: - async for output in func(**(request_dict or {})): + # FastAPI 0.117 closes yield dependencies before iterating a + # StreamingResponse. Re-enter the captured binding inside the generator so the + # plugin sees the right request regardless of dependency-cleanup timing. + with invocation_envelope(bound_settings, bound_context): + try: + async for output in func(**(request_dict or {})): + yield ( + InvokeResponse( + usage=usage, + message_channels=message_channels, + filedata_meta=filedata_meta_model.model_validate( + filedata_meta.model_dump() + ), + status_code=status.HTTP_200_OK, + output=output, + file_data=request_dict.get("file_data", None), + ).model_dump_json() + + "\n" + ) + except Exception as e: + logger.error( + f"Failure streaming response: {_safe_str(e)}", exc_info=True + ) yield ( InvokeResponse( usage=usage, @@ -237,27 +305,13 @@ async def _stream_response(): filedata_meta=filedata_meta_model.model_validate( filedata_meta.model_dump() ), - status_code=status.HTTP_200_OK, - output=output, - file_data=request_dict.get("file_data", None), + status_code=status_code_of(e), + status_code_text=f"[{type(e).__name__}] {_safe_str(e)}", + failure_category=failure_category_of(e), + plugin_error=plugin_error_of(e), ).model_dump_json() + "\n" ) - except Exception as e: - logger.error(f"Failure streaming response: {_safe_str(e)}", exc_info=True) - yield ( - InvokeResponse( - usage=usage, - message_channels=message_channels, - filedata_meta=filedata_meta_model.model_validate( - filedata_meta.model_dump() - ), - status_code=status_code_of(e), - status_code_text=f"[{type(e).__name__}] {_safe_str(e)}", - failure_category=failure_category_of(e), - ).model_dump_json() - + "\n" - ) return StreamingResponse(_stream_response(), media_type="application/x-ndjson") else: @@ -299,6 +353,7 @@ async def _stream_response(): status_code=status_code_of(exc), status_code_text=_safe_str(exc), failure_category=failure_category_of(exc), + plugin_error=plugin_error_of(exc), file_data=request_dict.get("file_data", None), ) except Exception as invoke_error: @@ -376,6 +431,7 @@ class InvokePrecheckResponse(BaseModel): status_code: int status_code_text: Optional[str] = None failure_category: Optional[str] = None + plugin_error: Optional[PluginErrorMetadata] = None @fastapi_app.get("/schema") async def get_schema() -> SchemaOutputResponse: @@ -391,6 +447,7 @@ async def run_precheck() -> InvokePrecheckResponse: status_code=fn_response.status_code, status_code_text=fn_response.status_code_text, failure_category=fn_response.failure_category, + plugin_error=fn_response.plugin_error, usage=fn_response.usage, ) else: @@ -406,6 +463,13 @@ async def get_id() -> str: except TypeError as e: raise TypeError(f"failed to validate function schema: {e}") from e + # Registered last so add_metadata_route replaces any /metadata the plugin registered itself. + add_metadata_route( + fastapi_app, + identifier=plugin_id, + invoke_with_sealed_dag_node_settings_v2=invoke_with_sealed_dag_node_settings_v2, + ) + FastAPIInstrumentor.instrument_app( fastapi_app, tracer_provider=get_trace_provider(), meter_provider=get_metric_provider() ) @@ -420,6 +484,7 @@ def generate_fast_api( id_method: Optional[str] = None, precheck_str: Optional[str] = None, precheck_method: Optional[str] = None, + invoke_with_sealed_dag_node_settings_v2: bool = False, ) -> FastAPI: instance = import_from_string(app) func = get_func(instance, method_name) @@ -438,4 +503,9 @@ def generate_fast_api( elif precheck_method: precheck_func = get_func(instance, precheck_method) - return wrap_in_fastapi(func=func, plugin_id=plugin_id, precheck_func=precheck_func) + return wrap_in_fastapi( + func=func, + plugin_id=plugin_id, + precheck_func=precheck_func, + invoke_with_sealed_dag_node_settings_v2=invoke_with_sealed_dag_node_settings_v2, + ) diff --git a/unstructured_platform_plugins/etl_uvicorn/main.py b/unstructured_platform_plugins/etl_uvicorn/main.py index 600e0c1..72f7ff2 100644 --- a/unstructured_platform_plugins/etl_uvicorn/main.py +++ b/unstructured_platform_plugins/etl_uvicorn/main.py @@ -56,6 +56,7 @@ def api_wrapper( plugin_id_method: Optional[str] = None, precheck_app: Optional[str] = None, precheck_app_method: Optional[str] = None, + sealed_dag_node_settings_v2: bool = False, **kwargs, ): # Make sure logging is configured before the call to run() so any setup has the same format @@ -73,6 +74,7 @@ def api_wrapper( id_method=plugin_id_method, precheck_str=precheck_app, precheck_method=precheck_app_method, + invoke_with_sealed_dag_node_settings_v2=sealed_dag_node_settings_v2, ) # Explicitly map values that are manipulated in the original # call to run(), preventing **kwargs reference @@ -130,6 +132,14 @@ def api_wrapper( "If precheck-app not provided, assumes method " "lives on main class passes in.", ), + click.Option( + ["--sealed-dag-node-settings-v2"], + is_flag=True, + default=False, + help="Advertise the invoke_with_sealed_dag_node_settings_v2 capability on " + "/metadata. Set only for a plugin that consumes per-invoke settings " + "through current_invocation_settings().", + ), ] ) return cmd diff --git a/unstructured_platform_plugins/generated/__init__.py b/unstructured_platform_plugins/generated/__init__.py new file mode 100644 index 0000000..91143b5 --- /dev/null +++ b/unstructured_platform_plugins/generated/__init__.py @@ -0,0 +1 @@ +"""Schema-generated local wire-contract bindings.""" diff --git a/unstructured_platform_plugins/generated/error_audience_v1.py b/unstructured_platform_plugins/generated/error_audience_v1.py new file mode 100644 index 0000000..dc52918 --- /dev/null +++ b/unstructured_platform_plugins/generated/error_audience_v1.py @@ -0,0 +1,28 @@ +# Generated by scripts/generate_invocation_contracts.py; do not edit by hand. +# ruff: noqa: E501 +# Source: https://schemas.u10d.dev/errors/audience/v1.json +"""Error Audience — generated from the ratified JSON Schema. +https://schemas.u10d.dev/errors/audience/v1 + +Non-null actionability audience carried by plugin_error.audience; distinct from technical fault- +source classifications such as utic_invocation_settings.Blame +""" + +from __future__ import annotations + +from enum import Enum + + +class ErrorAudienceValue(str, Enum): + """Who can act to resolve an error.""" + USER = "user" + PLATFORM = "platform" + + +# Non-null actionability audience carried by plugin_error.audience; distinct from technical +# fault-source classifications such as utic_invocation_settings.Blame +ErrorAudience = ErrorAudienceValue + +# Generation provenance used by drift tests and reviewers. +SCHEMA_ID = 'https://schemas.u10d.dev/errors/audience/v1.json' +SCHEMA_SHA256 = '36d017a1b132d0c06ded5a60376c425e8d5a5e681f134cfdc4dcb1fa4c9498d8' diff --git a/unstructured_platform_plugins/generated/invocation_context_v1.py b/unstructured_platform_plugins/generated/invocation_context_v1.py new file mode 100644 index 0000000..e7e70c5 --- /dev/null +++ b/unstructured_platform_plugins/generated/invocation_context_v1.py @@ -0,0 +1,89 @@ +# Generated by scripts/generate_invocation_contracts.py; do not edit by hand. +# ruff: noqa: E501 +# Source: https://schemas.u10d.dev/invocation-context/v1.json +"""Invocation Context — generated from the ratified JSON Schema. +https://schemas.u10d.dev/invocation-context/v1 + +Versioned request-scoped identity and correlation context carried in the reserved +invocation_context field of a plugin /invoke body +""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field + + +class InvocationContext(BaseModel): + """ + Versioned request-scoped identity and correlation context carried in the reserved + invocation_context field of a plugin /invoke body + """ + + model_config = ConfigDict(extra="allow") + + schema_version: Literal["1"] = Field( + default="1", + description="Payload contract version. A missing value defaults to v1; consumers reject an unknown value rather than silently dropping attribution. Additive fields do not change this value.", + ) + invocation_id: str | None = Field( + default=None, + description="Correlation identifier for this individual invocation.", + ) + job_id: str | None = Field(default=None, description="Job that owns the invocation.") + workflow_id: str | None = Field( + default=None, + description="Workflow that owns the job, when known.", + ) + attribution_id: str | None = Field( + default=None, + description="Platform attribution identifier associated with the invocation.", + ) + tenant_id: str | None = Field( + default=None, + description="Tenant identity to which the invocation is attributed.", + ) + org_id: str | None = Field( + default=None, + description="Organization identity to which the invocation is attributed.", + ) + dag_node_id: str | None = Field( + default=None, + description="Identifier of the DAG node being invoked.", + ) + dag_node_type: str | None = Field( + default=None, + description="Plugin type of the DAG node being invoked.", + ) + dag_node_subtype: str | None = Field( + default=None, + description="Plugin subtype of the DAG node being invoked.", + ) + record_id: str | None = Field( + default=None, + description="Record being processed by a single-record invocation.", + ) + attempt: int | None = Field( + default=None, + description="Attempt number associated with the claimed work.", + ) + job_created_timestamp: str | None = Field( + default=None, + description="Job creation timestamp forwarded for lifecycle timing. It is context metadata, not a telemetry dimension.", + ) + record_ids: list[str] | None = Field( + default=None, + description="Record identifiers in a batch invocation. When invocation_ids is also present, the two arrays are index-aligned.", + ) + invocation_ids: list[str | None] | None = Field( + default=None, + description="Per-record invocation identifiers for a batch invocation. Entry i identifies record_ids[i]; null means that record carried no invocation identifier.", + ) + +# Generation provenance used by drift tests and reviewers. +SCHEMA_ID = 'https://schemas.u10d.dev/invocation-context/v1.json' +SCHEMA_SHA256 = 'ca64be22a104acc697b7aff0fb47633ad75fa30cd88a0c92259af732fa1023c0' +RESERVED_CONTEXT_KEY = 'invocation_context' +SUPPORTED_CONTEXT_VERSIONS = frozenset(['1']) +DIMENSION_FIELDS = ('invocation_id', 'tenant_id', 'org_id', 'job_id', 'workflow_id', 'attribution_id', 'dag_node_id', 'dag_node_type', 'dag_node_subtype', 'record_id', 'attempt') diff --git a/unstructured_platform_plugins/invocation_context.py b/unstructured_platform_plugins/invocation_context.py new file mode 100644 index 0000000..54f406f --- /dev/null +++ b/unstructured_platform_plugins/invocation_context.py @@ -0,0 +1,125 @@ +"""The ``invocation_context`` companion to the settings envelope. + +Where ``invocation_settings`` carries *what* a plugin should be configured with, the context +carries *who* the invocation is for: the identity facets a shared-tenancy pod can no longer read +from its process environment. It travels in a second reserved, out-of-schema field of the +``/invoke`` body, extracted by the same route dependency that resolves the settings field. + +The context is `/invoke` protocol identity, not settings security: it touches no crypto and no +secrets, and it evolves with the plugin protocol this package defines. The errors it raises come +from the shared ``InvocationSettingsError`` taxonomy so hosts classify context failures with the +same ``reason``/``blame`` machinery as settings failures. + +The model below is the **consumer** view of that contract, deliberately lenient: unknown keys are +preserved for additive forward compatibility, and every identity field is optional so a +partially-populated context degrades to "less telemetry" rather than a failed invoke. The one thing +it is strict about is ``schema_version`` — that field makes incompatible producer and consumer +contracts detectable. The payload carries the version, so evolving the contract does not require +adding endpoints. +""" + +from __future__ import annotations + +from typing import Any, Mapping + +import pydantic +from utic_invocation_settings import Blame, InvocationSettingsError, MalformedEnvelopeError + +from unstructured_platform_plugins.generated.invocation_context_v1 import ( + DIMENSION_FIELDS, + RESERVED_CONTEXT_KEY, + SUPPORTED_CONTEXT_VERSIONS, +) +from unstructured_platform_plugins.generated.invocation_context_v1 import ( + InvocationContext as _GeneratedInvocationContext, +) + +# Sentinel distinguishing a truly-absent reserved key from one present with a ``None`` value. +_ABSENT = object() + + +class UnsupportedContextVersionError(InvocationSettingsError): + """The ``invocation_context`` declares a ``schema_version`` this package does not understand. + + This indicates deployment skew between platform components, not a fault in the request. + ``CONTENT`` (a 5xx) rather than ``CALLER``: contexts are produced by the platform's own claim + pipeline, and a 422 would make an upstream blame classifier pin a version-skew failure on the + customer. Failing the request prevents an unreadable context from silently removing telemetry + dimensions. + """ + + reason = "unsupported_context_version" + blame = Blame.CONTENT + + +class InvocationContext(_GeneratedInvocationContext): + """Request-scoped identity delivered alongside one claimed unit of work. + + The field shape, version literal, extra-field policy, reserved key, and dimensions are generated + from the ratified schema. This adapter retains the cross-field invariant JSON Schema cannot + express. + """ + + @pydantic.model_validator(mode="after") + def _batch_lists_stay_index_aligned(self) -> "InvocationContext": + if ( + self.record_ids is not None + and self.invocation_ids is not None + and len(self.record_ids) != len(self.invocation_ids) + ): + raise ValueError( + "record_ids and invocation_ids must be the same length: " + "entry i of invocation_ids describes record i" + ) + return self + +def extract_context(payload: Mapping[str, Any]) -> InvocationContext | None: + """Return the :class:`InvocationContext` from ``payload[RESERVED_CONTEXT_KEY]``. + + Returns ``None`` only when the producer omitted the reserved key. A present-but-invalid value + fails closed rather than degrading to "no context", because a context that silently vanishes + takes a pod's tenant attribution with it. + + A recognizable context carrying an unknown ``schema_version`` raises + :class:`UnsupportedContextVersionError` so an incompatible contract cannot be mistaken for + absent context. + """ + raw = payload.get(RESERVED_CONTEXT_KEY, _ABSENT) + if raw is _ABSENT: + return None + if isinstance(raw, InvocationContext): + return raw + try: + return InvocationContext.model_validate(raw) + except pydantic.ValidationError as exc: + errors = exc.errors() + # Only a well-typed version string this package does not know reads as deployment skew; a + # schema_version of the wrong type is the caller's malformed context like any other field. + reported = _reported_version(raw) + if isinstance(reported, str) and any( + error["loc"] == ("schema_version",) for error in errors + ): + raise UnsupportedContextVersionError( + f"unsupported invocation_context schema_version: " + f"{_reported_version(raw)!r}; expected one of {sorted(SUPPORTED_CONTEXT_VERSIONS)}" + ) from None + # `from None` so the pydantic error tree does not cross the domain-error boundary; a count + # plus the first message is enough signal. Mirrors the envelope extraction. + raise MalformedEnvelopeError( + f"invalid invocation_context: {exc.error_count()} validation error(s), " + f"first: {errors[0]['msg']}" + ) from None + + +def _reported_version(raw: Any) -> Any: + """The offending ``schema_version``, for the error message only. Never trusted.""" + return raw.get("schema_version") if isinstance(raw, Mapping) else None + + +def dimensions(context: InvocationContext | None) -> dict[str, Any]: + """The context's populated identity facets, ready to bind as telemetry dimensions.""" + if context is None: + return {} + return { + field: value for field in DIMENSION_FIELDS if (value := getattr(context, field)) is not None + } diff --git a/unstructured_platform_plugins/invocation_settings.py b/unstructured_platform_plugins/invocation_settings.py new file mode 100644 index 0000000..8606e8f --- /dev/null +++ b/unstructured_platform_plugins/invocation_settings.py @@ -0,0 +1,536 @@ +"""Transport for the reserved `/invoke` fields: request dependency, `/metadata`, optional body cap. + +The *settings contract* — including the only accepted sealed `/invoke` shape (the v2 settings +document), field-level resolution, and what an absent field is allowed to mean — lives in +`utic_invocation_settings`, next to the crypto it governs; every decision about a settings payload +is delegated there. The *identity contract* — +the `invocation_context` model — is +`/invoke` protocol rather than settings security and lives in this package's +`invocation_context` module. This module is the delivery mechanism for both: getting the payloads +off the wire and the results to the handler, and spelling the shared `blame` taxonomy as HTTP +statuses. + +The reserved fields are a first-class HTTP contract independent of the generated input schema. +They never appear in a plugin's declared signature, so `wrap_in_fastapi` keeps producing a handler +model built purely from the wrapped function, and a plugin reads the fields through +`current_invocation_settings()` / `current_invocation_context()` instead. + +Well-formed extraction runs in a route dependency (`bind_invocation_envelope`), so the `/invoke` +body is buffered and parsed exactly once: Starlette caches both the bytes and the parsed JSON on +the `Request`, and the dependency reads that cached parse. Generated routes can reject malformed +JSON while validating their typed body before dependencies run, so the installer normalizes that +specific validation failure to the same transport error. Hosts that have established a safe +request ceiling may opt into `InvokeBodyLimitMiddleware`, which counts bytes as they stream through +without buffering. Wrapped plugins do not acquire a fleet-wide limit merely by upgrading this +package. +""" + +from __future__ import annotations + +import asyncio +import hashlib +import inspect +import json +import logging +import threading +import time +from collections import OrderedDict +from collections.abc import AsyncIterator, Callable, Iterator, Mapping +from contextlib import contextmanager +from contextvars import ContextVar +from typing import Any, Optional, TypeVar + +from fastapi import Depends, FastAPI, Request +from fastapi.exception_handlers import request_validation_exception_handler +from fastapi.exceptions import RequestValidationError +from opentelemetry import trace +from starlette.requests import ClientDisconnect +from starlette.responses import JSONResponse +from starlette.routing import get_route_path +from starlette.types import ASGIApp, Receive, Scope, Send +from utic_invocation_settings import ( + INVOKE_WITH_SEALED_DAG_NODE_SETTINGS_V2_CAPABILITY, + RESERVED_ENVELOPE_KEY, + Blame, + InvocationSettingsError, + MalformedEnvelopeError, + resolve_invocation_settings, +) + +from unstructured_platform_plugins.invocation_context import ( + RESERVED_CONTEXT_KEY, + InvocationContext, + dimensions, + extract_context, +) + +logger = logging.getLogger(__name__) + + +def http_status_for(error: BaseException) -> int: + """The HTTP status this transport answers for a failed resolution, from ``blame``. + + One rule, the one the library README states normatively: ``Blame.CALLER`` -> 422, everything + else -> 500. The line it draws is whether a different request would work. Sealing drift, an + envelope for another recipient and a broken local mount are all 5xx, which keeps a controller's + blame classification off the customer, whose request was fine. Anything that is not a + classified error is a 500: an unclassified failure is not the caller's. + """ + return 422 if getattr(error, "blame", None) is Blame.CALLER else 500 + + +T = TypeVar("T") + +_METADATA_PATH = "/metadata" +_INVOKE_PATH = "/invoke" + +# A convenient opt-in ceiling for hosts that have verified it against their request distribution. +# It is deliberately not the install default: batch invokes carry arrays of file_data payloads, +# and a wrapper upgrade must not reject previously valid fleet traffic without an explicit choice. +MAX_INVOKE_BODY_BYTES = 64 * 1024 * 1024 + +_INVOCATION: ContextVar[tuple[Optional[dict], Optional[InvocationContext]]] = ContextVar( + "invocation", default=(None, None) +) + + +def current_invocation_settings() -> Optional[dict]: + """Reserved `invocation_settings` field bound for the current request, if any. + + `None` means the field was genuinely absent — the only case in which a plugin may fall back to + its boot-time settings. A field that arrived and could not be opened never reaches a handler: + the binding dependency fails the request first. + """ + return _INVOCATION.get()[0] + + +def current_invocation_context() -> Optional[InvocationContext]: + """Reserved `invocation_context` field bound for the current request, if any.""" + return _INVOCATION.get()[1] + + +@contextmanager +def invocation_envelope( + invocation_settings: Optional[dict], invocation_context: Optional[InvocationContext] +) -> Iterator[None]: + """Bind the reserved /invoke fields for the current context.""" + token = _INVOCATION.set((invocation_settings, invocation_context)) + try: + yield + finally: + _INVOCATION.reset(token) + + +def add_metadata_route( + app: FastAPI, + identifier: Optional[str] = None, + invoke_with_sealed_dag_node_settings_v2: bool = False, +) -> None: + """Register GET /metadata advertising the reserved /invoke fields this plugin accepts. + + `/metadata` is the plugin API spec's own discovery surface (`PluginMetadataOutput`): capability + flags are strings in its `capabilities` list, which is where the controller looks before + forwarding the reserved fields — no controller-private probe route. + + `invocation_settings` and `invocation_context` are transport capabilities: installing the + dependency makes the host receive, resolve, and bind those fields. The sealed-settings + capability is stronger: it tells the controller that the plugin handler consumes the resolved + field-level v2 settings in place of boot-time state. The controller may therefore send the + versioned v2 document, so this remains an explicit opt-in. + + Last call wins: the payload lives on `app.state` and every call overwrites it, while the route + is registered once. A host wrapper may register at app construction and a plugin can still + re-register with its own identifier afterwards, with no route-order dependence. + """ + capabilities = [RESERVED_ENVELOPE_KEY, RESERVED_CONTEXT_KEY] + if invoke_with_sealed_dag_node_settings_v2: + capabilities.append(INVOKE_WITH_SEALED_DAG_NODE_SETTINGS_V2_CAPABILITY) + app.state.plugin_metadata_payload = { + "api_version": "3", + "identifier": identifier, + "capabilities": capabilities, + } + if getattr(app.state, "plugin_metadata_route_installed", False): + return + app.state.plugin_metadata_route_installed = True + + # A /metadata route registered by the application itself would win by route order and pin its + # own stale payload; drop it so the last add_metadata_route call is the one that answers. + app.router.routes = [r for r in app.router.routes if getattr(r, "path", None) != _METADATA_PATH] + + @app.get(_METADATA_PATH) + async def plugin_metadata() -> dict: + return app.state.plugin_metadata_payload + + +class UnusableInvocationEnvelope(Exception): + """A reserved /invoke field arrived but cannot be used. + + Raised by `bind_invocation_envelope` and answered by the handler + `install_invocation_envelope` registers, so the response shape — `detail` plus the library's + stable `reason` code as top-level siblings — stays what orchestrators parse, independent of + FastAPI's own error envelope. + """ + + def __init__(self, status_code: int, payload: dict): + super().__init__(payload.get("detail")) + self.status_code = status_code + self.payload = payload + + +async def _unusable_envelope_response( + _request: Request, exc: UnusableInvocationEnvelope +) -> JSONResponse: + return JSONResponse(status_code=exc.status_code, content=exc.payload) + + +def _is_malformed_json_validation(exc: RequestValidationError) -> bool: + """Whether FastAPI rejected the request body before route dependencies could run.""" + # A top-level JSONDecodeError stores the raw document and reports its integer parser offset as + # exactly ("body", offset). Pydantic's Json fields also emit `json_invalid`, but by then the + # outer request has been decoded into a mapping/list and the location continues through the + # model field. Those are ordinary schema-validation errors and must retain FastAPI's detail. + if not isinstance(exc.body, (str, bytes, bytearray)): + return False + return any( + error.get("type") == "json_invalid" + and len(location := tuple(error.get("loc", ()))) == 2 + and location[0] == "body" + and isinstance(location[1], int) + for error in exc.errors() + ) + + +async def bind_invocation_envelope(request: Request) -> AsyncIterator[None]: + """Resolve the reserved /invoke fields and bind them for the duration of the request. + + Runs as a route dependency, after the framework has read the body: `request.json()` is + Starlette-cached, so the parse is shared with the framework's own body handling. + A reserved field that is present but unusable fails the request rather than being + treated as absent, because absence is the signal to fall back to the boot-time settings file: + degrading a malformed field to absence would quietly answer a request configured for one + tenant with whatever the pod happened to boot with. Under + `FF_INVOCATION_SETTINGS` there is no settings file to fall back to, + so absent or plaintext settings fail too — as does any /invoke whose body is not a JSON + object, since such a body cannot carry the envelope a native pod requires. + """ + # ASGI `path` includes any deployment root_path; get_route_path strips it, which is how the + # router itself matches, so binding fires exactly when the /invoke route does. + if request.method != "POST" or get_route_path(request.scope) != _INVOKE_PATH: + yield + return + + try: + parsed = await request.json() + except ValueError: + # request.json() reads through Starlette's cached body, so this does not consume or buffer + # the stream a second time. A truly empty body is absence and remains subject to the + # native-pod policy below. Any bytes that fail JSON parsing are a malformed request, not + # absence: collapsing them would turn a caller-fixable syntax error into a native pod's + # SealedDagNodeSettingsRequiredError (RECIPIENT -> 500). + if await request.body(): + raise UnusableInvocationEnvelope( + 422, + { + "detail": "Invalid JSON body", + "reason": MalformedEnvelopeError.reason, + }, + ) from None + parsed = None + + raw_settings: Optional[Any] = None + if isinstance(parsed, dict): + raw_settings = parsed.get(RESERVED_ENVELOPE_KEY) + if RESERVED_ENVELOPE_KEY in parsed and not isinstance(raw_settings, dict): + raise UnusableInvocationEnvelope( + 422, + { + "detail": f"Invalid field: {RESERVED_ENVELOPE_KEY}", + "reason": MalformedEnvelopeError.reason, + }, + ) + try: + # Off the event loop: resolution may perform blocking cryptography for independently + # sealed fields, and this dependency fronts every invoke on the pod. + invocation_settings = await asyncio.to_thread(resolve_invocation_settings, raw_settings) + except Exception as exc: + # Class name only — never envelope contents, and never the exception's own message, + # which can embed request-controlled values. + logger.warning("unusable %s payload: %s", RESERVED_ENVELOPE_KEY, type(exc).__name__) + payload = {"detail": f"Unusable invocation settings: {type(exc).__name__}"} + reason = getattr(exc, "reason", None) + if isinstance(reason, str): + payload["reason"] = reason + raise UnusableInvocationEnvelope(http_status_for(exc), payload) from exc + + invocation_context: Optional[InvocationContext] = None + if isinstance(parsed, dict): + try: + invocation_context = extract_context(parsed) + except InvocationSettingsError as exc: + # A context this plugin cannot read fails loudly here rather than running with + # silently absent identity. Status comes from the blame taxonomy: a malformed + # field is the caller's 422, but an unreadable schema_version is deployment skew + # between platform components and must not read as a caller fault. The log line is + # truncated because the message can embed request-controlled values. + logger.warning("rejecting invalid %s: %.200s", RESERVED_CONTEXT_KEY, exc) + status = http_status_for(exc) + detail = ( + f"Invalid field: {RESERVED_CONTEXT_KEY}" + if status == 422 + else f"Unusable {RESERVED_CONTEXT_KEY}: {type(exc).__name__}" + ) + raise UnusableInvocationEnvelope( + status, {"detail": detail, "reason": exc.reason} + ) from exc + + # Keep plugin-side request spans aligned with the controller's wide-event dimensions. The + # ratified DIMENSION_FIELDS list deliberately excludes batch correlation and unknown additive + # fields, so only the shared invocation identity is promoted to indexed telemetry. + invocation_dimensions = dimensions(invocation_context) + if invocation_dimensions: + span = trace.get_current_span() + for key, value in invocation_dimensions.items(): + span.set_attribute(key, value) + + with invocation_envelope(invocation_settings, invocation_context): + yield + + +async def _send_json(send: Send, status_code: int, payload: dict) -> None: + body = json.dumps(payload).encode() + await send( + { + "type": "http.response.start", + "status": status_code, + "headers": [ + (b"content-type", b"application/json"), + (b"content-length", str(len(body)).encode()), + ], + } + ) + await send({"type": "http.response.body", "body": body}) + + +class InvokeBodyLimitMiddleware: + """Reject a POST /invoke body over ``max_body_bytes`` with 413. + + Counts bytes as the framework consumes them; nothing is buffered here. When the count crosses + the cap the downstream read is answered with ``http.disconnect``, which aborts the framework's + body read before another byte is held, and the 413 is sent once the application has unwound. + This has to sit below the framework because neither Starlette nor uvicorn bounds request-body + size. + """ + + def __init__(self, app: ASGIApp, max_body_bytes: int = MAX_INVOKE_BODY_BYTES): + self.app = app + self.max_body_bytes = max_body_bytes + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if ( + scope["type"] != "http" + or scope.get("method") != "POST" + or get_route_path(scope) != _INVOKE_PATH + ): + await self.app(scope, receive, send) + return + + seen = 0 + exceeded = False + response_started = False + + async def counting_receive() -> dict: + nonlocal seen, exceeded + message = await receive() + if message["type"] == "http.request": + seen += len(message.get("body", b"")) + if seen > self.max_body_bytes: + exceeded = True + return {"type": "http.disconnect"} + return message + + async def guarded_send(message: dict) -> None: + nonlocal response_started + if exceeded and not response_started: + # A response computed after the body was cut is answering a truncated request; + # drop it so the 413 below is what the caller sees. A response that started + # before the cap tripped keeps streaming — its start is already on the wire. + return + if message["type"] == "http.response.start": + response_started = True + await send(message) + + try: + await self.app(scope, counting_receive, guarded_send) + except ClientDisconnect: + if not exceeded: + raise + except Exception as exc: + # The cut body stream can surface downstream as something other than + # ClientDisconnect; once the cap is the cause, the 413 below is the answer. Retain a + # type-only diagnostic for a coincident downstream bug without rendering its message, + # which may contain request data or credentials. + if not exceeded: + raise + logger.debug( + "downstream raised after /invoke body limit was exceeded: %s", + type(exc).__name__, + ) + if exceeded and not response_started: + await _send_json(send, 413, {"detail": "Request body too large"}) + + +def install_invocation_envelope(app: FastAPI, max_body_bytes: int | None = None) -> None: + """Install reserved-field binding before registering a FastAPI app's routes. + + Adds `bind_invocation_envelope` through the router's public dependency list and registers the + failure response shape. FastAPI parses a generated route's typed body before dependencies run, + so malformed JSON validation is normalized to that same response shape at the app boundary; + every other validation error retains the handler the app already had. When ``max_body_bytes`` + is supplied, also installs the body-size cap beneath the framework. The cap is opt-in because + a wrapper upgrade must not impose an unvalidated fleet-wide request limit. The dependency is a + path/method-aware no-op outside POST /invoke, so routes registered after this call can all + inherit it without private dependency-graph mutation. Calling after routes have already been + registered raises rather than silently leaving those routes uncovered. + + Idempotent per app because both host-wrapper and plugin setup may call this function, while a + double installation would resolve settings twice per request. A repeated call asking for a + different ``max_body_bytes`` raises because the installed configuration cannot be changed and + silently keeping the first value would misrepresent the limit actually enforced. + """ + if getattr(app.state, "invocation_envelope_installed", False): + installed_max = app.state.invocation_envelope_max_body_bytes + if max_body_bytes != installed_max: + raise ValueError( + "install_invocation_envelope already installed with " + f"max_body_bytes={installed_max}; cannot reinstall with {max_body_bytes}" + ) + return + if any( + getattr(route, "path", None) == _INVOKE_PATH + and "POST" in (getattr(route, "methods", None) or set()) + for route in app.router.routes + ): + raise RuntimeError( + "install_invocation_envelope must be called before the POST /invoke route is registered" + ) + app.state.invocation_envelope_installed = True + app.state.invocation_envelope_max_body_bytes = max_body_bytes + app.router.dependencies.append(Depends(bind_invocation_envelope)) + if max_body_bytes is not None: + app.add_middleware(InvokeBodyLimitMiddleware, max_body_bytes=max_body_bytes) + app.add_exception_handler(UnusableInvocationEnvelope, _unusable_envelope_response) + + previous_validation_handler = app.exception_handlers.get( + RequestValidationError, request_validation_exception_handler + ) + + async def invocation_request_validation_response(request: Request, exc: RequestValidationError): + if ( + request.method == "POST" + and get_route_path(request.scope) == _INVOKE_PATH + and _is_malformed_json_validation(exc) + ): + return JSONResponse( + status_code=422, + content={ + "detail": "Invalid JSON body", + "reason": MalformedEnvelopeError.reason, + }, + ) + response = previous_validation_handler(request, exc) + return await response if inspect.isawaitable(response) else response + + app.add_exception_handler(RequestValidationError, invocation_request_validation_response) + + +def settings_cache_key(invocation_settings: Mapping[str, Any]) -> str: + """Digest of an ordinary resolved settings mapping, safe for secret-bearing values.""" + return hashlib.sha256(json.dumps(invocation_settings, sort_keys=True).encode()).hexdigest() + + +class SettingsScopedCache: + """Bind expensive derived state (clients, models, handlers) to the settings that built it. + + A plugin consuming ``current_invocation_settings()`` derives a handler from each distinct + resolved mapping. Construction typically performs network work (model resolution, prechecks), + so results are memoized by ``settings_cache_key``. Raw field envelopes never reach this cache: + the public settings library resolves and caches them at the field boundary before this + transport binds the result. Both bounds matter under shared tenancy: size caps how many + distinct mappings stay live, and age evicts state after credential rotation. Eviction driven + only by the count of distinct mappings can take arbitrarily long on a quiet pod. + + Thread-safe for lookups and inserts. Concurrent misses for the same settings may build twice; + the extra build is wasted work, never wrong state. + """ + + def __init__( + self, + *, + ttl_seconds: float = 15 * 60, + maxsize: int = 32, + clock: Callable[[], float] = time.monotonic, + ) -> None: + if ttl_seconds <= 0: + raise ValueError("ttl_seconds must be positive") + if maxsize < 1: + raise ValueError("maxsize must be at least 1") + self._ttl_seconds = float(ttl_seconds) + self._maxsize = maxsize + self._clock = clock + self._lock = threading.Lock() + self._entries: OrderedDict[str, tuple[float, Any]] = OrderedDict() + + def get_or_build(self, invocation_settings: Mapping[str, Any], build: Callable[[], T]) -> T: + """Return the cached value for these settings, building it on a miss.""" + key = settings_cache_key(invocation_settings) + now = self._clock() + with self._lock: + entry = self._entries.get(key) + if entry is not None: + expires_at, value = entry + if now < expires_at: + self._entries.move_to_end(key) + return value + del self._entries[key] + value = build() + with self._lock: + # Every insert also sweeps entries whose TTL has lapsed, so a tenant that stops + # sending requests does not keep its credential-bearing handler live while the pod + # stays busy for others; per-key expiry alone only fires on that tenant's next hit. + # Re-read the clock: build() may take long enough for more entries to lapse. + now = self._clock() + for stale_key, (expires_at, _) in list(self._entries.items()): + if now >= expires_at: + del self._entries[stale_key] + self._entries[key] = (now + self._ttl_seconds, value) + self._entries.move_to_end(key) + while len(self._entries) > self._maxsize: + self._entries.popitem(last=False) + return value + + def handler_for( + self, + resolved_settings: Optional[Mapping[str, Any]], + *, + boot: Callable[[], Optional[T]], + build: Callable[[], T], + ) -> T: + """The handler for a request: cached per distinct resolved mapping, or the boot fallback. + + Absent settings select the single handler configured by the boot-time settings file. + ``boot`` returning ``None`` means the pod has no boot configuration and sealed per-invoke + settings are required, so the request fails rather than running an unconfigured handler. + """ + if resolved_settings is None: + handler = boot() + if handler is None: + raise ValueError( + "no boot-time handler on this pod: sealed per-invoke settings are required" + ) + return handler + return self.get_or_build(resolved_settings, build) + + def clear(self) -> None: + with self._lock: + self._entries.clear() diff --git a/uv.lock b/uv.lock index 18c13ae..d76420e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 -revision = 2 -requires-python = ">=3.10" +revision = 3 +requires-python = ">=3.11" resolution-markers = [ "python_full_version >= '3.13'", "python_full_version < '3.13'", @@ -20,7 +20,6 @@ name = "anyio" version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, @@ -34,9 +33,6 @@ wheels = [ name = "asgiref" version = "3.9.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] sdist = { url = "https://files.pythonhosted.org/packages/7f/bf/0f3ecda32f1cb3bf1dca480aca08a7a8a3bdc4bed2343a103f30731565c9/asgiref-3.9.2.tar.gz", hash = "sha256:a0249afacb66688ef258ffe503528360443e2b9a8d8c4581b6ebefa58c841ef1", size = 36894, upload-time = "2025-09-23T15:00:55.136Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d1/69d02ce34caddb0a7ae088b84c356a625a93cd4ff57b2f97644c03fad905/asgiref-3.9.2-py3-none-any.whl", hash = "sha256:0b61526596219d70396548fc003635056856dba5d0d086f86476f10b33c75960", size = 23788, upload-time = "2025-09-23T15:00:53.627Z" }, @@ -57,10 +53,8 @@ version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "os_name == 'nt'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" } wheels = [ @@ -85,14 +79,8 @@ dependencies = [ ] 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/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, @@ -101,6 +89,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, @@ -108,6 +101,11 @@ wheels = [ { 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" }, @@ -115,18 +113,31 @@ wheels = [ { 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]] @@ -135,17 +146,6 @@ version = "3.4.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, - { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, - { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, - { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, - { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, - { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, - { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, - { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, @@ -220,18 +220,6 @@ version = "7.10.7" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" }, - { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" }, - { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" }, { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" }, { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" }, { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" }, @@ -324,10 +312,10 @@ version = "46.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, @@ -339,6 +327,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fb/c73588561afcd5e24b089952bd210b14676c0c5bf1213376350ae111945c/cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee", size = 7193928, upload-time = "2025-09-17T00:09:10.595Z" }, { url = "https://files.pythonhosted.org/packages/26/34/0ff0bb2d2c79f25a2a63109f3b76b9108a906dd2a2eb5c1d460b9938adbb/cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd", size = 4293515, upload-time = "2025-09-17T00:09:12.861Z" }, { url = "https://files.pythonhosted.org/packages/df/b7/d4f848aee24ecd1be01db6c42c4a270069a4f02a105d9c57e143daf6cf0f/cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a", size = 4545619, upload-time = "2025-09-17T00:09:15.397Z" }, { url = "https://files.pythonhosted.org/packages/44/a5/42fedefc754fd1901e2d95a69815ea4ec8a9eed31f4c4361fcab80288661/cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a", size = 4299160, upload-time = "2025-09-17T00:09:17.155Z" }, @@ -350,6 +342,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/3b/d8fb17ffeb3a83157a1cc0aa5c60691d062aceecba09c2e5e77ebfc1870c/cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657", size = 4576958, upload-time = "2025-09-17T00:09:29.924Z" }, { url = "https://files.pythonhosted.org/packages/d9/46/86bc3a05c10c8aa88c8ae7e953a8b4e407c57823ed201dbcba55c4d655f4/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0", size = 4422507, upload-time = "2025-09-17T00:09:32.222Z" }, { url = "https://files.pythonhosted.org/packages/a8/4e/387e5a21dfd2b4198e74968a541cfd6128f66f8ec94ed971776e15091ac3/cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0", size = 4683964, upload-time = "2025-09-17T00:09:34.118Z" }, + { url = "https://files.pythonhosted.org/packages/25/a3/f9f5907b166adb8f26762071474b38bbfcf89858a5282f032899075a38a1/cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277", size = 3029705, upload-time = "2025-09-17T00:09:36.381Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/4d3a4f1850db2e71c2b1628d14b70b5e4c1684a1bd462f7fffb93c041c38/cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05", size = 3502175, upload-time = "2025-09-17T00:09:38.261Z" }, + { url = "https://files.pythonhosted.org/packages/52/c7/9f10ad91435ef7d0d99a0b93c4360bea3df18050ff5b9038c489c31ac2f5/cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784", size = 2912354, upload-time = "2025-09-17T00:09:40.078Z" }, + { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, @@ -361,10 +357,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, + { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/27/27/077e09fd92075dd1338ea0ffaf5cfee641535545925768350ad90d8c36ca/cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70", size = 3722319, upload-time = "2025-09-17T00:10:20.273Z" }, { url = "https://files.pythonhosted.org/packages/db/32/6fc7250280920418651640d76cee34d91c1e0601d73acd44364570cf041f/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f", size = 4249030, upload-time = "2025-09-17T00:10:22.396Z" }, { url = "https://files.pythonhosted.org/packages/32/33/8d5398b2da15a15110b2478480ab512609f95b45ead3a105c9a9c76f9980/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc", size = 4528009, upload-time = "2025-09-17T00:10:24.418Z" }, { url = "https://files.pythonhosted.org/packages/fd/1c/4012edad2a8977ab386c36b6e21f5065974d37afa3eade83a9968cba4855/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d", size = 4248902, upload-time = "2025-09-17T00:10:26.255Z" }, { url = "https://files.pythonhosted.org/packages/58/a3/257cd5ae677302de8fa066fca9de37128f6729d1e63c04dd6a15555dd450/cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46", size = 4527150, upload-time = "2025-09-17T00:10:28.28Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cd/fe6b65e1117ec7631f6be8951d3db076bac3e1b096e3e12710ed071ffc3c/cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a", size = 3448210, upload-time = "2025-09-17T00:10:30.145Z" }, ] [[package]] @@ -389,18 +390,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/dd/f95350e853a4468ec37478414fc04ae2d61dad7a947b3015c3dcc51a09b9/docutils-0.22.2-py3-none-any.whl", hash = "sha256:b0e98d679283fc3bb0ead8a5da7f501baa632654e7056e9c5846842213d674d8", size = 632667, upload-time = "2025-09-20T17:55:43.052Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - [[package]] name = "fastapi" version = "0.117.1" @@ -436,16 +425,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/9d/f7/8963848164c7604efb3a3e6ee457fdb3a469653e19002bd24742473254f8/grpcio-1.75.1.tar.gz", hash = "sha256:3e81d89ece99b9ace23a6916880baca613c03a799925afb2857887efa8b1b3d2", size = 12731327, upload-time = "2025-09-26T09:03:36.887Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/57/89fd829fb00a6d0bee3fbcb2c8a7aa0252d908949b6ab58bfae99d39d77e/grpcio-1.75.1-cp310-cp310-linux_armv7l.whl", hash = "sha256:1712b5890b22547dd29f3215c5788d8fc759ce6dd0b85a6ba6e2731f2d04c088", size = 5705534, upload-time = "2025-09-26T09:00:52.225Z" }, - { url = "https://files.pythonhosted.org/packages/76/dd/2f8536e092551cf804e96bcda79ecfbc51560b214a0f5b7ebc253f0d4664/grpcio-1.75.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8d04e101bba4b55cea9954e4aa71c24153ba6182481b487ff376da28d4ba46cf", size = 11484103, upload-time = "2025-09-26T09:00:59.457Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3d/affe2fb897804c98d56361138e73786af8f4dd876b9d9851cfe6342b53c8/grpcio-1.75.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:683cfc70be0c1383449097cba637317e4737a357cfc185d887fd984206380403", size = 6289953, upload-time = "2025-09-26T09:01:03.699Z" }, - { url = "https://files.pythonhosted.org/packages/87/aa/0f40b7f47a0ff10d7e482bc3af22dac767c7ff27205915f08962d5ca87a2/grpcio-1.75.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:491444c081a54dcd5e6ada57314321ae526377f498d4aa09d975c3241c5b9e1c", size = 6949785, upload-time = "2025-09-26T09:01:07.504Z" }, - { url = "https://files.pythonhosted.org/packages/a5/45/b04407e44050781821c84f26df71b3f7bc469923f92f9f8bc27f1406dbcc/grpcio-1.75.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce08d4e112d0d38487c2b631ec8723deac9bc404e9c7b1011426af50a79999e4", size = 6465708, upload-time = "2025-09-26T09:01:11.028Z" }, - { url = "https://files.pythonhosted.org/packages/09/3e/4ae3ec0a4d20dcaafbb6e597defcde06399ccdc5b342f607323f3b47f0a3/grpcio-1.75.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5a2acda37fc926ccc4547977ac3e56b1df48fe200de968e8c8421f6e3093df6c", size = 7100912, upload-time = "2025-09-26T09:01:14.393Z" }, - { url = "https://files.pythonhosted.org/packages/34/3f/a9085dab5c313bb0cb853f222d095e2477b9b8490a03634cdd8d19daa5c3/grpcio-1.75.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:745c5fe6bf05df6a04bf2d11552c7d867a2690759e7ab6b05c318a772739bd75", size = 8042497, upload-time = "2025-09-26T09:01:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/ea54eba931ab9ed3f999ba95f5d8d01a20221b664725bab2fe93e3dee848/grpcio-1.75.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:259526a7159d39e2db40d566fe3e8f8e034d0fb2db5bf9c00e09aace655a4c2b", size = 7493284, upload-time = "2025-09-26T09:01:20.896Z" }, - { url = "https://files.pythonhosted.org/packages/b7/5e/287f1bf1a998f4ac46ef45d518de3b5da08b4e86c7cb5e1108cee30b0282/grpcio-1.75.1-cp310-cp310-win32.whl", hash = "sha256:f4b29b9aabe33fed5df0a85e5f13b09ff25e2c05bd5946d25270a8bd5682dac9", size = 3950809, upload-time = "2025-09-26T09:01:23.695Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/3cbfc06a4ec160dc77403b29ecb5cf76ae329eb63204fea6a7c715f1dfdb/grpcio-1.75.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf2e760978dcce7ff7d465cbc7e276c3157eedc4c27aa6de7b594c7a295d3d61", size = 4644704, upload-time = "2025-09-26T09:01:25.763Z" }, { url = "https://files.pythonhosted.org/packages/0c/3c/35ca9747473a306bfad0cee04504953f7098527cd112a4ab55c55af9e7bd/grpcio-1.75.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:573855ca2e58e35032aff30bfbd1ee103fbcf4472e4b28d4010757700918e326", size = 5709761, upload-time = "2025-09-26T09:01:28.528Z" }, { url = "https://files.pythonhosted.org/packages/c9/2c/ecbcb4241e4edbe85ac2663f885726fea0e947767401288b50d8fdcb9200/grpcio-1.75.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:6a4996a2c8accc37976dc142d5991adf60733e223e5c9a2219e157dc6a8fd3a2", size = 11496691, upload-time = "2025-09-26T09:01:31.214Z" }, { url = "https://files.pythonhosted.org/packages/81/40/bc07aee2911f0d426fa53fe636216100c31a8ea65a400894f280274cb023/grpcio-1.75.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b1ea1bbe77ecbc1be00af2769f4ae4a88ce93be57a4f3eebd91087898ed749f9", size = 6296084, upload-time = "2025-09-26T09:01:34.596Z" }, @@ -895,7 +874,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.9" +version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -903,96 +882,111 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, - { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, - { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, - { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, - { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, - { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, - { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, - { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, - { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, - { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, - { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, - { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, - { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, - { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, - { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, - { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, - { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, - { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, - { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, - { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, - { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, - { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, - { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, ] [[package]] @@ -1019,12 +1013,10 @@ version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1294,14 +1286,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] @@ -1332,6 +1324,7 @@ dependencies = [ { name = "opentelemetry-instrumentation-fastapi" }, { name = "requests" }, { name = "unstructured-ingest" }, + { name = "utic-invocation-settings" }, { name = "uvicorn" }, ] @@ -1359,6 +1352,7 @@ requires-dist = [ { name = "opentelemetry-instrumentation-fastapi" }, { name = "requests" }, { name = "unstructured-ingest" }, + { name = "utic-invocation-settings", specifier = ">=0.5.0,<1.0.0" }, { name = "uvicorn" }, ] @@ -1384,6 +1378,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] +[[package]] +name = "utic-invocation-settings" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pydantic" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/df/8443bd23f6c0cbf0f179a45dd0e318b6aabd42e231ac6cf727a5f0206915/utic_invocation_settings-0.5.0-py3-none-any.whl", hash = "sha256:75788c7f1cea5b033bdb1a40e19f35c6a3326ce9b016d12039353a78765ecb7e", size = 78072, upload-time = "2026-09-01T16:31:34.114Z" }, +] + [[package]] name = "uvicorn" version = "0.37.0" @@ -1391,7 +1397,6 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } wheels = [ @@ -1413,16 +1418,6 @@ 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, upload-time = "2025-08-12T05:53:21.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" },