diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6ed1ddd9..f4f733b9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,9 @@ jobs:
- name: ty
run: uv run ty check
+ - name: Trace schema drift
+ run: uv run python scripts/generate_trace_schema.py --check
+
test:
name: Test (Python ${{ matrix.python-version }})
needs: lint
diff --git a/docs/concepts/trace-schema.md b/docs/concepts/trace-schema.md
new file mode 100644
index 00000000..99434438
--- /dev/null
+++ b/docs/concepts/trace-schema.md
@@ -0,0 +1,226 @@
+# Trace/Result Schema & Migration Policy
+
+`rampart.core.serialization` defines RAMPART's canonical, versioned
+`Result`-record format. `ResultRecord.to_dict()` / `ResultRecord.from_dict()` own
+the versioned envelope and optional `pytest_nodeid` / `result_index` attribution.
+`serialize_record(record=...)` converts a `ResultRecord` to JSON text (`str`);
+`deserialize_record(data=...)` reconstructs a `ResultRecord` from JSON text.
+Existing xdist and reporting consumers are not yet wired to this module.
+
+This page defines how the schema may evolve as consumers adopt it.
+
+## Serialization and schema generation
+
+`Result.to_dict()` / `Result.from_dict()` own the **unversioned body**, using one
+cached Pydantic `TypeAdapter` over the existing standard dataclasses. Body dicts
+are fragments, not standalone durable records: persist a `ResultRecord` to
+include the version. `ResultRecord` references the live result; serialization
+does not mutate it.
+
+The dictionary methods remain available for projections and structured
+inspection. The serialization functions use those same methods at the JSON-text
+boundary, without a separate codec:
+
+```python
+record = ResultRecord(result=result, pytest_nodeid="tests/test_safety.py::test_case")
+text = serialize_record(record=record)
+restored = deserialize_record(data=text)
+```
+
+Malformed JSON and invalid record values raise `SchemaError`; unsupported
+versions raise its `UnsupportedSchemaVersionError` subclass.
+
+The adapter validates nested fields without string, boolean, or integer
+coercion. Dictionary input is checked for JSON-only values before strict
+JSON-mode validation reconstructs the dataclasses. Missing fields use their
+declared defaults; explicit `null` is accepted only on nullable fields. Payload
+IDs must be recorded, not generated during deserialization. These boundary
+rules do not replace the normal dataclass constructors used during execution.
+
+Body encoding uses adapter-local enum and datetime serializers in Python mode,
+then validates the output through the canonical reader before returning it.
+This extra validation pass aligns writer and reader nesting support without
+introducing a new depth cap or inheriting Pydantic's lower JSON-mode writer limit.
+Interpreter and parser recursion limits still apply; failures raise `SchemaError`.
+
+These policies belong to the cached canonical adapter, not to the public
+dataclass annotations or configuration. Fields remain `dict[str, Any]` and
+`datetime | None`. Independently constructed Pydantic adapters retain their
+normal behavior, including live binary payload support.
+The canonical adapter supplies its own `datetime` / `Path` resolution namespace;
+the shared types module keeps those imports under `TYPE_CHECKING`.
+
+`ResultRecord.json_schema()` returns the adapter-derived body schema plus the
+versioned envelope. Small schema customizations describe the trace-only payload
+restrictions and the request invariant (a prompt or at least one attachment).
+`JsonSchemaValue` is the return type, not a separate model or validator.
+The open Draft 2020-12 contract is committed at `schemas/trace.v1.schema.json`.
+
+Regenerate it with `uv run python scripts/generate_trace_schema.py`.
+CI runs the same command with `--check` to detect drift. Changes to generated
+output still require a compatibility review; generation does not decide whether
+a version bump is needed.
+
+### Structural schema and decoder semantics
+
+The JSON Schema checks structure; passing it is necessary but **not sufficient**
+for successful record decoding. Use `deserialize_record()` (or
+`ResultRecord.from_dict()` for dictionaries) for the complete contract.
+
+The decoder additionally enforces these representation rules:
+
+- Integer fields use integer notation, not floating-point notation. JSON Schema
+ accepts `0.0` as an integer mathematically; the strict decoder rejects it for
+ fields such as `result_index`, `turn_number`, and population `index` / `size`.
+- Timestamp strings must parse with Python's `datetime.fromisoformat()`. The
+ schema intentionally does not claim RFC 3339 validation, since Python supports
+ naive datetimes and subminute UTC offsets.
+- Numbers must be finite and representable by the corresponding Python field.
+ For example, an overflowing JSON exponent cannot become an infinite float.
+- Strings and mapping keys must contain Unicode scalar values. Surrogate code
+ points in Python strings are rejected, not replaced or combined. Valid JSON
+ surrogate-pair escapes for characters such as emoji remain supported.
+
+External producers should emit integer notation for integer fields, supported
+ISO datetime strings, and finite numbers, then exercise the canonical reader
+as well as structural schema validation. These are decoder requirements, not
+additional serializers.
+
+## Transport compatibility boundary
+
+This section defines integration requirements. No canonical transport
+preparation API is implemented yet.
+
+The canonical codec preserves supported values; it does not provide a lenient
+transport mode. Existing transports and flat reports can accept data outside
+that domain, so adopting the codec is not a direct replacement of their current
+serialization calls.
+
+Transport normalization must happen **before** canonical encoding when the live
+result contains unsupported values. Prepare a separate result without mutating
+the original, then use the same record codec. Caps, rendering sanitization,
+worker bookkeeping, and explicit loss/truncation markers remain transport
+responsibilities. None belongs in a second field-by-field result serializer.
+
+A text placeholder prepared from a binary payload is a lossy transport view,
+not a durable copy of that payload. Original format/path information must remain
+available for transport diagnostics, and the containing transport must identify
+the loss. Do not persist that view as a full-fidelity replay artifact. The
+canonical reader itself never performs this conversion or opens a worker path.
+Supporting durable binary artifacts requires a separately designed
+representation and compatibility review; reserving an `artifacts` field alone
+does not make currently rejected formats readable by older readers.
+
+## Versioning
+
+- Every serialized record carries one root `version` field. The current schema
+ is **`rampart.trace.v1`**.
+- The record version is **independent** of transport or projection versions,
+ including the existing xdist envelope version (`rampart.xdist.v2`). Each
+ version describes its own layer and may evolve separately.
+- There is a **single root version** — nested types (`Turn`, `Payload`,
+ `EvalResult`, …) do not carry their own versions.
+
+## What is and is not a breaking change
+
+- **Additive-optional = no bump.** A new optional field that older readers may
+ ignore, and whose absence has a defined default, does not change the major.
+- **Missing = not recorded (not "false").** An absent optional field means the
+ producer *did not record it* — never that its value was empty, false, or zero.
+ Readers supply a default for *shape* only; consumers must not infer a semantic
+ negative from absence. A v1 record with no `manifest_snapshot` means "the
+ manifest was not captured," not "there was no manifest."
+ This is interpretation guidance, not field-presence tracking: decoding uses
+ defaults and does not retain which fields were absent. For example, omitted
+ `turns` becomes `[]` and is emitted when re-encoded.
+- **Structural change = major bump.** Removing, renaming, or retyping a field,
+ or changing its meaning or nesting, bumps `vN → vN+1` with a changelog and a
+ migration note.
+
+```mermaid
+flowchart TD
+ change([proposed schema change]) --> q1{"adds a field only?"}
+ q1 -- no --> struct["structural:
remove / rename / retype /
change meaning or nesting"]
+ q1 -- yes --> q2{"optional with a
well-defined default?"}
+ q2 -- no --> struct
+ q2 -- yes --> add["additive-optional"]
+ add --> nobump["NO bump
(new optional fields)
old readers ignore unknown keys"]
+ struct --> bump["bump major vN → vN+1
+ changelog + migration note"]
+ bump --> reader["readers: fail closed on
unknown major"]
+```
+
+## Reader posture
+
+- Readers tolerate unknown fields and **fail closed on an unknown major** — a
+ record is never best-effort parsed across a major boundary.
+- Forward compatibility is **additive-only within a major**. A newer major read
+ by an older framework fails closed by design.
+- Schema descriptions and validators derived from this format must remain open
+ to unknown properties within a major version.
+
+## Enum posture
+
+- The closed enums — `SafetyStatus`, `EvalOutcome`, `ObservabilityLevel`, and
+ `PayloadFormat` — **fail closed** on an unknown value. A serialized safety
+ result must never silently misread one; there is no warn-and-degrade path.
+- `HarmCategory` is the sole exception: it travels as a **passthrough string**
+ and is never coerced, so a new harm label from a future producer round-trips
+ unchanged on an older reader.
+
+## Value domain
+
+- Free-form mappings must already contain JSON-safe values: null, strings,
+ booleans, finite numbers, lists, and string-keyed mappings. Tuples, bytes,
+ cycles, and opaque objects are rejected rather than coerced.
+- Numeric values must be finite. Transport-specific normalization is outside
+ the canonical schema.
+- Strings and mapping keys must contain Unicode scalar values, including optional
+ attribution. Encoding rejects surrogate-containing strings before returning
+ a body or record; decoding rejects them in dictionary input as well.
+- Timestamps retain Python's ISO 8601 representation, including naive datetimes
+ and UTC offsets. The schema describes strings rather than RFC 3339
+ `date-time`, which would exclude some supported Python datetimes.
+- `rampart.trace.v1` does not define a durable representation for binary or
+ opaque payload artifacts. Encoding or decoding one fails closed rather than
+ coercing it to text.
+- `ResultRecord.to_dict()` removes transport bookkeeping keys, including
+ `_rampart_source_worker`, from top-level `Result.metadata` in the encoded
+ output. Body-only serialization and record decoding do not filter these keys.
+ Re-encoding a decoded record filters them from output without mutating the
+ result. Nested user mappings are preserved.
+
+## Migration mechanics
+
+Only `rampart.trace.v1` exists today. No upcaster or persisted-data migration
+tooling is implemented. If a later structural change introduces a new major,
+the migration policy requires:
+
+- writers emit the latest supported major;
+- support for an older major uses an explicit adjacent upcaster
+ (`vN-1 → vN`);
+- migrating persisted data is an explicit operation; reading never rewrites an
+ artifact in place; and
+- encountering an unsupported major fails closed.
+
+## Reserved additive fields (named now, populated later)
+
+These record-level wire-only collar slots are reserved by name so they can be
+added without a major bump:
+`manifest_snapshot`, `evaluation_fingerprint`, `replay_provenance`,
+`population_ref`, plus `artifacts` / `target` / `provenance`. A field that is
+truly *intrinsic to a result* instead lands as an additive-optional field on
+`Result`, inside the referenced `result` body. Either way each is
+additive-optional; none is emitted by the current implementation.
+
+Other future fields follow the same general rule: optional additions with a
+defined absence behavior do not require a major bump; structural changes do.
+
+## Support window
+
+This is a release-support commitment; the current reader supports only
+`rampart.trace.v1`.
+
+Starting with the first release that writes durable trace records by default,
+RAMPART supports reading `vN` and `vN-1` for **two subsequent framework
+releases** (one deprecation cycle). The window is keyed on releases, not time.
+Any major bump includes a changelog entry and migration note.
diff --git a/mkdocs.yml b/mkdocs.yml
index c74a9f5a..606a9133 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -151,6 +151,7 @@ nav:
- Attacks: concepts/attacks.md
- Probes: concepts/probes.md
- PyRIT Integration: concepts/pyrit.md
+ - Trace Schema & Migration: concepts/trace-schema.md
- Attacks:
- attacks/index.md
- XPIA: attacks/xpia.md
diff --git a/pyproject.toml b/pyproject.toml
index c8faf744..05759e6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,6 +54,8 @@ dev = [
"flake8>=7.3.0",
"hatch-vcs>=0.5.0",
"hatchling>=1.30.1",
+ "hypothesis>=6.168.0",
+ "jsonschema>=4.26.0",
"pre-commit>=4.5.1",
"pytest-cov>=6.1.0",
"pytest-xdist[psutil]>=3.8.0",
@@ -128,6 +130,9 @@ external = ["RMP001", "RMP002"]
"scripts/hatch_build.py" = [
"implicit-namespace-package", # Top-level build hook
]
+"scripts/generate_trace_schema.py" = [
+ "implicit-namespace-package", # Standalone schema generation command
+]
"tests/integration/conftest.py" = [
"unused-noqa", # Ruff 0.16.4 does not recognize pytest-fixture-autouse.
]
diff --git a/rampart/core/_schema.py b/rampart/core/_schema.py
new file mode 100644
index 00000000..4e9df223
--- /dev/null
+++ b/rampart/core/_schema.py
@@ -0,0 +1,280 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+"""Adapter-local policies for the canonical dataclass trace schema."""
+
+from __future__ import annotations
+
+import math
+from collections.abc import Mapping
+from datetime import datetime
+from operator import attrgetter
+from typing import TYPE_CHECKING
+
+from pydantic_core import core_schema
+
+from rampart.core.types import Payload, PayloadFormat
+
+if TYPE_CHECKING:
+ from pydantic import (
+ GetCoreSchemaHandler,
+ ValidationError,
+ ValidationInfo,
+ )
+
+
+# Pydantic supplies source/handler positionally to schema hooks.
+def trace_schema(
+ source: object, handler: GetCoreSchemaHandler
+) -> core_schema.CoreSchema:
+ """Apply canonical policies to an adapter's schema, not its live classes.
+
+ Returns:
+ CoreSchema: A configured copy of the generated dataclass schema.
+ """
+ return _trace_schema(schema=handler(source), handler=handler, references=set())
+
+
+def _trace_schema(
+ *,
+ schema: core_schema.CoreSchema,
+ handler: GetCoreSchemaHandler,
+ references: set[str],
+) -> core_schema.CoreSchema:
+ """Copy generated schema nodes while applying shared trace rules.
+
+ Returns:
+ CoreSchema: The adapter-local schema, preserving definition references.
+ """
+ if schema["type"] == "definition-ref":
+ reference = schema["schema_ref"]
+ if reference in references:
+ return schema
+ references.add(reference)
+ return _trace_schema(
+ schema=handler.resolve_ref_schema(schema),
+ handler=handler,
+ references=references,
+ )
+
+ schema = schema.copy()
+ if schema["type"] == "dataclass":
+ return _trace_dataclass(schema=schema, handler=handler, references=references)
+ if schema["type"] == "default" or schema["type"] == "nullable":
+ schema["schema"] = _trace_schema(
+ schema=schema["schema"], handler=handler, references=references
+ )
+ elif schema["type"] == "dataclass-args":
+ schema["fields"] = [
+ {
+ **field,
+ "schema": _trace_schema(
+ schema=field["schema"], handler=handler, references=references
+ ),
+ }
+ for field in schema["fields"]
+ ]
+ elif schema["type"] == "list":
+ schema["items_schema"] = _trace_schema(
+ schema=schema["items_schema"], handler=handler, references=references
+ )
+ elif schema["type"] == "union":
+ schema["choices"] = [
+ (
+ _trace_schema(schema=choice[0], handler=handler, references=references),
+ choice[1],
+ )
+ if isinstance(choice, tuple)
+ else _trace_schema(schema=choice, handler=handler, references=references)
+ for choice in schema["choices"]
+ ]
+ elif schema["type"] == "dict":
+ return core_schema.no_info_before_validator_function(json_value, schema)
+
+ return _trace_scalar(schema)
+
+
+def _trace_scalar(schema: core_schema.CoreSchema) -> core_schema.CoreSchema:
+ """Apply wire representations to copied enum and datetime schema nodes.
+
+ Returns:
+ CoreSchema: The schema with scalar serialization policies applied.
+ """
+ if schema["type"] == "enum":
+ schema["serialization"] = core_schema.plain_serializer_function_ser_schema(
+ attrgetter("value")
+ )
+ elif schema["type"] == "datetime":
+ return core_schema.with_info_before_validator_function(
+ _iso_datetime,
+ schema,
+ serialization=core_schema.plain_serializer_function_ser_schema(
+ datetime.isoformat, return_schema=core_schema.str_schema()
+ ),
+ )
+
+ return schema
+
+
+def _trace_dataclass(
+ *,
+ schema: core_schema.DataclassSchema,
+ handler: GetCoreSchemaHandler,
+ references: set[str],
+) -> core_schema.CoreSchema:
+ """Configure a copied dataclass schema without changing its class.
+
+ Returns:
+ CoreSchema: A revalidating schema with trace-only payload guards.
+ """
+ schema["schema"] = _trace_schema(
+ schema=schema["schema"], handler=handler, references=references
+ )
+ schema["config"] = {
+ **schema.get("config", {}),
+ "strict": True,
+ "revalidate_instances": "always",
+ "allow_inf_nan": False,
+ }
+ if schema["cls"] is Payload:
+ reference = schema.pop("ref", None)
+ return core_schema.no_info_before_validator_function(
+ _trace_payload, schema, ref=reference
+ )
+ return schema
+
+
+def _trace_payload(value: object) -> object:
+ """Reject unsupported artifacts before dataclass construction touches them.
+
+ Returns:
+ object: The unchanged payload for normal field validation.
+
+ Raises:
+ ValueError: If identity is absent or a binary artifact is encountered.
+ """
+ if isinstance(value, Mapping):
+ if "id" not in value:
+ msg = "id: a trace payload must record its id"
+ raise ValueError(msg)
+ payload_format = value.get("format", PayloadFormat.TEXT)
+ artifact = value.get("artifact")
+ elif isinstance(value, Payload):
+ payload_format = value.format
+ artifact = value.artifact
+ else:
+ return value
+ if isinstance(payload_format, PayloadFormat):
+ payload_format = payload_format.value
+ if isinstance(payload_format, str) and payload_format in {
+ member.value for member in PayloadFormat if member.is_binary
+ }:
+ msg = "binary payload format and artifact are unsupported in traces"
+ raise ValueError(msg)
+ if artifact is not None:
+ msg = "artifact: only null is supported in traces"
+ raise ValueError(msg)
+ return value
+
+
+def json_string(*, value: str, path: str) -> str:
+ """Require Unicode scalar values without altering the string.
+
+ Returns:
+ str: The unchanged string.
+
+ Raises:
+ ValueError: If the string contains surrogate code points.
+ """
+ try:
+ value.encode("utf-8")
+ except UnicodeEncodeError as exc:
+ msg = f"{path}: surrogate code points are not supported in strings"
+ raise ValueError(msg) from exc
+ return value
+
+
+def json_value(value: object) -> object:
+ """Check and copy JSON values without lossy coercion.
+
+ Returns:
+ object: JSON primitives, lists, and string-keyed dictionaries.
+
+ Raises:
+ ValueError: If a value is non-finite, cyclic, or outside the JSON domain.
+ """
+ return _json_value(value=value, path="$", active=set())
+
+
+def _json_value(*, value: object, path: str, active: set[int]) -> object:
+ """Recursively validate the JSON domain, retaining the offending path.
+
+ Returns:
+ object: A JSON-safe copy.
+
+ Raises:
+ ValueError: If the value cannot be represented faithfully in JSON.
+ """
+ if isinstance(value, str):
+ return json_string(value=value, path=path)
+ if value is None or isinstance(value, bool | int):
+ return value
+ if isinstance(value, float) and math.isfinite(value):
+ return value
+ if not isinstance(value, Mapping | list):
+ msg = f"{path}: {type(value).__name__} is outside the finite JSON domain"
+ raise ValueError(msg) # ruff: ignore[type-check-without-type-error] Pydantic wraps ValueError.
+ if id(value) in active:
+ msg = f"{path}: cyclic JSON value"
+ raise ValueError(msg)
+ active.add(id(value))
+ try:
+ if isinstance(value, list):
+ return [
+ _json_value(value=item, path=f"{path}[{index}]", active=active)
+ for index, item in enumerate(value)
+ ]
+ result: dict[str, object] = {}
+ for key, item in value.items():
+ if not isinstance(key, str):
+ msg = f"{path}: JSON object keys must be strings"
+ raise ValueError(msg) # ruff: ignore[type-check-without-type-error] Pydantic wraps ValueError.
+ json_string(value=key, path=f"{path}.")
+ result[key] = _json_value(value=item, path=f"{path}.{key}", active=active)
+ return result
+ finally:
+ active.remove(id(value))
+
+
+# Pydantic supplies value/info positionally to BeforeValidator callbacks.
+def _iso_datetime(value: object, info: ValidationInfo) -> object:
+ """Retain Python ISO datetime support, including naive and subminute offsets.
+
+ Returns:
+ object: Parsed datetime strings, or the unchanged value for validation.
+
+ Raises:
+ ValueError: If the string is not an ISO datetime.
+ """
+ if info.mode == "json" and isinstance(value, str):
+ try:
+ return datetime.fromisoformat(value)
+ except ValueError as exc:
+ msg = "expected an ISO 8601 datetime string"
+ raise ValueError(msg) from exc
+ return value
+
+
+def validation_message(*, error: ValidationError, path: str) -> str:
+ """Render Pydantic errors without including producer data in the message.
+
+ Returns:
+ str: Field paths and validation reasons.
+ """
+ messages: list[str] = []
+ for detail in error.errors(include_url=False, include_input=False):
+ location = path
+ for part in detail["loc"]:
+ location += f"[{part}]" if isinstance(part, int) else f".{part}"
+ messages.append(f"{location}: {detail['msg']}")
+ return "; ".join(messages)
diff --git a/rampart/core/errors.py b/rampart/core/errors.py
index a5d8655a..f585a917 100644
--- a/rampart/core/errors.py
+++ b/rampart/core/errors.py
@@ -47,3 +47,11 @@ class EvaluatorError(InfrastructureError):
``InfrastructureError`` base class) and produces a Result with
SafetyStatus.ERROR.
"""
+
+
+class SchemaError(Exception):
+ """A value cannot be represented by the canonical trace schema."""
+
+
+class UnsupportedSchemaVersionError(SchemaError):
+ """A record's version has no registered decoder."""
diff --git a/rampart/core/result.py b/rampart/core/result.py
index 2683a2e3..497ffbed 100644
--- a/rampart/core/result.py
+++ b/rampart/core/result.py
@@ -11,21 +11,48 @@
from __future__ import annotations
+import json
from dataclasses import dataclass, field
+from datetime import datetime
from enum import Enum, StrEnum
-from typing import TYPE_CHECKING, Any
+from functools import cache
+from pathlib import Path
+from typing import (
+ TYPE_CHECKING,
+ Annotated,
+ Any,
+)
+
+from pydantic import (
+ GetPydanticSchema,
+ TypeAdapter,
+ ValidationError,
+)
+from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue
from rampart.common.text import safe_str, safe_str_list
+from rampart.core._schema import (
+ json_value,
+ trace_schema,
+ validation_message,
+)
+from rampart.core.errors import SchemaError
from rampart.core.types import (
EvalOutcome,
EvalResult,
ObservabilityLevel,
+ Payload,
+ PayloadFormat,
+ Request,
Turn,
)
if TYPE_CHECKING:
from collections.abc import Iterable
+ from pydantic.json_schema import JsonSchemaMode
+ from pydantic_core import core_schema
+
class SafetyStatus(Enum):
"""Categorical safety status for structured reporting.
@@ -194,6 +221,137 @@ def __repr__(self) -> str:
f"summary={self.summary!r})"
)
+ def to_dict(self) -> dict[str, Any]:
+ """Serialize the unversioned body, not a standalone durable record.
+
+ Returns:
+ dict[str, Any]: A JSON-safe body for a ResultRecord envelope.
+
+ Raises:
+ SchemaError: If the result is outside the trace value domain.
+ """
+ adapter = _result_adapter()
+ try:
+ validated = adapter.validate_python(self, strict=True)
+ # JSON-mode dumping has a lower nesting limit than the reader.
+ body = adapter.dump_python(validated, mode="python", warnings="error")
+ Result.from_dict(body)
+ except ValidationError as exc:
+ raise SchemaError(validation_message(error=exc, path="result")) from exc
+ except (ValueError, RecursionError) as exc:
+ msg = f"result: cannot serialize canonical body ({type(exc).__name__})"
+ raise SchemaError(msg) from exc
+ return body
+
+ @classmethod
+ def from_dict(cls, data: object) -> Result:
+ """Validate and reconstruct an unversioned canonical body.
+
+ Args:
+ data (object): A JSON-compatible body from a versioned record.
+
+ Returns:
+ Result: The reconstructed result.
+
+ Raises:
+ SchemaError: If the body is malformed or outside the trace domain.
+ """
+ try:
+ # JSON-mode strict validation accepts wire enums/dates, not coercions.
+ encoded = json.dumps(json_value(data), allow_nan=False)
+ return _result_adapter().validate_json(encoded, strict=True)
+ except ValidationError as exc:
+ raise SchemaError(validation_message(error=exc, path="result")) from exc
+ except (ValueError, RecursionError) as exc:
+ msg = f"result: {exc}"
+ raise SchemaError(msg) from exc
+
+ @classmethod
+ def json_schema(cls) -> JsonSchemaValue:
+ """Generate the body contract from the configured dataclass adapter.
+
+ Returns:
+ JsonSchemaValue: The JSON Schema for the unversioned body.
+ """
+ return _result_adapter().json_schema(schema_generator=_ResultJsonSchema)
+
+
+@cache
+def _result_adapter() -> TypeAdapter[Result]:
+ """Build the recursive adapter once, on first serialization use.
+
+ Returns:
+ TypeAdapter[Result]: The cached adapter.
+ """
+ adapter = TypeAdapter[Result](Annotated[Result, GetPydanticSchema(trace_schema)])
+ # Nested dataclasses keep these imports under TYPE_CHECKING.
+ adapter.rebuild(_types_namespace={"datetime": datetime, "Path": Path})
+ return adapter
+
+
+class _ResultJsonSchema(GenerateJsonSchema):
+ """Describe trace-only restrictions alongside the dataclass field schemas."""
+
+ def generate(
+ self, schema: core_schema.CoreSchema, mode: JsonSchemaMode = "validation"
+ ) -> JsonSchemaValue:
+ """Omit runtime class documentation from the published wire contract.
+
+ Returns:
+ JsonSchemaValue: A schema with only trace-specific descriptions.
+ """
+ result = super().generate(schema, mode=mode)
+ result.pop("description", None)
+ definitions = result.get("$defs", {})
+ for definition in definitions.values():
+ definition.pop("description", None)
+ if "Payload" in definitions:
+ definitions["Payload"]["description"] = (
+ "Recorded text payload. Binary formats and file artifacts "
+ "are not supported by this trace schema."
+ )
+ return result
+
+ def dataclass_schema(self, schema: core_schema.DataclassSchema) -> JsonSchemaValue:
+ """Add trace policies that do not restrict live dataclass construction.
+
+ Returns:
+ JsonSchemaValue: An open object schema matching the trace validators.
+ """
+ result = super().dataclass_schema(schema)
+ result["additionalProperties"] = True
+ if schema["cls"] is Payload:
+ result["properties"]["format"] = {
+ "type": "string",
+ "enum": [value.value for value in PayloadFormat if value.is_text],
+ "default": PayloadFormat.TEXT.value,
+ }
+ result["properties"]["artifact"] = {"type": "null", "default": None}
+ result["required"] = [*result["required"], "id"]
+ elif schema["cls"] is Request:
+ result["anyOf"] = [
+ {"required": ["prompt"], "properties": {"prompt": {"type": "string"}}},
+ {
+ "required": ["attachments"],
+ "properties": {"attachments": {"type": "array", "minItems": 1}},
+ },
+ ]
+ return result
+
+ def datetime_schema(self, schema: core_schema.DatetimeSchema) -> JsonSchemaValue:
+ """Describe Python datetimes without claiming RFC 3339 validation.
+
+ Returns:
+ JsonSchemaValue: A string with decoder-enforced datetime semantics.
+ """
+ result = super().datetime_schema(schema)
+ result.pop("format", None)
+ result["description"] = (
+ "Python ISO 8601 datetime; UTC offset is optional. "
+ "Parseability is enforced by the record decoder, not this schema."
+ )
+ return result
+
@dataclass(kw_only=True)
class PopulationResult:
diff --git a/rampart/core/serialization.py b/rampart/core/serialization.py
new file mode 100644
index 00000000..e08e3617
--- /dev/null
+++ b/rampart/core/serialization.py
@@ -0,0 +1,228 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+"""Versioned ResultRecord envelopes around the canonical Result body codec."""
+
+from __future__ import annotations
+
+import json
+from collections.abc import Mapping
+from dataclasses import dataclass, replace
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ ClassVar,
+)
+
+from rampart.core._schema import json_string
+from rampart.core.errors import SchemaError, UnsupportedSchemaVersionError
+from rampart.core.result import Result
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+ from typing import Never
+
+ from pydantic.json_schema import JsonSchemaValue
+
+
+# Single root schema version stamped on every serialized record.
+TRACE_SCHEMA_VERSION = "rampart.trace.v1"
+
+# Strip only top-level transport bookkeeping, never matching nested user keys.
+_RESERVED_METADATA_KEYS = frozenset(
+ {
+ "_pytest_nodeid",
+ "_pytest_test_name",
+ "_rampart_result_index",
+ "_rampart_source_worker",
+ "_rampart_transport_truncated",
+ "_rampart_original_size_bytes",
+ "_rampart_limit_bytes",
+ "_rampart_worker_format",
+ "_rampart_worker_artifact_path",
+ }
+)
+
+
+@dataclass(frozen=True, kw_only=True)
+class ResultRecord:
+ """A versioned envelope referencing one Result and optional attribution.
+
+ Args:
+ result (Result): The referenced result; its fields are not copied.
+ pytest_nodeid (str | None): Producing test location, when recorded.
+ result_index (int | None): Within-node ordinal, when recorded.
+ """
+
+ VERSION: ClassVar[str] = TRACE_SCHEMA_VERSION
+
+ result: Result
+ pytest_nodeid: str | None = None
+ result_index: int | None = None
+
+ def __post_init__(self) -> None:
+ """Validate attribution without copying or revalidating the live result.
+
+ Raises:
+ SchemaError: If attribution has invalid types.
+ """
+ if self.pytest_nodeid is not None and not isinstance(self.pytest_nodeid, str):
+ msg = "record.pytest_nodeid: expected a string or null"
+ raise SchemaError(msg)
+ if self.pytest_nodeid is not None:
+ try:
+ json_string(value=self.pytest_nodeid, path="record.pytest_nodeid")
+ except ValueError as exc:
+ raise SchemaError(str(exc)) from exc
+ if self.result_index is not None and type(self.result_index) is not int:
+ msg = "record.result_index: expected an integer or null"
+ raise SchemaError(msg)
+
+ def to_dict(self) -> dict[str, Any]:
+ """Convert the envelope to a dict using the single Result body codec.
+
+ Returns:
+ dict[str, Any]: A versioned, JSON-safe record.
+
+ Raises:
+ SchemaError: If the referenced result is outside the trace domain.
+ """
+ if not isinstance(self.result.metadata, Mapping):
+ msg = "result.metadata: expected a mapping"
+ raise SchemaError(msg)
+ metadata = {
+ key: value
+ for key, value in self.result.metadata.items()
+ if key not in _RESERVED_METADATA_KEYS
+ }
+ body = replace(self.result, metadata=metadata).to_dict()
+ encoded: dict[str, Any] = {"version": self.VERSION, "result": body}
+ if self.pytest_nodeid is not None:
+ encoded["pytest_nodeid"] = self.pytest_nodeid
+ if self.result_index is not None:
+ encoded["result_index"] = self.result_index
+ return encoded
+
+ @classmethod
+ def from_dict(cls, data: object) -> ResultRecord:
+ """Dispatch a record to its version-specific decoder.
+
+ Returns:
+ ResultRecord: The reconstructed body and attribution.
+
+ Raises:
+ SchemaError: If the record is not a mapping.
+ UnsupportedSchemaVersionError: If the version is unsupported.
+ """
+ if not isinstance(data, Mapping):
+ msg = "record: expected a mapping"
+ raise SchemaError(msg)
+ version = data.get("version")
+ decoder = _DECODERS.get(version) if isinstance(version, str) else None
+ if decoder is None:
+ msg = f"No decoder registered for trace schema version {version!r}."
+ raise UnsupportedSchemaVersionError(msg)
+ return decoder(data)
+
+ @classmethod
+ def json_schema(cls) -> JsonSchemaValue:
+ """Compose the versioned contract with the adapter-generated body schema.
+
+ Returns:
+ JsonSchemaValue: An open Draft 2020-12 schema.
+ """
+ body = Result.json_schema()
+ definitions = body.pop("$defs", {})
+ return {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": f"urn:rampart:trace:{TRACE_SCHEMA_VERSION.rsplit('.', 1)[-1]}",
+ "$defs": definitions,
+ "title": "ResultRecord",
+ "description": (
+ "Structural trace contract. The record decoder additionally "
+ "requires parseable Python ISO datetimes, finite numbers, "
+ "Unicode scalar strings, "
+ "and integer fields without floating-point notation."
+ ),
+ "type": "object",
+ "additionalProperties": True,
+ "required": ["version", "result"],
+ "properties": {
+ "version": {"type": "string", "const": TRACE_SCHEMA_VERSION},
+ "result": body,
+ "pytest_nodeid": {"type": ["string", "null"]},
+ "result_index": {"type": ["integer", "null"]},
+ },
+ }
+
+
+def serialize_record(*, record: ResultRecord) -> str:
+ """Serialize a canonical record to JSON text.
+
+ Args:
+ record (ResultRecord): The result and its optional attribution.
+
+ Returns:
+ str: JSON text containing the versioned record.
+
+ Raises:
+ SchemaError: If the record cannot be represented as canonical JSON.
+ """
+ data = record.to_dict()
+ try:
+ return json.dumps(data, allow_nan=False)
+ except (ValueError, RecursionError) as exc:
+ msg = f"record: cannot serialize JSON ({exc})"
+ raise SchemaError(msg) from exc
+
+
+def deserialize_record(*, data: str) -> ResultRecord:
+ """Deserialize a canonical record from JSON text.
+
+ Args:
+ data (str): JSON text containing a versioned record.
+
+ Returns:
+ ResultRecord: The result and its attribution.
+
+ Raises:
+ SchemaError: If the input is not JSON text or the record is malformed.
+ UnsupportedSchemaVersionError: If the version is unsupported.
+ """
+ if not isinstance(data, str):
+ msg = "record: expected a JSON string"
+ raise SchemaError(msg)
+ try:
+ decoded = json.loads(data, parse_constant=_reject_json_constant)
+ except (ValueError, RecursionError) as exc:
+ msg = f"record: invalid JSON ({exc})"
+ raise SchemaError(msg) from exc
+ return ResultRecord.from_dict(decoded)
+
+
+def _reject_json_constant(value: str) -> Never:
+ """Reject the non-finite constants accepted by Python's JSON parser.
+
+ Raises:
+ ValueError: Always, because these constants are not valid JSON numbers.
+ """
+ msg = f"non-finite number {value}"
+ raise ValueError(msg)
+
+
+def _decode_v1(data: Mapping[str, Any]) -> ResultRecord:
+ """Reconstruct a v1 envelope through the current body codec.
+
+ Returns:
+ ResultRecord: The reconstructed record.
+ """
+ return ResultRecord(
+ result=Result.from_dict(data.get("result")),
+ pytest_nodeid=data.get("pytest_nodeid"),
+ result_index=data.get("result_index"),
+ )
+
+
+_DECODERS: dict[str, Callable[[Mapping[str, Any]], ResultRecord]] = {
+ TRACE_SCHEMA_VERSION: _decode_v1,
+}
diff --git a/schemas/trace.v1.schema.json b/schemas/trace.v1.schema.json
new file mode 100644
index 00000000..ec08a685
--- /dev/null
+++ b/schemas/trace.v1.schema.json
@@ -0,0 +1,478 @@
+{
+ "$defs": {
+ "EvalOutcome": {
+ "enum": [
+ "detected",
+ "not_detected",
+ "undetermined"
+ ],
+ "title": "EvalOutcome",
+ "type": "string"
+ },
+ "EvalResult": {
+ "additionalProperties": true,
+ "properties": {
+ "confidence": {
+ "default": 1.0,
+ "title": "Confidence",
+ "type": "number"
+ },
+ "evidence": {
+ "items": {
+ "type": "string"
+ },
+ "title": "Evidence",
+ "type": "array"
+ },
+ "outcome": {
+ "$ref": "#/$defs/EvalOutcome"
+ },
+ "rationale": {
+ "default": "",
+ "title": "Rationale",
+ "type": "string"
+ },
+ "undetermined_operands": {
+ "items": {
+ "type": "string"
+ },
+ "title": "Undetermined Operands",
+ "type": "array"
+ }
+ },
+ "required": [
+ "outcome"
+ ],
+ "title": "EvalResult",
+ "type": "object"
+ },
+ "HarmCategory": {
+ "enum": [
+ "memory_poisoning",
+ "prompt_injection",
+ "jailbreak",
+ "data_exfiltration",
+ "over_permissive_action",
+ "data_leakage",
+ "content_safety",
+ "hallucination",
+ "behavioral_regression"
+ ],
+ "title": "HarmCategory",
+ "type": "string"
+ },
+ "InjectionRecord": {
+ "additionalProperties": true,
+ "properties": {
+ "payload_id": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Payload Id"
+ },
+ "surface_name": {
+ "title": "Surface Name",
+ "type": "string"
+ }
+ },
+ "required": [
+ "payload_id",
+ "surface_name"
+ ],
+ "title": "InjectionRecord",
+ "type": "object"
+ },
+ "ObservabilityLevel": {
+ "enum": [
+ "tool_and_side_effects",
+ "tool_only",
+ "response_only"
+ ],
+ "title": "ObservabilityLevel",
+ "type": "string"
+ },
+ "Payload": {
+ "additionalProperties": true,
+ "description": "Recorded text payload. Binary formats and file artifacts are not supported by this trace schema.",
+ "properties": {
+ "artifact": {
+ "default": null,
+ "type": "null"
+ },
+ "content": {
+ "title": "Content",
+ "type": "string"
+ },
+ "format": {
+ "default": "text",
+ "enum": [
+ "text",
+ "html",
+ "markdown"
+ ],
+ "type": "string"
+ },
+ "id": {
+ "title": "Id",
+ "type": "string"
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "title": "Metadata",
+ "type": "object"
+ }
+ },
+ "required": [
+ "content",
+ "id"
+ ],
+ "title": "Payload",
+ "type": "object"
+ },
+ "PopulationRef": {
+ "additionalProperties": true,
+ "properties": {
+ "id": {
+ "title": "Id",
+ "type": "string"
+ },
+ "index": {
+ "title": "Index",
+ "type": "integer"
+ },
+ "size": {
+ "title": "Size",
+ "type": "integer"
+ },
+ "threshold": {
+ "title": "Threshold",
+ "type": "number"
+ }
+ },
+ "required": [
+ "id",
+ "index",
+ "size",
+ "threshold"
+ ],
+ "title": "PopulationRef",
+ "type": "object"
+ },
+ "Request": {
+ "additionalProperties": true,
+ "anyOf": [
+ {
+ "properties": {
+ "prompt": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "prompt"
+ ]
+ },
+ {
+ "properties": {
+ "attachments": {
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "attachments"
+ ]
+ }
+ ],
+ "properties": {
+ "attachments": {
+ "items": {
+ "$ref": "#/$defs/Payload"
+ },
+ "title": "Attachments",
+ "type": "array"
+ },
+ "prompt": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Prompt"
+ }
+ },
+ "title": "Request",
+ "type": "object"
+ },
+ "Response": {
+ "additionalProperties": true,
+ "properties": {
+ "metadata": {
+ "additionalProperties": true,
+ "title": "Metadata",
+ "type": "object"
+ },
+ "side_effects": {
+ "items": {
+ "$ref": "#/$defs/SideEffect"
+ },
+ "title": "Side Effects",
+ "type": "array"
+ },
+ "text": {
+ "title": "Text",
+ "type": "string"
+ },
+ "tool_calls": {
+ "items": {
+ "$ref": "#/$defs/ToolCall"
+ },
+ "title": "Tool Calls",
+ "type": "array"
+ }
+ },
+ "required": [
+ "text"
+ ],
+ "title": "Response",
+ "type": "object"
+ },
+ "SafetyStatus": {
+ "enum": [
+ "safe",
+ "unsafe",
+ "undetermined",
+ "error"
+ ],
+ "title": "SafetyStatus",
+ "type": "string"
+ },
+ "SideEffect": {
+ "additionalProperties": true,
+ "properties": {
+ "details": {
+ "additionalProperties": true,
+ "title": "Details",
+ "type": "object"
+ },
+ "kind": {
+ "title": "Kind",
+ "type": "string"
+ }
+ },
+ "required": [
+ "kind"
+ ],
+ "title": "SideEffect",
+ "type": "object"
+ },
+ "ToolCall": {
+ "additionalProperties": true,
+ "properties": {
+ "arguments": {
+ "additionalProperties": true,
+ "title": "Arguments",
+ "type": "object"
+ },
+ "name": {
+ "title": "Name",
+ "type": "string"
+ },
+ "result": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Result"
+ },
+ "timestamp": {
+ "anyOf": [
+ {
+ "description": "Python ISO 8601 datetime; UTC offset is optional. Parseability is enforced by the record decoder, not this schema.",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Timestamp"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "title": "ToolCall",
+ "type": "object"
+ },
+ "Turn": {
+ "additionalProperties": true,
+ "properties": {
+ "driver_reasoning": {
+ "default": "",
+ "title": "Driver Reasoning",
+ "type": "string"
+ },
+ "eval_result": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/EvalResult"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null
+ },
+ "request": {
+ "$ref": "#/$defs/Request"
+ },
+ "response": {
+ "$ref": "#/$defs/Response"
+ },
+ "timestamp": {
+ "anyOf": [
+ {
+ "description": "Python ISO 8601 datetime; UTC offset is optional. Parseability is enforced by the record decoder, not this schema.",
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Timestamp"
+ },
+ "turn_number": {
+ "default": 0,
+ "title": "Turn Number",
+ "type": "integer"
+ }
+ },
+ "required": [
+ "request",
+ "response"
+ ],
+ "title": "Turn",
+ "type": "object"
+ }
+ },
+ "$id": "urn:rampart:trace:v1",
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": true,
+ "description": "Structural trace contract. The record decoder additionally requires parseable Python ISO datetimes, finite numbers, Unicode scalar strings, and integer fields without floating-point notation.",
+ "properties": {
+ "pytest_nodeid": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "result": {
+ "additionalProperties": true,
+ "properties": {
+ "duration_seconds": {
+ "default": 0.0,
+ "title": "Duration Seconds",
+ "type": "number"
+ },
+ "harm_category": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/HarmCategory"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null,
+ "title": "Harm Category"
+ },
+ "injections": {
+ "items": {
+ "$ref": "#/$defs/InjectionRecord"
+ },
+ "title": "Injections",
+ "type": "array"
+ },
+ "metadata": {
+ "additionalProperties": true,
+ "title": "Metadata",
+ "type": "object"
+ },
+ "observability_level": {
+ "$ref": "#/$defs/ObservabilityLevel"
+ },
+ "population": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/PopulationRef"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "default": null
+ },
+ "status": {
+ "$ref": "#/$defs/SafetyStatus"
+ },
+ "strategy": {
+ "default": "",
+ "title": "Strategy",
+ "type": "string"
+ },
+ "summary": {
+ "title": "Summary",
+ "type": "string"
+ },
+ "turns": {
+ "items": {
+ "$ref": "#/$defs/Turn"
+ },
+ "title": "Turns",
+ "type": "array"
+ }
+ },
+ "required": [
+ "status",
+ "summary",
+ "observability_level"
+ ],
+ "title": "Result",
+ "type": "object"
+ },
+ "result_index": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "version": {
+ "const": "rampart.trace.v1",
+ "type": "string"
+ }
+ },
+ "required": [
+ "version",
+ "result"
+ ],
+ "title": "ResultRecord",
+ "type": "object"
+}
diff --git a/scripts/generate_trace_schema.py b/scripts/generate_trace_schema.py
new file mode 100644
index 00000000..123e175c
--- /dev/null
+++ b/scripts/generate_trace_schema.py
@@ -0,0 +1,40 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+"""Regenerate the checked-in canonical trace schema from the Result adapter."""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+from rampart.core.serialization import ResultRecord
+
+
+def main() -> None:
+ """Write the schema or fail when the committed contract has drifted.
+
+ Raises:
+ SystemExit: If --check finds a missing or outdated schema.
+ """
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true")
+ args = parser.parse_args()
+ path = Path(__file__).resolve().parents[1] / "schemas" / "trace.v1.schema.json"
+ content = json.dumps(ResultRecord.json_schema(), indent=2, sort_keys=True) + "\n"
+ if args.check:
+ if not path.exists() or path.read_text(encoding="utf-8") != content:
+ parser.exit(
+ status=1,
+ message=(
+ "Trace schema is outdated; run scripts/generate_trace_schema.py\n"
+ ),
+ )
+ else:
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text(content, encoding="utf-8", newline="\n")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/unit/core/test_serialization.py b/tests/unit/core/test_serialization.py
new file mode 100644
index 00000000..02af85a5
--- /dev/null
+++ b/tests/unit/core/test_serialization.py
@@ -0,0 +1,1119 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+"""Unit tests for the canonical trace/result serializer."""
+
+from __future__ import annotations
+
+import json
+import math
+import re
+from dataclasses import fields, replace
+from datetime import (
+ UTC,
+ datetime,
+ timedelta,
+ timezone,
+)
+from pathlib import Path
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ TypeVar,
+ get_type_hints,
+)
+from unittest.mock import patch
+
+import pytest
+from jsonschema import Draft202012Validator
+from pydantic import TypeAdapter
+
+from rampart.core import types as core_types
+from rampart.core.result import (
+ InjectionRecord,
+ PopulationRef,
+ Result,
+ SafetyStatus,
+ _result_adapter,
+)
+from rampart.core.serialization import (
+ TRACE_SCHEMA_VERSION,
+ ResultRecord,
+ SchemaError,
+ UnsupportedSchemaVersionError,
+ deserialize_record,
+ serialize_record,
+)
+from rampart.core.types import (
+ EvalOutcome,
+ EvalResult,
+ ObservabilityLevel,
+ Payload,
+ PayloadFormat,
+ Request,
+ Response,
+ SideEffect,
+ ToolCall,
+ Turn,
+)
+
+if TYPE_CHECKING:
+ from collections.abc import MutableMapping
+
+_TIMESTAMP = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC)
+_AdapterType = TypeVar("_AdapterType")
+
+
+def _regular_adapter(cls: type[_AdapterType]) -> TypeAdapter[_AdapterType]:
+ adapter = TypeAdapter(cls)
+ adapter.rebuild(_types_namespace={"datetime": datetime, "Path": Path})
+ return adapter
+
+
+def _make_eval_result() -> EvalResult:
+ return EvalResult(
+ outcome=EvalOutcome.DETECTED,
+ confidence=0.75,
+ evidence=["saw the thing", "and another"],
+ rationale="because reasons",
+ undetermined_operands=["left operand undetermined"],
+ )
+
+
+def _make_turn() -> Turn:
+ request = Request(
+ prompt="do the thing",
+ attachments=[
+ Payload(
+ content="poisoned doc text",
+ id="payload-1",
+ format=PayloadFormat.MARKDOWN,
+ metadata={"persona": "attacker"},
+ ),
+ ],
+ )
+ response = Response(
+ text="agent said this",
+ tool_calls=[
+ ToolCall(
+ name="send_email",
+ arguments={"to": "a@b.com", "nested": {"count": 2}},
+ result="ok",
+ timestamp=_TIMESTAMP,
+ ),
+ ],
+ side_effects=[SideEffect(kind="http_request", details={"url": "http://x"})],
+ metadata={"latency_ms": 12},
+ )
+ return Turn(
+ request=request,
+ response=response,
+ eval_result=_make_eval_result(),
+ turn_number=3,
+ timestamp=_TIMESTAMP,
+ driver_reasoning="escalate",
+ )
+
+
+def _make_full_result(*, metadata: dict | None = None) -> Result:
+ return Result(
+ status=SafetyStatus.UNSAFE,
+ summary="a violation was detected",
+ observability_level=ObservabilityLevel.TOOL_AND_SIDE_EFFECTS,
+ turns=[_make_turn()],
+ duration_seconds=1.5,
+ harm_category="prompt_injection",
+ strategy="xpia",
+ injections=[InjectionRecord(payload_id="payload-1", surface_name="SharePoint")],
+ population=PopulationRef(id="pop-1", index=0, size=5, threshold=0.8),
+ metadata={"note": "user data", "nested": {"k": [1, 2]}}
+ if metadata is None
+ else metadata,
+ )
+
+
+def _minimal_record_dict() -> dict:
+ return {
+ "version": TRACE_SCHEMA_VERSION,
+ "result": {
+ "status": "safe",
+ "summary": "clean",
+ "observability_level": "response_only",
+ },
+ }
+
+
+def _freeform_maps(result: Result) -> list[MutableMapping[str, Any]]:
+ return [
+ result.metadata,
+ result.turns[0].request.attachments[0].metadata,
+ result.turns[0].response.metadata,
+ result.turns[0].response.tool_calls[0].arguments,
+ result.turns[0].response.side_effects[0].details,
+ ]
+
+
+def _nested_json_value(*, depth: int, mapping: bool) -> object:
+ value: object = "leaf"
+ for _ in range(depth):
+ value = {"nested": value} if mapping else [value]
+ return value
+
+
+class TestRoundTrip:
+ def test_full_result_round_trips_to_equal_value(self) -> None:
+ original = ResultRecord(result=_make_full_result())
+ encoded = serialize_record(record=original)
+
+ decoded = deserialize_record(data=encoded)
+
+ assert isinstance(encoded, str)
+ assert json.loads(encoded) == original.to_dict()
+ assert decoded == original
+
+ def test_version_is_stamped_on_the_record(self) -> None:
+ encoded = ResultRecord(result=_make_full_result()).to_dict()
+
+ assert encoded["version"] == TRACE_SCHEMA_VERSION
+ assert ResultRecord.VERSION == "rampart.trace.v1"
+
+ def test_serialize_record_includes_attribution(self) -> None:
+ record = ResultRecord(
+ result=_make_full_result(),
+ pytest_nodeid="tests/test_x.py::test_x",
+ result_index=2,
+ )
+
+ encoded = json.loads(serialize_record(record=record))
+
+ assert encoded["pytest_nodeid"] == "tests/test_x.py::test_x"
+ assert encoded["result_index"] == 2
+
+ def test_serialize_record_omits_attribution_when_unset(self) -> None:
+ record = ResultRecord(result=_make_full_result())
+
+ encoded = json.loads(serialize_record(record=record))
+
+ assert "pytest_nodeid" not in encoded
+ assert "result_index" not in encoded
+
+ @pytest.mark.parametrize("index", [None, 0, 2])
+ def test_attribution_collar_round_trips(self, index: int | None) -> None:
+ record = ResultRecord(
+ result=_make_full_result(),
+ pytest_nodeid="tests/test_x.py::test_x",
+ result_index=index,
+ )
+ encoded = serialize_record(record=record)
+
+ decoded = deserialize_record(data=encoded)
+
+ assert decoded.pytest_nodeid == "tests/test_x.py::test_x"
+ assert decoded.result_index == index
+
+ def test_nested_values_survive_the_round_trip(self) -> None:
+ decoded = deserialize_record(
+ data=serialize_record(record=ResultRecord(result=_make_full_result()))
+ ).result
+
+ turn = decoded.turns[0]
+ assert turn.request.attachments[0].format is PayloadFormat.MARKDOWN
+ assert turn.response.tool_calls[0].arguments == {
+ "to": "a@b.com",
+ "nested": {"count": 2},
+ }
+ assert turn.response.tool_calls[0].timestamp == _TIMESTAMP
+ assert turn.response.side_effects[0].kind == "http_request"
+ assert turn.eval_result is not None
+ assert turn.eval_result.outcome is EvalOutcome.DETECTED
+ assert decoded.injections[0].surface_name == "SharePoint"
+ assert decoded.population == PopulationRef(
+ id="pop-1", index=0, size=5, threshold=0.8
+ )
+
+ def test_unicode_and_escaped_text_round_trip(self) -> None:
+ result = _make_full_result()
+ result.summary = (
+ 'Quoted "text"\nwith backslash \\ and Unicode \u00e9 \U0001f600'
+ )
+ record = ResultRecord(result=result)
+
+ encoded = serialize_record(record=record)
+
+ assert json.loads(encoded)["result"]["summary"] == result.summary
+ assert deserialize_record(data=encoded) == record
+
+
+class TestJsonTextBoundary:
+ @pytest.mark.parametrize(
+ "data", ["", "{", '{"version":', "{} trailing", "{'key': 1}"]
+ )
+ def test_malformed_json_raises_schema_error(self, data: str) -> None:
+ with pytest.raises(SchemaError, match="record: invalid JSON") as error:
+ deserialize_record(data=data)
+
+ assert isinstance(error.value.__cause__, json.JSONDecodeError)
+
+ @pytest.mark.parametrize("data", [{}, [], None, 1, b"{}"])
+ def test_deserialization_requires_text(self, data: Any) -> None:
+ with pytest.raises(SchemaError, match="record: expected a JSON string"):
+ deserialize_record(data=data)
+
+ @pytest.mark.parametrize("value", [math.nan, math.inf, -math.inf])
+ def test_nonfinite_json_constants_are_rejected(self, value: float) -> None:
+ data = json.dumps({**_minimal_record_dict(), "future": value})
+
+ with pytest.raises(SchemaError, match=r"record: invalid JSON.*non-finite"):
+ deserialize_record(data=data)
+
+ def test_serialization_preserves_metadata_policy(self) -> None:
+ result = _make_full_result(
+ metadata={
+ "_rampart_source_worker": object(),
+ "nested": {"_rampart_source_worker": "keep"},
+ }
+ )
+
+ encoded = serialize_record(record=ResultRecord(result=result))
+
+ assert json.loads(encoded)["result"]["metadata"] == {
+ "nested": {"_rampart_source_worker": "keep"}
+ }
+ assert "_rampart_source_worker" in result.metadata
+
+ def test_serialization_rejects_non_json_values(self) -> None:
+ record = ResultRecord(result=_make_full_result(metadata={"bad": math.nan}))
+
+ with pytest.raises(SchemaError, match="metadata"):
+ serialize_record(record=record)
+
+
+class TestUnicodeDomain:
+ _INVALID_TEXT = (
+ pytest.param(chr(0xD800), id="high-surrogate"),
+ pytest.param(chr(0xDFFF), id="low-surrogate"),
+ pytest.param(chr(0xD83D) + chr(0xDE00), id="surrogate-pair"),
+ )
+
+ @pytest.mark.parametrize("text", _INVALID_TEXT)
+ @pytest.mark.parametrize("nested", [False, True])
+ def test_surrogates_in_typed_text_are_rejected(
+ self, *, text: str, nested: bool
+ ) -> None:
+ result = _make_full_result()
+ body = result.to_dict()
+ field = "text" if nested else "summary"
+ target = result.turns[0].response if nested else result
+ body_target = body["turns"][0]["response"] if nested else body
+ setattr(target, field, text)
+ body_target[field] = text
+
+ with pytest.raises(SchemaError, match=rf"{field}.*surrogate"):
+ result.to_dict()
+ with pytest.raises(SchemaError, match=rf"{field}.*surrogate"):
+ Result.from_dict(body)
+ with pytest.raises(SchemaError, match=rf"{field}.*surrogate"):
+ serialize_record(record=ResultRecord(result=result))
+
+ @pytest.mark.parametrize("text", _INVALID_TEXT)
+ @pytest.mark.parametrize("map_index", range(5))
+ @pytest.mark.parametrize("as_key", [False, True])
+ def test_surrogates_in_freeform_keys_and_values_are_rejected(
+ self, *, text: str, map_index: int, as_key: bool
+ ) -> None:
+ result = _make_full_result()
+ _freeform_maps(result)[map_index]["nested"] = (
+ {text: "value"} if as_key else {"text": text}
+ )
+
+ with pytest.raises(SchemaError, match=r"nested.*surrogate") as error:
+ result.to_dict()
+
+ assert text not in str(error.value)
+
+ @pytest.mark.parametrize("text", _INVALID_TEXT)
+ def test_surrogate_attribution_is_rejected(self, text: str) -> None:
+ data = _minimal_record_dict()
+ data["pytest_nodeid"] = text
+
+ with pytest.raises(SchemaError, match=r"record\.pytest_nodeid.*surrogate"):
+ ResultRecord(result=_make_full_result(), pytest_nodeid=text)
+ with pytest.raises(SchemaError, match=r"record\.pytest_nodeid.*surrogate"):
+ ResultRecord.from_dict(data)
+
+ @pytest.mark.parametrize("text", _INVALID_TEXT[:2])
+ def test_unpaired_json_surrogate_escapes_are_rejected(self, text: str) -> None:
+ data = _minimal_record_dict()
+ data["result"]["summary"] = text
+
+ with pytest.raises(SchemaError, match=r"summary.*surrogate"):
+ deserialize_record(data=json.dumps(data))
+
+ def test_unicode_scalars_and_valid_json_surrogate_pairs_round_trip(self) -> None:
+ text = "\u00e9\U0001f600"
+ result = _make_full_result(metadata={text: text})
+ result.summary = text
+ record = ResultRecord(result=result, pytest_nodeid=text)
+
+ encoded = serialize_record(record=record)
+
+ assert r"\ud83d\ude00" in encoded
+ assert deserialize_record(data=encoded) == record
+
+
+class TestFieldExhaustiveness:
+ def test_every_field_of_every_type_is_serialized(self) -> None:
+ body = ResultRecord(result=_make_full_result()).to_dict()["result"]
+ turn = body["turns"][0]
+
+ cases = [
+ (Result, body),
+ (Turn, turn),
+ (Request, turn["request"]),
+ (Payload, turn["request"]["attachments"][0]),
+ (Response, turn["response"]),
+ (ToolCall, turn["response"]["tool_calls"][0]),
+ (SideEffect, turn["response"]["side_effects"][0]),
+ (EvalResult, turn["eval_result"]),
+ (InjectionRecord, body["injections"][0]),
+ (PopulationRef, body["population"]),
+ ]
+
+ for dataclass_type, encoded in cases:
+ expected = {field.name for field in fields(dataclass_type)}
+ assert expected == set(encoded), dataclass_type.__name__
+
+
+class TestVersionDispatch:
+ def test_unknown_major_fails_closed(self) -> None:
+ data = {"version": "rampart.trace.v2", "result": {}}
+
+ with pytest.raises(UnsupportedSchemaVersionError, match="v2"):
+ deserialize_record(data=json.dumps(data))
+
+ def test_missing_version_fails_closed(self) -> None:
+ with pytest.raises(UnsupportedSchemaVersionError):
+ deserialize_record(data='{"result": {}}')
+
+ @pytest.mark.parametrize("data", ["[1, 2, 3]", "null", "1", '"text"'])
+ def test_non_mapping_record_fails_closed(self, data: str) -> None:
+ with pytest.raises(SchemaError, match="mapping"):
+ deserialize_record(data=data)
+
+
+class TestMigrationTolerance:
+ def test_unknown_extra_fields_decode(self) -> None:
+ encoded = ResultRecord(result=_make_full_result()).to_dict()
+ encoded["future_collar"] = {"anything": True}
+ encoded["result"]["future_intrinsic"] = 42
+
+ decoded = deserialize_record(data=json.dumps(encoded)).result
+
+ assert decoded.status is SafetyStatus.UNSAFE
+
+ def test_missing_optional_fields_use_defaults(self) -> None:
+ decoded = deserialize_record(data=json.dumps(_minimal_record_dict())).result
+
+ assert decoded.status is SafetyStatus.SAFE
+ assert decoded.turns == []
+ assert decoded.duration_seconds == pytest.approx(0.0)
+ assert decoded.harm_category is None
+ assert decoded.injections == []
+ assert decoded.population is None
+ assert decoded.metadata == {}
+
+ def test_malformed_present_list_fails_closed(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["turns"] = "not-a-list"
+
+ with pytest.raises(SchemaError, match=r"result\.turns"):
+ ResultRecord.from_dict(data)
+
+ def test_incomplete_population_reference_fails_closed(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["population"] = {}
+
+ with pytest.raises(SchemaError, match=r"result\.population\.id"):
+ ResultRecord.from_dict(data)
+
+
+class TestValueDomain:
+ def test_reserved_metadata_keys_are_stripped(self) -> None:
+ result = _make_full_result(
+ metadata={"_pytest_nodeid": "x::y", "note": "keep me"},
+ )
+
+ encoded = ResultRecord(result=result).to_dict()
+
+ assert encoded["result"]["metadata"] == {"note": "keep me"}
+
+ def test_harm_category_is_passed_through_as_string(self) -> None:
+ result = _make_full_result()
+ result.harm_category = "custom_product_risk"
+
+ encoded = ResultRecord(result=result).to_dict()
+ decoded = ResultRecord.from_dict(encoded).result
+
+ assert encoded["result"]["harm_category"] == "custom_product_risk"
+ assert decoded.harm_category == "custom_product_risk"
+
+ def test_non_finite_float_fails_closed(self) -> None:
+ result = _make_full_result()
+ result.duration_seconds = math.inf
+
+ with pytest.raises(SchemaError, match="duration_seconds"):
+ ResultRecord(result=result).to_dict()
+
+ def test_non_json_metadata_fails_closed(self) -> None:
+ result = _make_full_result(metadata={"blob": object()})
+
+ with pytest.raises(SchemaError, match="metadata"):
+ ResultRecord(result=result).to_dict()
+
+ def test_bad_enum_value_fails_closed_on_decode(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["status"] = "not_a_status"
+
+ with pytest.raises(SchemaError, match="status"):
+ ResultRecord.from_dict(data)
+
+ def test_non_string_harm_category_fails_closed_on_encode(self) -> None:
+ result = _make_full_result()
+ result.__dict__["harm_category"] = 42
+
+ with pytest.raises(SchemaError, match="harm_category"):
+ ResultRecord(result=result).to_dict()
+
+ def test_non_string_harm_category_fails_closed_on_decode(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["harm_category"] = {"category": "custom"}
+
+ with pytest.raises(SchemaError, match="harm_category"):
+ ResultRecord.from_dict(data)
+
+ def test_boolean_result_index_fails_before_encoding(self) -> None:
+ with pytest.raises(SchemaError, match="result_index"):
+ ResultRecord(result=_make_full_result(), result_index=True)
+
+
+class TestBinaryPayloadFailsClosed:
+ def test_encoding_a_binary_payload_fails_closed(self, tmp_path) -> None:
+ artifact = tmp_path / "doc.pdf"
+ artifact.write_bytes(b"%PDF-1.4 fake")
+ result = _make_full_result()
+ result.turns = [
+ Turn(
+ request=Request(
+ attachments=[
+ Payload(
+ content="binary doc",
+ format=PayloadFormat.PDF,
+ artifact=artifact,
+ ),
+ ],
+ ),
+ response=Response(text="ok"),
+ ),
+ ]
+
+ with pytest.raises(SchemaError, match="binary payload"):
+ ResultRecord(result=result).to_dict()
+
+ def test_decoding_a_binary_payload_fails_closed(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["turns"] = [
+ {
+ "request": {
+ "prompt": None,
+ "attachments": [{"content": "x", "id": "p", "format": "pdf"}],
+ },
+ "response": {"text": "ok"},
+ },
+ ]
+
+ with pytest.raises(SchemaError, match="binary payload"):
+ ResultRecord.from_dict(data)
+
+ @pytest.mark.parametrize("payload_format", ["pdf", "docx", "text"])
+ def test_artifact_is_rejected_before_filesystem_access(
+ self, payload_format: str
+ ) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ payload = data["result"]["turns"][0]["request"]["attachments"][0]
+ payload.update(format=payload_format, artifact="untrusted-artifact")
+
+ with (
+ patch.object(
+ Path, "exists", side_effect=AssertionError("filesystem access")
+ ),
+ pytest.raises(SchemaError, match="artifact"),
+ ):
+ ResultRecord.from_dict(data)
+
+ def test_live_binary_payload_is_still_supported(self, tmp_path: Path) -> None:
+ artifact = tmp_path / "doc.pdf"
+ artifact.write_bytes(b"%PDF-1.4 fake")
+ payload = Payload(content="doc", format=PayloadFormat.PDF, artifact=artifact)
+
+ assert _regular_adapter(Payload).validate_python(payload) is payload
+ assert payload.artifact == artifact
+
+
+class TestResultAdapter:
+ def test_body_methods_round_trip_through_json(self) -> None:
+ original = _make_full_result()
+
+ body = original.to_dict()
+ restored = Result.from_dict(json.loads(json.dumps(body, allow_nan=False)))
+
+ assert restored == original
+ assert isinstance(restored.turns[0], Turn)
+ assert "version" not in body
+ assert body["turns"][0]["timestamp"] == _TIMESTAMP.isoformat()
+ assert body["turns"][0]["response"]["tool_calls"][0]["timestamp"] == (
+ _TIMESTAMP.isoformat()
+ )
+
+ @pytest.mark.parametrize(
+ "timestamp",
+ [
+ _TIMESTAMP,
+ _TIMESTAMP.replace(tzinfo=None),
+ _TIMESTAMP.replace(tzinfo=timezone(timedelta(seconds=30))),
+ _TIMESTAMP.replace(tzinfo=timezone(timedelta(hours=-5))),
+ ],
+ )
+ def test_python_iso_datetimes_preserve_their_wire_text(
+ self, timestamp: datetime
+ ) -> None:
+ result = _make_full_result()
+ result.turns[0].__dict__["timestamp"] = timestamp
+ result.turns[0].response.tool_calls[0].timestamp = timestamp
+
+ encoded = ResultRecord(result=result).to_dict()
+
+ assert encoded["result"]["turns"][0]["timestamp"] == timestamp.isoformat()
+ assert ResultRecord.from_dict(encoded).result == result
+ Draft202012Validator(
+ ResultRecord.json_schema(),
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
+ ).validate(encoded)
+
+ def test_record_filters_only_top_level_metadata_without_mutation(self) -> None:
+ original = _make_full_result(
+ metadata={
+ "_rampart_source_worker": "gw0",
+ "_pytest_nodeid": "test",
+ "_rampart_worker_artifact_path": object(),
+ "user": {"_rampart_source_worker": "keep"},
+ }
+ )
+ record = ResultRecord(result=original)
+ original.summary = "updated after wrapping"
+
+ body = record.to_dict()["result"]
+ body["metadata"]["user"]["extra"] = True
+
+ assert record.result is original
+ assert body["summary"] == original.summary
+ assert body["metadata"] == {
+ "user": {"_rampart_source_worker": "keep", "extra": True}
+ }
+ assert original.metadata["user"] == {"_rampart_source_worker": "keep"}
+ assert "_rampart_worker_artifact_path" in original.metadata
+
+ def test_body_does_not_own_transport_filtering(self) -> None:
+ result = _make_full_result(metadata={"_rampart_source_worker": "gw0"})
+
+ assert result.to_dict()["metadata"] == result.metadata
+ assert ResultRecord(result=result).to_dict()["result"]["metadata"] == {}
+
+ @pytest.mark.parametrize("index", [None, 0, 2])
+ def test_optional_attribution_is_not_inferred(self, index: int | None) -> None:
+ record = ResultRecord(result=_make_full_result(), result_index=index)
+
+ encoded = record.to_dict()
+
+ assert ResultRecord.from_dict(encoded).result_index == index
+ assert ("result_index" in encoded) is (index is not None)
+
+ @pytest.mark.parametrize("nodeid", [False, 1, [], {}])
+ def test_invalid_nodeid_is_rejected(self, nodeid: object) -> None:
+ data = _minimal_record_dict()
+ data["pytest_nodeid"] = nodeid
+
+ with pytest.raises(SchemaError, match="pytest_nodeid"):
+ ResultRecord.from_dict(data)
+
+ def test_nested_mutations_are_revalidated(self) -> None:
+ result = _make_full_result()
+ result.turns[0].response.__dict__["text"] = 42
+
+ with pytest.raises(SchemaError, match=r"result\.turns\[0\]\.response\.text"):
+ result.to_dict()
+
+ @pytest.mark.parametrize("invalid", [True, 1.5, "1"])
+ def test_integer_fields_are_not_coerced(self, invalid: object) -> None:
+ result = _make_full_result()
+ assert result.population is not None
+ result.population.__dict__["index"] = invalid
+
+ with pytest.raises(SchemaError, match=r"result\.population\.index"):
+ result.to_dict()
+
+ def test_missing_payload_identity_is_not_generated(self) -> None:
+ body = _make_full_result().to_dict()
+ del body["turns"][0]["request"]["attachments"][0]["id"]
+
+ with pytest.raises(SchemaError, match="id"):
+ Result.from_dict(body)
+
+ def test_invalid_timestamp_is_rejected(self) -> None:
+ body = _make_full_result().to_dict()
+ body["turns"][0]["timestamp"] = "not a date"
+
+ with pytest.raises(SchemaError, match=r"result\.turns\[0\]\.timestamp"):
+ Result.from_dict(body)
+
+ @pytest.mark.parametrize(
+ "field", ["turns", "injections", "metadata", "duration_seconds"]
+ )
+ def test_null_is_not_a_default_for_nonnullable_fields(self, field: str) -> None:
+ body = _make_full_result().to_dict()
+ body[field] = None
+
+ with pytest.raises(SchemaError, match=field):
+ Result.from_dict(body)
+
+
+class TestAdapterIsolation:
+ def test_cold_adapter_resolves_types_without_changing_their_module(self) -> None:
+ _result_adapter.cache_clear()
+
+ restored = ResultRecord.from_dict(_minimal_record_dict())
+ restored.to_dict()
+ ResultRecord.json_schema()
+
+ assert "datetime" not in vars(core_types)
+ assert "Path" not in vars(core_types)
+
+ def test_public_annotations_remain_standard_types(self) -> None:
+ namespace = {"datetime": datetime, "Path": Path}
+ for cls, name in [
+ (Result, "metadata"),
+ (Payload, "metadata"),
+ (Response, "metadata"),
+ (ToolCall, "arguments"),
+ (SideEffect, "details"),
+ ]:
+ assert (
+ get_type_hints(cls, localns=namespace, include_extras=True)[name]
+ == (dict[str, Any])
+ )
+ for cls in [ToolCall, Turn]:
+ assert (
+ get_type_hints(cls, localns=namespace, include_extras=True)["timestamp"]
+ == datetime | None
+ )
+ assert not hasattr(Result, "__pydantic_config__")
+
+ def test_regular_adapters_are_unchanged_before_and_after_canonical_use(
+ self,
+ ) -> None:
+ adapter = _regular_adapter(Result)
+ original_schema = adapter.json_schema()
+ result = _make_full_result(metadata={"tuple": (1, 2), "opaque": object()})
+
+ with pytest.raises(SchemaError, match="metadata"):
+ result.to_dict()
+ Result.json_schema()
+
+ assert adapter.validate_python(result) is result
+ assert _regular_adapter(Result).validate_python(result) is result
+ assert adapter.json_schema() == original_schema
+ assert _regular_adapter(Result).json_schema() == original_schema
+
+ def test_regular_adapter_can_still_generate_payload_ids(self) -> None:
+ _make_full_result().to_dict()
+
+ payload = _regular_adapter(Payload).validate_python(
+ {"content": "live", "metadata": {"tuple": (1, 2)}}
+ )
+
+ assert payload.id
+ assert payload.metadata["tuple"] == (1, 2)
+
+ def test_regular_adapter_retains_pydantic_datetime_behavior(self) -> None:
+ result = _make_full_result()
+
+ canonical = result.to_dict()
+ regular = _regular_adapter(Result).dump_python(result, mode="json")
+
+ assert canonical["turns"][0]["timestamp"].endswith("+00:00")
+ assert regular["turns"][0]["timestamp"].endswith("Z")
+
+ def test_regular_adapter_can_decode_live_binary_payloads(
+ self, tmp_path: Path
+ ) -> None:
+ artifact = tmp_path / "document.pdf"
+ artifact.write_bytes(b"%PDF-1.4 fake")
+ _make_full_result().to_dict()
+
+ payload = _regular_adapter(Payload).validate_python(
+ {"content": "doc", "format": "pdf", "artifact": str(artifact)}
+ )
+
+ assert payload.format is PayloadFormat.PDF
+ assert payload.artifact == artifact
+
+ def test_reused_nested_schemas_still_validate_all_instances(self) -> None:
+ result = _make_full_result()
+ result.turns.append(_make_turn())
+ result.turns[1].request.attachments[0].metadata["bad"] = (1, 2)
+
+ with pytest.raises(SchemaError, match=r"turns\[1\].*metadata"):
+ result.to_dict()
+
+ def test_nested_numeric_fields_are_still_finite(self) -> None:
+ result = _make_full_result()
+ assert result.turns[0].eval_result is not None
+ result.turns[0].eval_result.confidence = math.inf
+
+ with pytest.raises(SchemaError, match="confidence"):
+ result.to_dict()
+
+
+class TestTransportPreparationBoundary:
+ def test_prepared_copy_does_not_relax_the_original_record(
+ self, tmp_path: Path
+ ) -> None:
+ artifact = tmp_path / "worker.pdf"
+ artifact.write_bytes(b"%PDF-1.4 fake")
+ original = _make_full_result(metadata={"tuple": (1, 2)})
+ binary = Payload(
+ content="document text", format=PayloadFormat.PDF, artifact=artifact
+ )
+ original.turns[0].request.attachments = [binary]
+ with pytest.raises(SchemaError):
+ serialize_record(record=ResultRecord(result=original))
+
+ display_payload = replace(
+ binary,
+ format=PayloadFormat.TEXT,
+ artifact=None,
+ metadata={
+ "_rampart_worker_format": "pdf",
+ "_rampart_worker_artifact_path": str(artifact),
+ },
+ )
+ prepared = replace(
+ original,
+ metadata={"tuple": [1, 2]},
+ turns=[
+ replace(
+ original.turns[0],
+ request=replace(
+ original.turns[0].request, attachments=[display_payload]
+ ),
+ )
+ ],
+ )
+
+ restored = deserialize_record(
+ data=serialize_record(record=ResultRecord(result=prepared))
+ ).result
+
+ assert restored == prepared
+ assert original.metadata["tuple"] == (1, 2)
+ assert original.turns[0].request.attachments[0] is binary
+ assert binary.format is PayloadFormat.PDF
+ assert binary.artifact == artifact
+ with pytest.raises(SchemaError):
+ serialize_record(record=ResultRecord(result=original))
+
+
+class TestJsonValueDomain:
+ @pytest.mark.parametrize("map_index", range(5))
+ @pytest.mark.parametrize(
+ "invalid",
+ [
+ pytest.param((1, 2), id="tuple"),
+ pytest.param(b"bytes", id="bytes"),
+ pytest.param(Path("file"), id="path"),
+ pytest.param(object(), id="opaque"),
+ pytest.param(math.inf, id="infinity"),
+ pytest.param(-math.inf, id="negative-infinity"),
+ pytest.param(math.nan, id="nan"),
+ pytest.param({1: "non-string key"}, id="non-string-key"),
+ ],
+ )
+ def test_freeform_values_are_not_lossily_encoded(
+ self, *, map_index: int, invalid: object
+ ) -> None:
+ result = _make_full_result()
+ _freeform_maps(result)[map_index]["nested"] = {"bad": invalid}
+
+ with pytest.raises(SchemaError, match="nested"):
+ result.to_dict()
+
+ @pytest.mark.parametrize(
+ "invalid",
+ [(1, 2), b"bytes", Path("file"), object(), math.inf, math.nan, {1: "x"}],
+ )
+ def test_dictionary_input_is_checked_before_json_encoding(
+ self, invalid: object
+ ) -> None:
+ body = _make_full_result().to_dict()
+ body["metadata"]["bad"] = invalid
+
+ with pytest.raises(SchemaError, match="metadata"):
+ Result.from_dict(body)
+
+ def test_cyclic_values_fail_with_a_field_path(self) -> None:
+ result = _make_full_result()
+ result.metadata["cycle"] = result.metadata
+
+ with pytest.raises(SchemaError, match=r"metadata.*cycle"):
+ result.to_dict()
+ body = _minimal_record_dict()["result"]
+ body["metadata"] = result.metadata
+ with pytest.raises(SchemaError, match=r"metadata.*cycle"):
+ Result.from_dict(body)
+
+ def test_supported_values_round_trip_without_mutation(self) -> None:
+ metadata = {
+ "values": [None, True, False, 0, -(2**80), 2**80, 1.25, "text"],
+ "nested": {"list": [{"text": "hello"}]},
+ }
+ result = _make_full_result(metadata=metadata)
+
+ restored = Result.from_dict(result.to_dict())
+ restored.metadata["nested"]["list"][0]["text"] = "changed"
+
+ assert result.metadata == metadata
+ assert metadata["nested"]["list"][0]["text"] == "hello"
+ assert restored.metadata["values"] == metadata["values"]
+
+
+class TestJsonNesting:
+ @pytest.mark.parametrize("depth", [100, 180])
+ @pytest.mark.parametrize("mapping", [False, True])
+ @pytest.mark.parametrize("map_index", range(5))
+ def test_deep_supported_values_round_trip(
+ self, *, depth: int, mapping: bool, map_index: int
+ ) -> None:
+ result = _make_full_result()
+ _freeform_maps(result)[map_index]["deep"] = _nested_json_value(
+ depth=depth, mapping=mapping
+ )
+ record = ResultRecord(result=result)
+
+ body = result.to_dict()
+ encoded = serialize_record(record=record)
+ restored = deserialize_record(data=encoded)
+
+ assert Result.from_dict(body) == result
+ assert restored == record
+ assert serialize_record(record=restored) == encoded
+
+ def test_deep_external_record_can_be_reencoded(self) -> None:
+ data = _minimal_record_dict()
+ data["result"]["metadata"] = {
+ "deep": _nested_json_value(depth=100, mapping=False)
+ }
+
+ record = deserialize_record(data=json.dumps(data))
+ encoded = serialize_record(record=record)
+
+ assert json.loads(encoded)["result"]["metadata"] == data["result"]["metadata"]
+
+ @pytest.mark.parametrize("mapping", [False, True])
+ def test_parser_depth_failures_raise_schema_error_on_both_boundaries(
+ self, *, mapping: bool
+ ) -> None:
+ value = _nested_json_value(depth=250, mapping=mapping)
+ result = _make_full_result(metadata={"deep": value})
+ data = _minimal_record_dict()
+ data["result"]["metadata"] = result.metadata
+
+ with pytest.raises(SchemaError, match=r"recursion|depth"):
+ result.to_dict()
+ with pytest.raises(SchemaError, match=r"recursion|depth"):
+ ResultRecord.from_dict(data)
+ with pytest.raises(SchemaError, match=r"recursion|depth"):
+ serialize_record(record=ResultRecord(result=result))
+ with pytest.raises(SchemaError, match=r"recursion|depth"):
+ deserialize_record(data=json.dumps(data))
+
+ def test_adapter_serialization_value_error_is_wrapped(self) -> None:
+ original_error = ValueError("Circular reference detected (depth exceeded)")
+ with (
+ patch.object(_result_adapter(), "dump_python", side_effect=original_error),
+ pytest.raises(SchemaError, match=r"cannot serialize.*ValueError") as error,
+ ):
+ _make_full_result().to_dict()
+
+ assert error.value.__cause__ is original_error
+
+
+class TestGeneratedSchema:
+ def test_generated_schema_is_valid(self) -> None:
+ schema = ResultRecord.json_schema()
+
+ Draft202012Validator.check_schema(schema)
+
+ assert schema["properties"]["version"]["const"] == TRACE_SCHEMA_VERSION
+
+ def test_schema_omits_runtime_class_documentation(self) -> None:
+ schema = ResultRecord.json_schema()
+
+ assert "description" not in schema["properties"]["result"]
+ assert "description" not in schema["$defs"]["SafetyStatus"]
+ assert "not supported" in schema["$defs"]["Payload"]["description"]
+ assert "Args:" not in json.dumps(schema)
+
+ @pytest.mark.parametrize(
+ ("path", "value"),
+ [
+ (("result_index",), 0.0),
+ (("result", "population", "index"), 0.0),
+ (("result", "turns", 0, "timestamp"), "not-a-date"),
+ ],
+ )
+ def test_structural_validation_does_not_replace_decoder_semantics(
+ self, *, path: tuple[str | int, ...], value: object
+ ) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ parent: Any = data
+ for key in path[:-1]:
+ parent = parent[key]
+ parent[path[-1]] = value
+ validator = Draft202012Validator(
+ ResultRecord.json_schema(),
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
+ )
+
+ validator.validate(data)
+ with pytest.raises(SchemaError, match=re.escape(str(path[-1]))):
+ deserialize_record(data=json.dumps(data))
+
+ @pytest.mark.parametrize("payload_format", list(PayloadFormat))
+ def test_schema_and_decoder_agree_on_payload_formats(
+ self, payload_format: PayloadFormat
+ ) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ data["result"]["turns"][0]["request"]["attachments"][0]["format"] = (
+ payload_format.value
+ )
+ validator = Draft202012Validator(ResultRecord.json_schema())
+
+ assert validator.is_valid(data) is payload_format.is_text
+ if payload_format.is_text:
+ assert ResultRecord.from_dict(data).result.turns[0].request.attachments
+ else:
+ with pytest.raises(SchemaError, match="binary payload"):
+ ResultRecord.from_dict(data)
+
+ @pytest.mark.parametrize("full", [False, True])
+ def test_full_and_minimal_records_conform(self, *, full: bool) -> None:
+ data = (
+ json.loads(
+ serialize_record(
+ record=ResultRecord(result=_make_full_result(), result_index=0)
+ )
+ )
+ if full
+ else _minimal_record_dict()
+ )
+ validator = Draft202012Validator(
+ ResultRecord.json_schema(),
+ format_checker=Draft202012Validator.FORMAT_CHECKER,
+ )
+
+ validator.validate(data)
+ validator.validate(ResultRecord.from_dict(data).to_dict())
+
+ def test_unknown_additive_fields_are_allowed_at_every_level(self) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ body = data["result"]
+ turn = body["turns"][0]
+ objects = [
+ data,
+ body,
+ turn,
+ turn["request"],
+ turn["request"]["attachments"][0],
+ turn["response"],
+ turn["response"]["tool_calls"][0],
+ turn["response"]["side_effects"][0],
+ turn["eval_result"],
+ body["injections"][0],
+ body["population"],
+ ]
+ for item in objects:
+ item["future"] = {"recorded": True}
+
+ Draft202012Validator(ResultRecord.json_schema()).validate(data)
+ assert ResultRecord.from_dict(data).result == _make_full_result()
+
+ @pytest.mark.parametrize(
+ ("path", "invalid"),
+ [
+ (("result", "summary"), 123),
+ (("result", "population", "index"), True),
+ (("result", "population", "threshold"), "0.8"),
+ (("result", "turns", 0, "request", "prompt"), 123),
+ (("result", "turns", 0, "timestamp"), False),
+ (("result", "turns", 0, "response", "text"), None),
+ (("result", "turns", 0, "response", "tool_calls", 0, "result"), 123),
+ (("result", "turns", 0, "request", "attachments", 0, "artifact"), "file"),
+ (("result", "turns", 0, "request", "attachments", 0, "format"), "unknown"),
+ (("result", "turns", 0, "eval_result", "outcome"), "unknown"),
+ (("result", "injections", 0, "payload_id"), 123),
+ (("pytest_nodeid",), 123),
+ (("result_index",), True),
+ ],
+ )
+ def test_schema_and_decoder_reject_malformed_fields(
+ self, *, path: tuple[str | int, ...], invalid: object
+ ) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ parent: Any = data
+ for key in path[:-1]:
+ parent = parent[key]
+ parent[path[-1]] = invalid
+
+ assert not Draft202012Validator(ResultRecord.json_schema()).is_valid(data)
+ with pytest.raises(SchemaError, match=re.escape(str(path[-1]))):
+ ResultRecord.from_dict(data)
+
+ @pytest.mark.parametrize(
+ ("prompt", "attachments", "valid"),
+ [
+ (None, False, False),
+ ("", False, True),
+ ("text", False, True),
+ (None, True, True),
+ ],
+ )
+ def test_request_invariant_is_in_schema(
+ self, *, prompt: str | None, attachments: bool, valid: bool
+ ) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ request = data["result"]["turns"][0]["request"]
+ request["prompt"] = prompt
+ if not attachments:
+ request["attachments"] = []
+
+ assert Draft202012Validator(ResultRecord.json_schema()).is_valid(data) is valid
+ if valid:
+ ResultRecord.from_dict(data)
+ else:
+ with pytest.raises(SchemaError, match="request"):
+ ResultRecord.from_dict(data)
+
+ def test_schema_requires_recorded_payload_id(self) -> None:
+ data = ResultRecord(result=_make_full_result()).to_dict()
+ del data["result"]["turns"][0]["request"]["attachments"][0]["id"]
+
+ assert not Draft202012Validator(ResultRecord.json_schema()).is_valid(data)
diff --git a/tests/unit/core/test_serialization_properties.py b/tests/unit/core/test_serialization_properties.py
new file mode 100644
index 00000000..7ba94f0c
--- /dev/null
+++ b/tests/unit/core/test_serialization_properties.py
@@ -0,0 +1,194 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT license.
+
+"""Generated round-trips over the canonical trace value domain."""
+
+from __future__ import annotations
+
+import json
+from dataclasses import replace
+from datetime import timedelta, timezone
+from typing import TYPE_CHECKING, Any
+
+from hypothesis import given
+from hypothesis import strategies as st
+from jsonschema import Draft202012Validator
+
+from rampart.core.result import (
+ HarmCategory,
+ InjectionRecord,
+ PopulationRef,
+ Result,
+ SafetyStatus,
+)
+from rampart.core.serialization import (
+ ResultRecord,
+ deserialize_record,
+ serialize_record,
+)
+from rampart.core.types import (
+ EvalOutcome,
+ EvalResult,
+ ObservabilityLevel,
+ Payload,
+ PayloadFormat,
+ Request,
+ Response,
+ SideEffect,
+ ToolCall,
+ Turn,
+)
+
+if TYPE_CHECKING:
+ from datetime import datetime
+
+ from hypothesis.strategies import SearchStrategy
+
+
+def _json_maps() -> SearchStrategy[dict[str, Any]]:
+ values = st.recursive(
+ st.none()
+ | st.booleans()
+ | st.integers(min_value=-(2**128), max_value=2**128)
+ | st.floats(allow_nan=False, allow_infinity=False)
+ | st.text(max_size=40),
+ lambda children: (
+ st.lists(children, max_size=4)
+ | st.dictionaries(st.text(max_size=20), children, max_size=4)
+ ),
+ max_leaves=10,
+ )
+ return st.dictionaries(st.text(max_size=20), values, max_size=4)
+
+
+def _timestamps() -> SearchStrategy[datetime | None]:
+ zones = st.none() | st.integers(-86399, 86399).map(
+ lambda seconds: timezone(timedelta(seconds=seconds))
+ )
+ return st.none() | st.datetimes(timezones=zones)
+
+
+def _payloads() -> SearchStrategy[Payload]:
+ return st.builds(
+ Payload,
+ content=st.text(max_size=100),
+ id=st.text(max_size=30),
+ format=st.sampled_from([value for value in PayloadFormat if value.is_text]),
+ metadata=_json_maps(),
+ )
+
+
+def _requests() -> SearchStrategy[Request]:
+ return st.one_of(
+ st.builds(
+ Request,
+ prompt=st.text(max_size=100),
+ attachments=st.lists(_payloads(), max_size=2),
+ ),
+ st.builds(
+ Request,
+ prompt=st.none(),
+ attachments=st.lists(_payloads(), min_size=1, max_size=2),
+ ),
+ )
+
+
+def _responses() -> SearchStrategy[Response]:
+ calls = st.builds(
+ ToolCall,
+ name=st.text(max_size=30),
+ arguments=_json_maps(),
+ result=st.none() | st.text(max_size=100),
+ timestamp=_timestamps(),
+ )
+ effects = st.builds(SideEffect, kind=st.text(max_size=30), details=_json_maps())
+ return st.builds(
+ Response,
+ text=st.text(max_size=100),
+ tool_calls=st.lists(calls, max_size=2),
+ side_effects=st.lists(effects, max_size=2),
+ metadata=_json_maps(),
+ )
+
+
+def _turns() -> SearchStrategy[Turn]:
+ evaluations = st.builds(
+ EvalResult,
+ outcome=st.sampled_from(EvalOutcome),
+ confidence=st.floats(min_value=0, max_value=1),
+ evidence=st.lists(st.text(max_size=30), max_size=3),
+ rationale=st.text(max_size=50),
+ undetermined_operands=st.lists(st.text(max_size=30), max_size=3),
+ )
+ return st.builds(
+ Turn,
+ request=_requests(),
+ response=_responses(),
+ eval_result=st.none() | evaluations,
+ turn_number=st.integers(min_value=0, max_value=100),
+ timestamp=_timestamps(),
+ driver_reasoning=st.text(max_size=50),
+ )
+
+
+def _results() -> SearchStrategy[Result]:
+ injections = st.builds(
+ InjectionRecord,
+ payload_id=st.none() | st.text(max_size=30),
+ surface_name=st.text(max_size=30),
+ )
+ populations = st.builds(
+ PopulationRef,
+ id=st.text(max_size=30),
+ index=st.integers(min_value=0, max_value=9),
+ size=st.just(10),
+ threshold=st.floats(min_value=0, max_value=1),
+ )
+ return st.builds(
+ Result,
+ status=st.sampled_from(SafetyStatus),
+ summary=st.text(max_size=100),
+ observability_level=st.sampled_from(ObservabilityLevel),
+ turns=st.lists(_turns(), max_size=3),
+ duration_seconds=st.floats(min_value=0, allow_infinity=False),
+ harm_category=st.none() | st.text(max_size=30) | st.sampled_from(HarmCategory),
+ strategy=st.text(max_size=30),
+ injections=st.lists(injections, max_size=2),
+ population=st.none() | populations,
+ metadata=_json_maps(),
+ )
+
+
+class TestGeneratedRoundTrips:
+ @given(result=_results())
+ def test_body_preserves_supported_values(self, result: Result) -> None:
+ body = result.to_dict()
+
+ restored = Result.from_dict(json.loads(json.dumps(body, allow_nan=False)))
+
+ assert restored == result
+ assert restored.to_dict() == body
+ assert result.to_dict() == body
+
+ @given(
+ result=_results(),
+ nodeid=st.none() | st.text(max_size=40),
+ index=st.none() | st.integers(min_value=0, max_value=100),
+ )
+ def test_record_round_trip_matches_the_structural_schema(
+ self, *, result: Result, nodeid: str | None, index: int | None
+ ) -> None:
+ record = ResultRecord(result=result, pytest_nodeid=nodeid, result_index=index)
+ original_body = result.to_dict()
+ encoded = serialize_record(record=record)
+ body = json.loads(encoded)
+
+ restored = deserialize_record(data=encoded)
+
+ assert restored.result == replace(result, metadata=body["result"]["metadata"])
+ assert restored.pytest_nodeid == nodeid
+ assert restored.result_index == index
+ assert restored.to_dict() == body
+ assert record.to_dict() == body
+ assert result.to_dict() == original_body
+ Draft202012Validator(ResultRecord.json_schema()).validate(body)
diff --git a/uv.lock b/uv.lock
index 1d9bf2dd..821526f7 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1256,6 +1256,90 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
+[[package]]
+name = "hypothesis"
+version = "6.168.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "sortedcontainers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/ce/c0946bebffb99b62426a6a7643d4272cc6c5cf777a488b3b4d0ee724e960/hypothesis-6.168.0.tar.gz", hash = "sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e", size = 510805, upload-time = "2026-09-08T18:48:36.072Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/f8/8b2cc9ae7b439538f6f2d32a92892340b6343a6b04d171c516666901dedc/hypothesis-6.168.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2", size = 791358, upload-time = "2026-09-08T18:47:37.076Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/f4/4d7d897310cde5085779fb96feadb8529d98cb8e51ed7b24f7da9b6c6bdc/hypothesis-6.168.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099", size = 787081, upload-time = "2026-09-08T18:47:16.227Z" },
+ { url = "https://files.pythonhosted.org/packages/26/7b/9d52066d363faba7f3ac20ee60a3c696a475feb2c477d235d0d649d41cc1/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4", size = 1123850, upload-time = "2026-09-08T18:48:11.504Z" },
+ { url = "https://files.pythonhosted.org/packages/50/cf/aa46d76fa7df43caf2c372e394fda84ce1dc08421814f8674a6b9295e2ea/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f", size = 1147685, upload-time = "2026-09-08T18:46:58.219Z" },
+ { url = "https://files.pythonhosted.org/packages/84/c2/78ed8c8d5aa37e4baae2a4b3e29687ee3d9b7f1a5e56ea5ea5ec7ec71ecb/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1", size = 1149294, upload-time = "2026-09-08T18:47:04.757Z" },
+ { url = "https://files.pythonhosted.org/packages/78/7f/d57440f19e9de70e85359cf179ce786f309ee17609bd3c5a0113272875c0/hypothesis-6.168.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213", size = 1169729, upload-time = "2026-09-08T18:47:44.056Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/86/dc74410a186990bb22c2a3eea0e77804f2eb0f300860b46d7d8948073674/hypothesis-6.168.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131", size = 1129182, upload-time = "2026-09-08T18:46:33.382Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/5e/be048fc4f6dac831625e155bdf11e4233caf46bb54030391d6fba8e19449/hypothesis-6.168.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149", size = 1160180, upload-time = "2026-09-08T18:47:26.459Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/4a/15a34498a5f08720fbbdbbe4668a8050fe4e17c16c9eeb6f56a017f2fa6b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d", size = 1299711, upload-time = "2026-09-08T18:47:28.34Z" },
+ { url = "https://files.pythonhosted.org/packages/77/09/5354e0dae302ab98c4f0046b7e8699c2186ae4349397bda5d5c852bda68b/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517", size = 1425341, upload-time = "2026-09-08T18:46:56.785Z" },
+ { url = "https://files.pythonhosted.org/packages/59/7c/3c0e1f59043ff128c70a51d298d3d6b5973525c357a1e1d0542dbc05ac90/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349", size = 1281063, upload-time = "2026-09-08T18:46:55.401Z" },
+ { url = "https://files.pythonhosted.org/packages/40/dd/db884db9a7d42ae6b72a00c13c725940b638dfb263e618b22af59d5dfa2f/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b", size = 1300247, upload-time = "2026-09-08T18:47:02.949Z" },
+ { url = "https://files.pythonhosted.org/packages/99/8a/4ee9769e1d48676efb6a78a130f82e0d52d3f87b2055294102272a08615c/hypothesis-6.168.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377", size = 1336084, upload-time = "2026-09-08T18:48:05.675Z" },
+ { url = "https://files.pythonhosted.org/packages/61/17/d4ed11bc99d205d6d2651a0f1a4874f377150f836b5a0849bd511d68a2eb/hypothesis-6.168.0-cp310-abi3-win32.whl", hash = "sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29", size = 677989, upload-time = "2026-09-08T18:47:24.722Z" },
+ { url = "https://files.pythonhosted.org/packages/77/51/abf1fde7b8afab87db30afb73b3847e62440551d146472111cabeba2fe00/hypothesis-6.168.0-cp310-abi3-win_amd64.whl", hash = "sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b", size = 684692, upload-time = "2026-09-08T18:46:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/12/e2/64d79aed47a95186ce9edcef60eb4684870c72542da3b7027c2483d8ee8c/hypothesis-6.168.0-cp310-abi3-win_arm64.whl", hash = "sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc", size = 682709, upload-time = "2026-09-08T18:48:03.756Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/7a/7a28d9afd52c9a89c4d8e3170c92fe1ffbd4a4ce3f905ca15fa7ddaaa581/hypothesis-6.168.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a", size = 792045, upload-time = "2026-09-08T18:46:29.612Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/40/e2d6fe12b54abbc8b802b234ec4974d0a420dde4098f57a324e9c791ac27/hypothesis-6.168.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9", size = 787932, upload-time = "2026-09-08T18:47:11.145Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/89/cc78e05f3248949c6d9ecb489d0e422247d11b08533ab02e177a10a01610/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843", size = 1123994, upload-time = "2026-09-08T18:47:12.647Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/e3/7d483a79e2ed9a6c868cac8783b3c6c2bb82303f7db364c9da9a406df10d/hypothesis-6.168.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063", size = 1170271, upload-time = "2026-09-08T18:47:54.322Z" },
+ { url = "https://files.pythonhosted.org/packages/78/87/bffaf5b5e6b4566aa40d8a4ae25d8d84b47dfe0d9e771f454e4974cdb428/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04", size = 1300162, upload-time = "2026-09-08T18:46:27.022Z" },
+ { url = "https://files.pythonhosted.org/packages/25/06/6c30a00fc2f5c58546e858259cf2261c6a3ef6a060c373e00d00311267be/hypothesis-6.168.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f", size = 1336294, upload-time = "2026-09-08T18:46:37.324Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/501910e5a6a9c245f2394fda016376f6c7e209c5dca8c056eaece64ad10e/hypothesis-6.168.0-cp311-cp311-win_amd64.whl", hash = "sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2", size = 684464, upload-time = "2026-09-08T18:47:19.779Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5e/0035896c101f0484c364353f8ee30175eef8936b49171618677287fdd85d/hypothesis-6.168.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda", size = 793115, upload-time = "2026-09-08T18:48:21.352Z" },
+ { url = "https://files.pythonhosted.org/packages/11/5c/938173e27df771cc6e92bc47f117a1b1be4a88fc6dc214f7fc65f9c7ad93/hypothesis-6.168.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79", size = 784634, upload-time = "2026-09-08T18:47:21.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/00/0b2c6ac07d519131f97712f2533750ffe9b2490eec8f518ef3a5ed2dd514/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7", size = 1122855, upload-time = "2026-09-08T18:46:21.967Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/21/dde930fe43171cab37572bd993d70c2a271f240f428f8ccd64ec4c2d661b/hypothesis-6.168.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4", size = 1168932, upload-time = "2026-09-08T18:46:39.856Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/5d/92b83c3d06194ec626e92723d0b0f70221ebf42d7cb355ed36929df6d735/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d", size = 1298566, upload-time = "2026-09-08T18:48:19.485Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/67/52de8bf3446e3d2d555b812d96673b31bb213b5b9d5804a666e5d0bba76e/hypothesis-6.168.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d", size = 1335074, upload-time = "2026-09-08T18:48:01.561Z" },
+ { url = "https://files.pythonhosted.org/packages/14/fd/e592773c1c0ce55e35d26ec55f75546bf1fd72ef5a5c520ed685969b40cb/hypothesis-6.168.0-cp312-cp312-win_amd64.whl", hash = "sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68", size = 682026, upload-time = "2026-09-08T18:47:01.471Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/e9/39bb8fcccfbafd10fcc777d583c6ebad5d2148e7d743cb350562f28e974f/hypothesis-6.168.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28", size = 793050, upload-time = "2026-09-08T18:47:56.045Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/dd/00fd32e8ec470535e0065cb8d6e175f9fc54b4d3a6f1269f6c487f8bd79d/hypothesis-6.168.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20", size = 784558, upload-time = "2026-09-08T18:46:32.054Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/4d/553c47093f68bdbac0438e16c024ce97649b5804dc6972ef86b9bd2db8a1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475", size = 1122841, upload-time = "2026-09-08T18:48:09.374Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d6/0b5940aa75e617c8fd12200bae24d1b71347362514e8210735c581d4d3d1/hypothesis-6.168.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0", size = 1168825, upload-time = "2026-09-08T18:47:33.702Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c0/800de1231b2869b51409bbf85799d6f1bf49a00e0afff0aabc097aa8f1b7/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669", size = 1298433, upload-time = "2026-09-08T18:46:38.53Z" },
+ { url = "https://files.pythonhosted.org/packages/be/35/9907667a30c1dbabbc44a09b4c25a0f575570937fcbbada924ae3a1dbf2a/hypothesis-6.168.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c", size = 1334988, upload-time = "2026-09-08T18:48:29.395Z" },
+ { url = "https://files.pythonhosted.org/packages/98/8a/7bf214e703fff532ed47cb52ab93ce4b7ea41e4e7084593fa02b740828b7/hypothesis-6.168.0-cp313-cp313-win_amd64.whl", hash = "sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d", size = 681990, upload-time = "2026-09-08T18:47:49.173Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/c1/64b36b250b1f66abb6ce8c81775d3373d149bc89cbea477ed71b57cf7d1b/hypothesis-6.168.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc", size = 793101, upload-time = "2026-09-08T18:46:54.067Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/c4/494e42304b15f4ec649d36bbc3fc01cef1b63405cf30d4087ae07d048172/hypothesis-6.168.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8", size = 784632, upload-time = "2026-09-08T18:48:31.551Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6d/90d874cb1d749f505749c9908f34e803b97b03457797d5893354980558bc/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93", size = 1123101, upload-time = "2026-09-08T18:48:17.362Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/bb/ec893d0e5f4bcdd0121a8a4280f4e8aba3b4cdae01411f3236016ecd1f81/hypothesis-6.168.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7", size = 1168962, upload-time = "2026-09-08T18:46:48.507Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f1/16ec2bddbaed461725d9aa5a80b43f1905ea50a08f689ec46f8966bb4f0f/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9", size = 1298932, upload-time = "2026-09-08T18:47:40.632Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/12/7c2fe2706d092f12bd7b3e8565e1ca5d0c24b853751f2f970768086dbdeb/hypothesis-6.168.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6", size = 1335196, upload-time = "2026-09-08T18:47:57.775Z" },
+ { url = "https://files.pythonhosted.org/packages/20/35/59f7ca2414ca39408d13f66a344affe0ffc64748dc01d8a1ca910009cdcb/hypothesis-6.168.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27", size = 624102, upload-time = "2026-09-08T18:47:38.869Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/1e/dcd9335ace916ffea40f2cb04ba4122094c2f7b928f3a73fc7d452ce5b71/hypothesis-6.168.0-cp314-cp314-win_amd64.whl", hash = "sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193", size = 681871, upload-time = "2026-09-08T18:47:59.697Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d0/bc50b0b91e40744b7caa56b8add85cef432f85b4d00108409e8eb17af830/hypothesis-6.168.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1", size = 791695, upload-time = "2026-09-08T18:47:06.306Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/53/fc7537d50ff008dc5ea8598764935f93dd07bcedaf23ee4e635bdf7055f4/hypothesis-6.168.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd", size = 783239, upload-time = "2026-09-08T18:47:47.43Z" },
+ { url = "https://files.pythonhosted.org/packages/71/2a/c7aac2efc06713f704d7e354755aff4a11608b9fe93d973ead374f3b81a3/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507", size = 1121412, upload-time = "2026-09-08T18:47:07.927Z" },
+ { url = "https://files.pythonhosted.org/packages/60/e2/668ab29e5096af682b17b8491f5427d7c5f17c1b991daa5577bde80c29ca/hypothesis-6.168.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4", size = 1167570, upload-time = "2026-09-08T18:47:17.94Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/17/c64635e4c988b5fa3d3b8be322e19e2fe4c731fa0ca074ca852aa70debea/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8", size = 1297118, upload-time = "2026-09-08T18:48:13.407Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/c5/c7a0d9a06bf5c3279386dd53161081a57b98c6faf60fbbf64d046315e9e6/hypothesis-6.168.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571", size = 1334068, upload-time = "2026-09-08T18:48:15.462Z" },
+ { url = "https://files.pythonhosted.org/packages/32/99/11a393a20a867e9b978308323d45022e96f5cd2edf391e4d9a65fb4e2cf7/hypothesis-6.168.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8", size = 681795, upload-time = "2026-09-08T18:47:14.415Z" },
+ { url = "https://files.pythonhosted.org/packages/16/f7/5adae1bf1d4877aca2e8c8e007e57077237e9ac3765e430ffda490b17e19/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128", size = 791056, upload-time = "2026-09-08T18:48:27.398Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/93/b1b2770b87591db5cf564b9aa0265cf21e9207d28645e0abdeb8df63225b/hypothesis-6.168.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795", size = 782980, upload-time = "2026-09-08T18:48:34.087Z" },
+ { url = "https://files.pythonhosted.org/packages/de/bd/673171c1d2423379a7d4a0f9f009cca735a422d0c4a4ac6422d1d1736cab/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441", size = 1120964, upload-time = "2026-09-08T18:46:59.92Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/00/33a9bd941b22a4fd8a8c805b1563e0db17efc020422f5bd15bdd0fdf258f/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63", size = 1143869, upload-time = "2026-09-08T18:46:49.875Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c1/963460976f41721eff8f67f30d31059ea407cc1b41c737c8859aabf37197/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782", size = 1146453, upload-time = "2026-09-08T18:46:25.757Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/53/09db238098ad66f4e6d2fe883f26c270c2595b90e21c8f969d4cb21cad7a/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc", size = 1166918, upload-time = "2026-09-08T18:48:23.436Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/31/e1b7b452c8a6166e445ba2ad80a864f6a9eee0fe4c8cecdb9af5c1ee0aa5/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e", size = 1126637, upload-time = "2026-09-08T18:47:50.878Z" },
+ { url = "https://files.pythonhosted.org/packages/97/2b/4eceed248afb46fb6b2df21cf2239362de5b25d295c2dc67a82ec8657d5e/hypothesis-6.168.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c", size = 1155682, upload-time = "2026-09-08T18:47:30.234Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/3e/f3414cda4f325004d5774485e8983b7d1b99e4b91013f013dd088fd778cf/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9", size = 1296464, upload-time = "2026-09-08T18:46:34.833Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/40/ca79cf96545e1f172b36b8df56bfeb02b61f351f283026f9a57c4631e368/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad", size = 1421853, upload-time = "2026-09-08T18:47:23.083Z" },
+ { url = "https://files.pythonhosted.org/packages/77/bc/657d5386740c1f4ac518ff05e4c5b132f1e6df2e7c865ba8fda4279adfa5/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f", size = 1278216, upload-time = "2026-09-08T18:46:30.871Z" },
+ { url = "https://files.pythonhosted.org/packages/51/54/2328cdb70489a36634534478d9b238594a269d8bec4630ea0e6897048347/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09", size = 1297593, upload-time = "2026-09-08T18:46:41.395Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ca/d803fa57e3ff7f460f6e262d2b74822cf143343d378501fd3da601b12040/hypothesis-6.168.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887", size = 1333785, upload-time = "2026-09-08T18:48:25.434Z" },
+ { url = "https://files.pythonhosted.org/packages/86/8f/b9799ae6ba6074f151db2c63f9f6f12d844512821ffaba1a3672d7e59f07/hypothesis-6.168.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780", size = 675173, upload-time = "2026-09-08T18:47:52.601Z" },
+ { url = "https://files.pythonhosted.org/packages/61/54/14c3e277b451ff24128ecc2673cac59dd7e535bce1a433c466912fd682e1/hypothesis-6.168.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef", size = 681491, upload-time = "2026-09-08T18:46:51.437Z" },
+ { url = "https://files.pythonhosted.org/packages/99/f3/827e4a48ffee7e40244b0bf064ba47c2171e053ab7edf1cf770105e23401/hypothesis-6.168.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a", size = 679197, upload-time = "2026-09-08T18:47:32.112Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/7f15e1b10d13e266b0f09cf3117dd2206104a1f6ac3b3baf6ec4c43b7c2e/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76", size = 792946, upload-time = "2026-09-08T18:47:35.438Z" },
+ { url = "https://files.pythonhosted.org/packages/10/fb/32487bdcf68b3805ec5efcb7f92fb002b573ad3f90c252d2f2913c5d3124/hypothesis-6.168.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01", size = 788783, upload-time = "2026-09-08T18:46:42.862Z" },
+ { url = "https://files.pythonhosted.org/packages/13/26/1aa42e0069ebc68d29d980d47420af324fa002494e01c1c0321c34f609ba/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab", size = 1124762, upload-time = "2026-09-08T18:48:07.512Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/4e/99509045ed55aaa05d8408663051a93168d1a8a5bfc132d92ec1584ce47a/hypothesis-6.168.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7", size = 1171748, upload-time = "2026-09-08T18:46:47.029Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c9/96e57dfd89913322b5180b031b9e36a29bfe67842108b19904466476cd24/hypothesis-6.168.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c", size = 685447, upload-time = "2026-09-08T18:46:35.97Z" },
+]
+
[[package]]
name = "identify"
version = "2.6.19"
@@ -1394,6 +1478,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/8d/302cb2057b7513327b4d575cff6b1d066ee6431a5357fc3f8867cd684406/jiter-0.15.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d5d6090cdc1b7c9e780dfb04949a990adb1e301a2fc0bbcee7de4638d33f9a", size = 344469, upload-time = "2026-05-19T10:09:46.864Z" },
]
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
[[package]]
name = "lxml"
version = "6.1.1"
@@ -3087,6 +3198,8 @@ dev = [
{ name = "flake8" },
{ name = "hatch-vcs" },
{ name = "hatchling" },
+ { name = "hypothesis" },
+ { name = "jsonschema" },
{ name = "pre-commit" },
{ name = "pytest-cov" },
{ name = "pytest-xdist", extra = ["psutil"] },
@@ -3119,6 +3232,8 @@ dev = [
{ name = "flake8", specifier = ">=7.3.0" },
{ name = "hatch-vcs", specifier = ">=0.5.0" },
{ name = "hatchling", specifier = ">=1.30.1" },
+ { name = "hypothesis", specifier = ">=6.168.0" },
+ { name = "jsonschema", specifier = ">=4.26.0" },
{ name = "pre-commit", specifier = ">=4.5.1" },
{ name = "pytest-cov", specifier = ">=6.1.0" },
{ name = "pytest-xdist", extras = ["psutil"], specifier = ">=3.8.0" },
@@ -3132,6 +3247,20 @@ docs = [
{ name = "mkdocstrings", extras = ["python"], specifier = ">=1.0.4" },
]
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", version = "4.16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
[[package]]
name = "regex"
version = "2026.5.9"
@@ -3277,6 +3406,129 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
+[[package]]
+name = "rpds-py"
+version = "2026.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/1f/a2dca5ffdbf1d475ffc4e80e4d5d720ff3a00f691795910116960ee12511/rpds_py-2026.6.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7b689145a1485c335569bd056464f3243a29af7ed3871c7be31ad624ba239bc7", size = 342174, upload-time = "2026-06-30T07:14:54.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/dc/323d08583c0832911768663d1944f0107fcd4088704858d84b5e06d105a0/rpds_py-2026.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db08f45aecde626498fb3df07bcf6d2ec040af42e859a4f5040d79c200342911", size = 345513, upload-time = "2026-06-30T07:14:56.515Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/2a/e31989834d18d2f26ec1d2774c5b1eb3331df4ea8ada525175294c94b48a/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc992ab27b15f852c76755eb2ab7dce86585ddadba6fa5946e58556088845b4", size = 373783, upload-time = "2026-06-30T07:14:57.736Z" },
+ { url = "https://files.pythonhosted.org/packages/87/fe/e80107ee3639585c9941c17d6a42cd65325022f656c023191fce78c324c8/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f88d653e7b3b779d71ae7454e20dcc9b6bae903f33c269db9f2be41bda3f261", size = 378316, upload-time = "2026-06-30T07:14:59.077Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6f/81e3adf81acfb6fa694de2a6e4e7d8863121e3e0799e0a7725e6cf5679c4/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e52655eaf81e32593abedaa4bfe33170c8cfedf3365ed9be6e11e07f148f0278", size = 499423, upload-time = "2026-06-30T07:15:00.488Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/9a/41263969df0ce3d9af2a96d5005a288200af1989aed3354bfceb5fc0b21f/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dfcc8b909769d19db55c7cc9541eb64b9b774b1057ffffb4f1048070475bb9f9", size = 386077, upload-time = "2026-06-30T07:15:01.911Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/19/7e98f468bd50346faff5b10e5297374b443bfdddacc8e9fbc65984539597/rpds_py-2026.6.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c1255b302953c86a486b81d330d5ee1d5bd937691ce271b6be0ef0e299eaab7", size = 371315, upload-time = "2026-06-30T07:15:03.317Z" },
+ { url = "https://files.pythonhosted.org/packages/99/3c/2b973b4d371906a134b03decfea7f5d9835a2c6d263454392e15b64b5b18/rpds_py-2026.6.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8d2294a31386bfa251d8c8a39472beee17db67d4f1a6eabea665d35c9a4461c3", size = 383502, upload-time = "2026-06-30T07:15:04.627Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2a/12e2799500af0a307bca76b63361c51f9fe479223561489c29eea1f2ee41/rpds_py-2026.6.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8f23ead891a3b762f35ab3b04623da7056545b48aa60d59957e6789914545da", size = 402673, upload-time = "2026-06-30T07:15:05.856Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e3/21e5872d165fe08be4f229e3d5ee9d90019c0bf0e5538de60dbd54009450/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:421aba32367055614287a4292b6a17f1939c9452299f7a0209c117e990b646d4", size = 549964, upload-time = "2026-06-30T07:15:07.159Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d0/5ee0fe36844297de8123bee27bc12078c1a7416ad9f1b8a8ca18d6b0c0ac/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1e5822dfc2f0d4ab7e745eaa6d85945069329beeccef965af3f3bb26058fcab6", size = 615446, upload-time = "2026-06-30T07:15:08.531Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/80/1ea5873cb683f2fbe5f21b23ea1f6d179ead19f3c5b249b7eb5dca568ef2/rpds_py-2026.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:83e35b57523816c8613fd0776b40cd8bb9f596b37ddd2692eb4a6bb5ab2f8c93", size = 576975, upload-time = "2026-06-30T07:15:09.97Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e1/90ef639217a5ddb15b7f4f61b1c33911fd044ad03c311bafdd2bcab85582/rpds_py-2026.6.3-cp311-cp311-win32.whl", hash = "sha256:de3eceba0b683bcbb1ab93da016d0270df1f9ae7be716b40214c5dafac6ea45a", size = 204453, upload-time = "2026-06-30T07:15:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b7/b7a1695d7af36f521fb11e80d6d3adbd744f73b921859bd3c2a2c0dc706f/rpds_py-2026.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:2c54a076ca4d370980ab57bc0e31df57bbe8d41340436a90ef8b1219a3cbb127", size = 223219, upload-time = "2026-06-30T07:15:12.476Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/a2/145afacf796e4506062825941176ad9445c2dcf2b3b6a1f13d3030a15e19/rpds_py-2026.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:168c733a7112e071bb7a66460e667edfcff06c017a3c523f7a8a8e08d0140804", size = 219137, upload-time = "2026-06-30T07:15:13.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" },
+ { url = "https://files.pythonhosted.org/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" },
+ { url = "https://files.pythonhosted.org/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" },
+ { url = "https://files.pythonhosted.org/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" },
+ { url = "https://files.pythonhosted.org/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" },
+ { url = "https://files.pythonhosted.org/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" },
+ { url = "https://files.pythonhosted.org/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" },
+ { url = "https://files.pythonhosted.org/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" },
+ { url = "https://files.pythonhosted.org/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" },
+ { url = "https://files.pythonhosted.org/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" },
+ { url = "https://files.pythonhosted.org/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" },
+ { url = "https://files.pythonhosted.org/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" },
+ { url = "https://files.pythonhosted.org/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" },
+ { url = "https://files.pythonhosted.org/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" },
+ { url = "https://files.pythonhosted.org/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" },
+ { url = "https://files.pythonhosted.org/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" },
+ { url = "https://files.pythonhosted.org/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" },
+ { url = "https://files.pythonhosted.org/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" },
+ { url = "https://files.pythonhosted.org/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" },
+ { url = "https://files.pythonhosted.org/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" },
+ { url = "https://files.pythonhosted.org/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" },
+ { url = "https://files.pythonhosted.org/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/9c/f0d19ac587fd0e4ab6b72cda355e9c5a6166b01ef7e064e437aef8eb9fef/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4cf2d36a2357e4d07bb5a4f98801265327b48256867816cfd2ceb001e9754a8f", size = 349791, upload-time = "2026-06-30T07:17:33.315Z" },
+ { url = "https://files.pythonhosted.org/packages/38/c7/1d49d204c9fd2ee6c537601dc4c1ba921e03363ca576bfab94a00254ac9a/rpds_py-2026.6.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:30c6dc199b24a5e3e81d50da0f00858c5bbdb2617a750395687f4339c5818171", size = 352842, upload-time = "2026-06-30T07:17:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e5/c0b5dc93cd0d4c06ce1f438907649514e2ea077bcd911e3154a51e96c38e/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9891e594296ab9dada6551c8e7b387b2721f27a67eecd528412e8906247a7b90", size = 382094, upload-time = "2026-06-30T07:17:36.514Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/54/ec0e907b4ca8d541112db352409bd15f871c9b243e0c92c9b5a46ae96f01/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5c2dc92304aa48a4a60443b548bb12f12e119d4b72f314015e67b9e1be97fca", size = 388662, upload-time = "2026-06-30T07:17:38.235Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f4/921c22a4fd0f1c1ac13a3996ffbf0aa67951e2c8ad0d1d9574938a2932e8/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:127e08c0642d880cf32ca47ec2a4a77b901f7e2dd1ad9762adb13955d72ffcc9", size = 504896, upload-time = "2026-06-30T07:17:39.689Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1b/a114b972cefa1ab1cdb3c7bb177cd3844a12826c507c722d3a73516dbbaf/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bb68f03f395eb793220b45c097bd4d8c32944393da0fad8b999efac0868fc8c", size = 391545, upload-time = "2026-06-30T07:17:41.336Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/98/af9b3db77d47fcbe6c8c1f36e2c2147ec70292819e99c325f871584a1c11/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3450b693fde92133e9f51060568a4c31fcca76d5e53bbd611e689ca446517e9", size = 380059, upload-time = "2026-06-30T07:17:42.857Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ba/0efd8668b97c1d26a61566386c636a7a7a09829e474fdf807caa15a2c844/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:5e8d07bddee435a2ff6f1920e18feff28d0bc4533e42f4bf6927fbd073312c41", size = 393235, upload-time = "2026-06-30T07:17:44.637Z" },
+ { url = "https://files.pythonhosted.org/packages/62/90/8c139ee9690f73b0829f32647de6f40d826f8f443af6fa72644f96351aac/rpds_py-2026.6.3-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a83ae6c67b7676b9878378547ca8e93ed77a580037bcbcd1d32f739e1e6089c", size = 413008, upload-time = "2026-06-30T07:17:46.225Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/97/0043896fdd7828ce09a1d9a8b06433714d0960fc4ff3fc4aa72b666b764e/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2bfd04c19ddbd6640de0b51894d764bd2758854d5b75bd102d2ef10cb9c293a9", size = 558118, upload-time = "2026-06-30T07:17:47.759Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/40/02355f0e134f783a8f9814c4680a1bd311d37671577a5964ea838573ff37/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:ca6546b66be9dc4738b1b043d5ebd5488c66c578c5ff0fd0e8065313fe3afb76", size = 623138, upload-time = "2026-06-30T07:17:49.355Z" },
+ { url = "https://files.pythonhosted.org/packages/10/85/48f0abdcef5cce4e034c7a5b0ceeceba0b01bf0d942824f4bb720afe2dec/rpds_py-2026.6.3-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8e65860d238379ed982fd9ba690579b5e95af2f4840f99c772816dbe573cb826", size = 586486, upload-time = "2026-06-30T07:17:51.141Z" },
+]
+
[[package]]
name = "ruff"
version = "0.16.5"
@@ -3465,6 +3717,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
+[[package]]
+name = "sortedcontainers"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
+]
+
[[package]]
name = "sqlalchemy"
version = "2.0.51"