From 809ae33edc2a1c750d2d9ad7a69d80102bea1d64 Mon Sep 17 00:00:00 2001 From: Eloi Date: Fri, 10 Jul 2026 12:45:52 +0200 Subject: [PATCH] feat: enforce strict structured output primitives --- CHANGELOG.md | 12 ++++ docs/api-stability.md | 7 +- docs/quickstart.md | 20 +++--- pyproject.toml | 4 +- src/agent_runtime_kit/__init__.py | 2 + src/agent_runtime_kit/_errors.py | 4 ++ src/agent_runtime_kit/_kit.py | 4 +- src/agent_runtime_kit/_runtime.py | 22 +++++- src/agent_runtime_kit/_schema.py | 116 ++++++++++++++++++++++++++++-- src/agent_runtime_kit/_types.py | 13 +++- tests/test_core.py | 36 ++++++++++ tests/test_kit.py | 46 ++++++++++++ tests/test_schema.py | 79 +++++++++++++++++++- uv.lock | 22 +++++- 14 files changed, 365 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0861ae1..1302dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Core JSON Schema validation via `jsonschema`, including construction-time + schema checks and `OutputSchemaError` for malformed definitions. +- `AgentResult.parsed_output_available` distinguishes a valid JSON `null` from + absent or rejected structured output. + +### Changed + +- Pydantic structured-output parsing now requests strict validation, so values + such as `"42"` no longer coerce into integer fields. + ### Fixed - `AgentKit.run(task=...)` now rejects every explicitly supplied per-field task diff --git a/docs/api-stability.md b/docs/api-stability.md index 7a7d604..2433e1c 100644 --- a/docs/api-stability.md +++ b/docs/api-stability.md @@ -44,7 +44,12 @@ import the names from the top-level package instead. `KIND_ALIASES` mapping; exact kind strings always work. `output_type=` supports a bounded, documented subset of the typing system and raises `OutputTypeError` on anything outside it (Pydantic-style models are used - through their own `model_json_schema`/`model_validate` when present). + through their own `model_json_schema`/`model_validate(strict=True)` when + present). Raw schemas are checked at construction. +- **Structured-output presence is explicit.** `parsed_output_available` is true + for a validated payload, including JSON `null`. Existing non-null custom + runtime results opt in automatically; a custom runtime returning valid null + must set the flag explicitly. ## Vendor SDK version policy diff --git a/docs/quickstart.md b/docs/quickstart.md index 5ca8f24..f6a22fb 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -51,9 +51,9 @@ asyncio.run(main()) ## Typed structured output -Pass a type instead of hand-writing JSON schema. Dataclasses and `TypedDict`s -work out of the box (no dependency); Pydantic v2 models are used through their -own `model_json_schema`/`model_validate` when you have Pydantic installed. +Pass a type instead of hand-writing JSON Schema. Dataclasses and `TypedDict`s +work without Pydantic; Pydantic v2 models are used through their own +`model_json_schema`/`model_validate` methods and validated with `strict=True`. ```python from dataclasses import dataclass @@ -75,17 +75,19 @@ async def main() -> None: goal="Summarize this repository", output_type=RepoSummary, ) - if result.parsed is not None: + if result.parsed_output_available: print(result.parsed.languages) # typed: list[str] else: print(result.error) ``` -A payload that does not conform yields `finish_reason="failed"` with the -mismatch in `result.error` — the same convention the adapters use for -unsatisfied structured output. Unsupported annotations (sets, plain unions, -...) raise `OutputTypeError` at call time rather than sending a half-true -schema. +A malformed raw `output_schema` raises `OutputSchemaError` before execution. +`AgentKit` strictly validates values requested with `output_type`; a mismatch +yields `finish_reason="failed"` with the details in `result.error`. +Unsupported Python annotations (sets, plain unions, ...) raise +`OutputTypeError` at call time. For nullable schemas, +`parsed_output_available=True` with `parsed_output=None` represents valid JSON +`null`. ## Events and custom runtimes diff --git a/pyproject.toml b/pyproject.toml index 9d36756..c8d0140 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Typing :: Typed", ] -dependencies = [] +dependencies = ["jsonschema>=4.18,<5"] [project.optional-dependencies] # Cautious upper bounds on the pre-1.0 vendor SDKs: they move fast and have shipped @@ -65,10 +65,12 @@ Issues = "https://github.com/ebarti/agent-runtime-kit/issues" dev = [ "build>=1.2", "mypy>=1.8", + "pydantic>=2,<3", "pytest>=8.0", "pytest-asyncio>=0.23", "pytest-cov>=5.0", "ruff>=0.8", + "types-jsonschema>=4.18", ] [tool.uv] diff --git a/src/agent_runtime_kit/__init__.py b/src/agent_runtime_kit/__init__.py index 3a98d79..8409473 100644 --- a/src/agent_runtime_kit/__init__.py +++ b/src/agent_runtime_kit/__init__.py @@ -3,6 +3,7 @@ from agent_runtime_kit._errors import ( AgentRuntimeError, AgentRuntimeUnavailableError, + OutputSchemaError, OutputTypeError, RuntimeNotRegisteredError, UnsupportedTaskInputError, @@ -59,6 +60,7 @@ "FinishReason", "KIND_ALIASES", "McpServerConfig", + "OutputSchemaError", "OutputTypeError", "ParsedResult", "PermissionMode", diff --git a/src/agent_runtime_kit/_errors.py b/src/agent_runtime_kit/_errors.py index cd2b1a3..509e51a 100644 --- a/src/agent_runtime_kit/_errors.py +++ b/src/agent_runtime_kit/_errors.py @@ -38,6 +38,10 @@ class OutputTypeError(AgentRuntimeError, TypeError): """ +class OutputSchemaError(AgentRuntimeError, ValueError): + """An output_schema is not a valid JSON Schema.""" + + class RuntimeNotRegisteredError(AgentRuntimeError, LookupError): """No runtime factory is registered for the requested runtime kind.""" diff --git a/src/agent_runtime_kit/_kit.py b/src/agent_runtime_kit/_kit.py index a26782d..be3de91 100644 --- a/src/agent_runtime_kit/_kit.py +++ b/src/agent_runtime_kit/_kit.py @@ -456,8 +456,9 @@ def _parse_result(output_type: type[_T], result: AgentResult) -> ParsedResult[_T # Never expose an adapter's unvalidated raw payload through the typed # ``ParsedResult`` surface when the adapter itself reported failure. values["parsed_output"] = None + values["parsed_output_available"] = False return cast("ParsedResult[_T]", ParsedResult(**values)) - if result.parsed_output is None: + if not result.parsed_output_available: return cast("ParsedResult[_T]", ParsedResult(**values)) try: instance = parse_as(output_type, result.parsed_output) @@ -466,6 +467,7 @@ def _parse_result(output_type: type[_T], result: AgentResult) -> ParsedResult[_T finish_reason=FinishReason.FAILED.value, error=f"structured output does not conform to {output_type.__name__}: {exc}", parsed_output=None, + parsed_output_available=False, ) return cast("ParsedResult[_T]", ParsedResult(**values)) values["parsed_output"] = instance diff --git a/src/agent_runtime_kit/_runtime.py b/src/agent_runtime_kit/_runtime.py index 39e63ed..5daafc9 100644 --- a/src/agent_runtime_kit/_runtime.py +++ b/src/agent_runtime_kit/_runtime.py @@ -1,4 +1,4 @@ -"""Dependency-free runtime implementations used by tests and examples.""" +"""Vendor-independent runtime implementations used by tests and examples.""" from __future__ import annotations @@ -6,6 +6,7 @@ from typing import Any from agent_runtime_kit._errors import UnsupportedTaskInputError +from agent_runtime_kit._schema import resolve_structured_output from agent_runtime_kit._types import ( AgentCapabilities, AgentResult, @@ -64,6 +65,19 @@ async def run(self, task: AgentTask) -> AgentResult: _ensure_supported(self.kind, self.capabilities, task) output = self._output if self._output is not None else f"Fake result for: {task.goal}" parsed = {"output": output} if task.output_schema is not None else None + parsed_available = parsed is not None + structured_error: str | None = None + if task.output_schema is not None: + resolution = resolve_structured_output( + task.output_schema, + output, + sdk_label="Fake runtime", + native=parsed, + native_available=True, + ) + parsed = resolution.value + parsed_available = resolution.available + structured_error = resolution.error tool_call = ToolCallAudit( tool_name="fake", arguments={"goal": task.goal}, @@ -94,7 +108,10 @@ async def run(self, task: AgentTask) -> AgentResult: ) result = AgentResult( output=output, + finish_reason="failed" if structured_error is not None else "done", + error=structured_error, parsed_output=parsed, + parsed_output_available=parsed_available, tool_calls=(tool_call,), session_id=task.session_id or task.task_id, rounds=1, @@ -103,6 +120,9 @@ async def run(self, task: AgentTask) -> AgentResult: except Exception as exc: await safe_emit(task, task_failed_event(task, self.kind, error=str(exc))) raise + if result.error is not None: + await safe_emit(task, task_failed_event(task, self.kind, error=result.error)) + return result await safe_emit(task, task_completed_event(task, self.kind, result)) return result diff --git a/src/agent_runtime_kit/_schema.py b/src/agent_runtime_kit/_schema.py index 7a43c4f..bcec59e 100644 --- a/src/agent_runtime_kit/_schema.py +++ b/src/agent_runtime_kit/_schema.py @@ -1,4 +1,4 @@ -"""Dependency-free bridge between Python types and JSON schema. +"""Bridge between Python types, JSON Schema, and structured payloads. ``json_schema_for`` turns an ``output_type`` into the JSON schema sent to the vendor SDK; ``parse_as`` turns the returned payload back into an instance of @@ -17,12 +17,19 @@ import dataclasses import enum +import inspect +import json import types import typing from collections.abc import Mapping +from dataclasses import dataclass from typing import Any, Literal, Union, get_args, get_origin, get_type_hints -from agent_runtime_kit._errors import OutputTypeError +from jsonschema import Draft202012Validator +from jsonschema.exceptions import SchemaError, best_match +from jsonschema.validators import validator_for + +from agent_runtime_kit._errors import OutputSchemaError, OutputTypeError # Deep enough for any sane payload model; recursive dataclasses hit this bound # and fail closed instead of overflowing. @@ -62,15 +69,116 @@ def parse_as(tp: Any, value: Any) -> Any: """ if supports_model_protocol(tp): + validator = tp.model_validate + validate_kwargs: dict[str, Any] = {} + try: + signature = inspect.signature(validator) + except (TypeError, ValueError): + signature = None + if signature is not None and ( + "strict" in signature.parameters + or any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + for parameter in signature.parameters.values() + ) + ): + validate_kwargs["strict"] = True try: - return tp.model_validate(value) + return validator(value, **validate_kwargs) except Exception as exc: + strict_note = "(strict=True)" if validate_kwargs else "" raise OutputTypeError( - f"{_name(tp)}.model_validate() rejected the payload: {exc}" + f"{_name(tp)}.model_validate{strict_note} rejected the payload: {exc}" ) from exc return _parse(tp, value, path="$", depth=0) +def validate_output_schema(schema: Mapping[str, Any]) -> None: + """Raise OutputSchemaError unless schema is valid and JSON-serializable.""" + + materialized = dict(schema) + try: + json.dumps(materialized, allow_nan=False) + except (TypeError, ValueError) as exc: + raise OutputSchemaError(f"invalid output_schema: not JSON-serializable: {exc}") from exc + + dialect = materialized.get("$schema") + default_validator: Any = Draft202012Validator if dialect is None else None + validator_cls = validator_for( + materialized, + default=default_validator, + ) + if validator_cls is None: + raise OutputSchemaError(f"invalid output_schema: unknown $schema dialect {dialect!r}") + try: + validator_cls.check_schema(materialized) + except SchemaError as exc: + raise OutputSchemaError(f"invalid output_schema: {exc.message}") from exc + + +def output_value_error(schema: Mapping[str, Any], value: Any) -> str | None: + """Return a concise instance-conformance error, or None when value is valid.""" + + materialized = dict(schema) + validate_output_schema(materialized) + validator_cls = validator_for(materialized, default=Draft202012Validator) + validator = validator_cls(materialized) + error = best_match(validator.iter_errors(value)) + if error is None: + return None + return f"{error.json_path}: {error.message}" + + +STRUCTURED_OUTPUT_MISSING = object() + + +@dataclass(frozen=True) +class StructuredOutputResolution: + """A locally validated output with explicit presence semantics.""" + + value: Any = None + available: bool = False + error: str | None = None + + +def resolve_structured_output( + schema: Mapping[str, Any], + output: str, + *, + sdk_label: str, + native: Any = STRUCTURED_OUTPUT_MISSING, + native_available: bool | None = None, +) -> StructuredOutputResolution: + """Resolve native/text JSON, preserve valid null, and enforce its schema.""" + + validate_output_schema(schema) + if native_available is None: + native_available = native is not STRUCTURED_OUTPUT_MISSING and native is not None + + value = native + available = native_available + if not available: + try: + value = json.loads(output) + available = True + except json.JSONDecodeError: + value = None + + if not available: + return StructuredOutputResolution( + error=f"{sdk_label} returned no parseable JSON for the requested output_schema" + ) + mismatch = output_value_error(schema, value) + if mismatch is not None: + return StructuredOutputResolution( + error=( + f"{sdk_label} returned structured output that does not conform to " + f"output_schema: {mismatch}" + ) + ) + return StructuredOutputResolution(value=value, available=True) + + def _schema(tp: Any, *, depth: int) -> dict[str, Any]: if depth > _MAX_DEPTH: raise OutputTypeError( diff --git a/src/agent_runtime_kit/_types.py b/src/agent_runtime_kit/_types.py index b2ed181..b089da5 100644 --- a/src/agent_runtime_kit/_types.py +++ b/src/agent_runtime_kit/_types.py @@ -377,6 +377,11 @@ class AgentTask: def __post_init__(self) -> None: object.__setattr__(self, "metadata", _freeze_mapping(self.metadata)) if self.output_schema is not None: + # Local import avoids an import cycle: _schema's typed errors import + # AgentRuntimeKind from this module. + from agent_runtime_kit._schema import validate_output_schema + + validate_output_schema(self.output_schema) object.__setattr__(self, "output_schema", _freeze_mapping(self.output_schema)) @@ -394,9 +399,15 @@ class AgentResult: session_id: str | None = None rounds: int = 0 metadata: Mapping[str, Any] = field(default_factory=dict) + # None is both valid JSON and the historical missing-value sentinel. This + # trailing bit preserves the distinction without shifting existing positional + # constructor arguments. + parsed_output_available: bool = False def __post_init__(self) -> None: object.__setattr__(self, "metadata", _freeze_mapping(self.metadata)) + if self.parsed_output is not None and not self.parsed_output_available: + object.__setattr__(self, "parsed_output_available", True) @property def cost_usd(self) -> float: @@ -420,7 +431,7 @@ class ParsedResult(AgentResult, Generic[_ParsedT]): @property def parsed(self) -> _ParsedT | None: - """The validated instance, or ``None`` when the run failed.""" + """The validated instance; use parsed_output_available to distinguish null.""" return cast("_ParsedT | None", self.parsed_output) diff --git a/tests/test_core.py b/tests/test_core.py index c60668d..c417a27 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -18,6 +18,7 @@ FilesystemAccess, FinishReason, McpServerConfig, + OutputSchemaError, PermissionMode, PermissionProfile, RuntimeAvailability, @@ -120,6 +121,22 @@ def test_agent_task_does_not_leak_caller_mapping() -> None: task.metadata["model"] = "nope" # type: ignore[index] +def test_agent_task_rejects_invalid_output_schema_at_construction() -> None: + with pytest.raises(OutputSchemaError, match="invalid output_schema"): + AgentTask(goal="g", output_schema={"required": "not-an-array"}) + + +def test_agent_result_distinguishes_absent_output_from_valid_null() -> None: + absent = AgentResult(output="") + inferred = AgentResult(output="", parsed_output={"ok": True}) + valid_null = AgentResult(output="null", parsed_output=None, parsed_output_available=True) + + assert absent.parsed_output_available is False + assert inferred.parsed_output_available is True + assert valid_null.parsed_output is None + assert valid_null.parsed_output_available is True + + @pytest.mark.parametrize( ("instance", "field_name"), [ @@ -217,3 +234,22 @@ async def test_fake_runtime_rejects_unsupported_structured_output() -> None: await runtime.run(task) assert exc_info.value.field == "output_schema" + + +@pytest.mark.asyncio +async def test_fake_runtime_fails_when_generated_output_does_not_match_schema() -> None: + runtime = FakeAgentRuntime(output="done") + task = AgentTask( + goal="return count", + output_schema={ + "type": "object", + "properties": {"count": {"type": "integer"}}, + "required": ["count"], + }, + ) + + result = await runtime.run(task) + + assert result.finish_reason == "failed" + assert "does not conform" in (result.error or "") + assert result.parsed_output_available is False diff --git a/tests/test_kit.py b/tests/test_kit.py index 113bac5..21d72bf 100644 --- a/tests/test_kit.py +++ b/tests/test_kit.py @@ -6,6 +6,7 @@ from typing import Any import pytest +from pydantic import BaseModel from agent_runtime_kit import ( AgentKit, @@ -29,6 +30,10 @@ class Point: y: int +class CountModel(BaseModel): + count: int + + class RecordingRuntime: """Protocol-complete runtime that records the task and returns a canned result.""" @@ -227,6 +232,46 @@ async def test_kit_output_type_mismatch_fails_the_result_not_the_call() -> None: assert result.finish_reason == "failed" assert result.error is not None and "Point" in result.error assert result.parsed is None + assert result.parsed_output_available is False + + +@pytest.mark.asyncio +async def test_kit_output_type_uses_strict_pydantic_validation() -> None: + runtime = RecordingRuntime( + AgentResult(output='{"count": "42"}', parsed_output={"count": "42"}) + ) + + result = await AgentKit(register_default_adapters=False).run( + runtime, + goal="count", + output_type=CountModel, + ) + + assert result.finish_reason == "failed" + assert "strict=True" in (result.error or "") + assert result.parsed_output_available is False + + +@pytest.mark.asyncio +async def test_kit_output_type_accepts_available_json_null() -> None: + runtime = RecordingRuntime( + AgentResult( + output="null", + parsed_output=None, + parsed_output_available=True, + ) + ) + + result = await AgentKit(register_default_adapters=False).run( + runtime, + goal="return null", + output_type=type(None), + ) + + assert result.finish_reason == "done" + assert result.error is None + assert result.parsed is None + assert result.parsed_output_available is True @pytest.mark.asyncio @@ -258,6 +303,7 @@ async def test_kit_output_type_never_leaks_failed_raw_payload() -> None: assert result.finish_reason == "failed" assert result.error == "vendor rejected structured output" assert result.parsed_output is None + assert result.parsed_output_available is False assert result.parsed is None diff --git a/tests/test_schema.py b/tests/test_schema.py index d717d3d..ae705f9 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -5,9 +5,16 @@ from typing import Any, Literal, TypedDict import pytest - -from agent_runtime_kit import OutputTypeError -from agent_runtime_kit._schema import json_schema_for, parse_as +from pydantic import BaseModel + +from agent_runtime_kit import OutputSchemaError, OutputTypeError +from agent_runtime_kit._schema import ( + json_schema_for, + output_value_error, + parse_as, + resolve_structured_output, + validate_output_schema, +) class Color(str, enum.Enum): @@ -198,3 +205,69 @@ def model_validate(cls, value: Any) -> BadDuck: with pytest.raises(OutputTypeError, match="expected a mapping"): json_schema_for(BadDuck) + + +def test_validate_output_schema_accepts_known_dialects_and_rejects_bad_definitions() -> None: + validate_output_schema({"type": "object", "properties": {"count": {"type": "integer"}}}) + validate_output_schema( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "null", + } + ) + + with pytest.raises(OutputSchemaError, match="invalid output_schema"): + validate_output_schema({"required": "count"}) + with pytest.raises(OutputSchemaError, match="unknown .* dialect"): + validate_output_schema({"$schema": "https://example.invalid/unknown-schema"}) + with pytest.raises(OutputSchemaError, match="not JSON-serializable"): + validate_output_schema({"enum": [{1, 2}]}) + + +def test_output_value_error_reports_nested_json_path() -> None: + mismatch = output_value_error( + { + "type": "object", + "properties": { + "item": { + "type": "object", + "properties": {"count": {"type": "integer"}}, + } + }, + }, + {"item": {"count": "42"}}, + ) + + assert mismatch is not None + assert "$.item.count" in mismatch + + +def test_structured_output_resolution_distinguishes_null_absence_and_mismatch() -> None: + valid_null = resolve_structured_output({"type": "null"}, "null", sdk_label="Test SDK") + missing = resolve_structured_output({"type": "null"}, "not JSON", sdk_label="Test SDK") + explicit_null = resolve_structured_output( + {"type": "null"}, + "", + sdk_label="Test SDK", + native=None, + native_available=True, + ) + mismatch = resolve_structured_output( + {"type": "object", "required": ["ok"]}, + '{"wrong": true}', + sdk_label="Test SDK", + ) + + assert valid_null.available is True and valid_null.value is None + assert explicit_null.available is True and explicit_null.value is None + assert missing.available is False and "no parseable JSON" in (missing.error or "") + assert mismatch.available is False and "does not conform" in (mismatch.error or "") + + +def test_pydantic_model_validation_is_strict() -> None: + class CountModel(BaseModel): + count: int + + assert parse_as(CountModel, {"count": 42}).count == 42 + with pytest.raises(OutputTypeError, match="strict=True"): + parse_as(CountModel, {"count": "42"}) diff --git a/uv.lock b/uv.lock index df10712..0fb792e 100644 --- a/uv.lock +++ b/uv.lock @@ -12,7 +12,7 @@ required-markers = [ ] [options] -exclude-newer = "2026-06-24T17:18:14.378186Z" +exclude-newer = "2026-07-02T10:44:27.590005Z" exclude-newer-span = "P8D" [manifest] @@ -31,6 +31,9 @@ wheels = [ name = "agent-runtime-kit" version = "0.4.0" source = { editable = "." } +dependencies = [ + { name = "jsonschema" }, +] [package.optional-dependencies] all = [ @@ -52,10 +55,12 @@ codex = [ dev = [ { name = "build" }, { name = "mypy" }, + { name = "pydantic" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "types-jsonschema" }, ] [package.metadata] @@ -64,6 +69,7 @@ requires-dist = [ { name = "claude-agent-sdk", marker = "extra == 'claude'", specifier = ">=0.2.87,<0.3" }, { name = "google-antigravity", marker = "extra == 'all'", specifier = ">=0.1.2,<0.2" }, { name = "google-antigravity", marker = "extra == 'antigravity'", specifier = ">=0.1.2,<0.2" }, + { name = "jsonschema", specifier = ">=4.18,<5" }, { name = "openai-codex", marker = "extra == 'all'", specifier = ">=0.1.0b3,<0.2" }, { name = "openai-codex", marker = "extra == 'codex'", specifier = ">=0.1.0b3,<0.2" }, ] @@ -73,10 +79,12 @@ provides-extras = ["claude", "codex", "antigravity", "all"] dev = [ { name = "build", specifier = ">=1.2" }, { name = "mypy", specifier = ">=1.8" }, + { name = "pydantic", specifier = ">=2,<3" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-asyncio", specifier = ">=0.23" }, { name = "pytest-cov", specifier = ">=5.0" }, { name = "ruff", specifier = ">=0.8" }, + { name = "types-jsonschema", specifier = ">=4.18" }, ] [[package]] @@ -1723,6 +1731,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "types-jsonschema" +version = "4.26.0.20260518" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/46/73b6a5d61a61015c4248030a8cb07e5bdddb4041430fae9e585a68692578/types_jsonschema-4.26.0.20260518.tar.gz", hash = "sha256:e1dd53dc97a64f5eccdd6fa9839666e09bb500a8ebba2db6fdaf1789faea81a6", size = 16638, upload-time = "2026-05-18T06:06:44.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/d5/134f8a147dcecda10db7f60cfc6af0578a25a5c53c87b3907a64385e0184/types_jsonschema-4.26.0.20260518-py3-none-any.whl", hash = "sha256:30b30a518c7fe335df85c919fcbcc631b69c03d4a4b5b632fa916bea03065307", size = 16072, upload-time = "2026-05-18T06:06:43.264Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0"