fastapi-streams 0.2.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/CHANGELOG.md +80 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/PKG-INFO +1 -1
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/clients.md +41 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/codegen.md +96 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/development.md +29 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/errors.py +1 -1
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/agent_runs/api.py +25 -4
- {fastapi_streams-0.2.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.3.1/examples/agent_runs/generated_python}/runtime.py +86 -26
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/__init__.py +1 -1
- fastapi_streams-0.3.1/fastapi_streams/codegen/cli.py +252 -0
- fastapi_streams-0.3.1/fastapi_streams/codegen/diagnostics.py +333 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/documentation.py +8 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/ir.py +22 -38
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/python.py +2 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/python/api.py.j2 +21 -2
- {fastapi_streams-0.2.0/tests/fixtures/generated_python → fastapi_streams-0.3.1/fastapi_streams/codegen/templates/python}/runtime.py +86 -26
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/pyproject.toml +1 -1
- fastapi_streams-0.3.1/scripts/benchmark_runtime.mjs +93 -0
- fastapi_streams-0.3.1/scripts/benchmark_runtime.py +293 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/scripts/codegen.py +7 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/scripts/smoke_package.py +54 -7
- fastapi_streams-0.3.1/tests/codegen/test_request_validation.py +226 -0
- fastapi_streams-0.3.1/tests/codegen/test_stream_validation_details.py +82 -0
- fastapi_streams-0.3.1/tests/codegen/test_structured_diagnostics.py +276 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_unknown_events.py +50 -0
- fastapi_streams-0.3.1/tests/fixtures/agent_runs.typescript.diagnostics.json +20 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/fixtures/api.py +17 -2
- {fastapi_streams-0.2.0/examples/agent_runs → fastapi_streams-0.3.1/tests/fixtures}/generated_python/runtime.py +86 -26
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/integration/test_end_to_end.py +8 -8
- fastapi_streams-0.2.0/fastapi_streams/codegen/cli.py +0 -120
- fastapi_streams-0.2.0/fastapi_streams/codegen/diagnostics.py +0 -83
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.gitattributes +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.github/workflows/ci.yml +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.github/workflows/compatibility.yml +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.github/workflows/release.yml +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.gitignore +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/.pre-commit-config.yaml +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/AGENTS.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/LICENSE +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/README.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/README.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/architecture.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/native-transports.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/quickstart.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/docs/releasing.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/consume.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/consume.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/errors.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/client.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/index.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated/types.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/client.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/models.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/agent_runs/generated_python/py.typed +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/native_proxy.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/examples/rest_codegen.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/enums.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/names.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/openapi.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/LICENSE +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/README.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/errors.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/sse.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/templating.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/typescript.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/codegen/writer.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/py.typed +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/fastapi_streams/router.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/package-lock.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/package.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/scripts/check_package.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/scripts/check_release.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/scripts/test_compat.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/helpers.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_additional_properties.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_codegen.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_contract_boundaries.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_diagnostics.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_discriminators.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_documentation.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_error_contracts.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_field_names.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_http_backends.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_intersections.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_literal_fields.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_native_contract.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_nested_models.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_python_client.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_python_typing.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_required_properties.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_scalar_constraints.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/codegen/test_schema_matrix.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/contracts/test_openapi.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/README.md +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/agent_runs.openapi.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/compat-client.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/client.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/fixtures/api.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/fixtures/index.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/index.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated/types.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/client.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/models.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/generated_python/py.typed +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/integration_app.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/models.openapi.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/fixtures/models_app.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/router/test_router.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/test_release.py +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/errors.test.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/live-client.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/models.test.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/narrowing.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/sse.test.ts +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typescript/tsconfig.json +0 -0
- {fastapi_streams-0.2.0 → fastapi_streams-0.3.1}/tests/typing/client_usage.py +0 -0
|
@@ -3,6 +3,86 @@
|
|
|
3
3
|
All notable changes and consumer upgrade steps are recorded here. Entries marked
|
|
4
4
|
unreleased describe prepared source versions that have not been published.
|
|
5
5
|
|
|
6
|
+
## 0.3.1 - 2026-10-18
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Repeatable Python and Node benchmarks for generated SDK request preparation,
|
|
11
|
+
decoding/validation, SSE framing, complete consumption, fragmented chunks and
|
|
12
|
+
Python unknown-event ignoring. Reports include individual timing samples and
|
|
13
|
+
optional separate Python peak-memory measurements; no CI timing thresholds.
|
|
14
|
+
- Add generation provenance and validation capabilities to version 1 diagnostic
|
|
15
|
+
reports: canonical schema SHA-256, generator version, target/HTTP backend and
|
|
16
|
+
request/event/error validation phases. Existing diagnostic fields, IDs and
|
|
17
|
+
exact reviewed allowances remain unchanged. Additional report object keys are
|
|
18
|
+
additive; the metadata does not introduce runtime checks or full JSON Schema
|
|
19
|
+
guarantees.
|
|
20
|
+
|
|
21
|
+
### Performance
|
|
22
|
+
|
|
23
|
+
- Reuse the JSON-value adapter for skipped unknown events and HTTP-error JSON
|
|
24
|
+
decoding. Cache top-level discriminator metadata per generated event adapter
|
|
25
|
+
instead of rebuilding its JSON Schema for every ignore-policy stream.
|
|
26
|
+
- Prepare sanitized validation details only for errors that are actually raised.
|
|
27
|
+
Known events still use direct Pydantic JSON validation; malformed known events,
|
|
28
|
+
missing/non-string tags, invalid JSON and unknown nested variants still fail.
|
|
29
|
+
Public signatures, coercion, validation timing and wire encoding are unchanged.
|
|
30
|
+
|
|
31
|
+
### Upgrade from 0.3.0
|
|
32
|
+
|
|
33
|
+
1. Upgrade the generator to 0.3.1 and regenerate with the existing HTTP backend
|
|
34
|
+
to obtain the Python runtime improvements. Existing generated SDKs keep working.
|
|
35
|
+
2. Diagnostic-report readers should tolerate the new `generation` and `validation`
|
|
36
|
+
keys. Existing exact diagnostic allowances need no migration.
|
|
37
|
+
3. Continue validating server requests and domain invariants at their own
|
|
38
|
+
boundaries; consumers should reuse parsed events instead of validating the
|
|
39
|
+
same schema again. TypeScript remains static-only with no added dependency.
|
|
40
|
+
|
|
41
|
+
## 0.3.0 - 2026-10-18
|
|
42
|
+
|
|
43
|
+
### Breaking changes
|
|
44
|
+
|
|
45
|
+
- Regenerated Python SDKs validate path, query and header parameters at the
|
|
46
|
+
method call, before serialization and HTTP I/O. Invalid types or supported
|
|
47
|
+
scalar constraints now raise Pydantic `ValidationError` locally. Regenerate
|
|
48
|
+
SDKs, fix rejected arguments, and catch request validation around the method
|
|
49
|
+
call rather than only around stream iteration. Request bodies retain their
|
|
50
|
+
existing validation timing; HTTP still starts when iteration begins.
|
|
51
|
+
- TypeScript generation now diagnoses scalar constraints it cannot enforce.
|
|
52
|
+
Review the selected target's diagnostics before promoting warnings to errors.
|
|
53
|
+
Existing known limitations can be accepted by exact diagnostic ID with a
|
|
54
|
+
review reason; do not suppress the warning category globally.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- Cache generated parameter, body and event TypeAdapters. Keep valid parameter
|
|
59
|
+
encoding, integer representations of number parameters, query array explosion,
|
|
60
|
+
optional omission and JSON body aliases unchanged.
|
|
61
|
+
- Source-located diagnostics include JSON Pointer, operation ID, method/path,
|
|
62
|
+
keyword/value, target and enforcement limitation. Referenced and inline schemas
|
|
63
|
+
are located in the original document, with each affected operation identified.
|
|
64
|
+
- Add `--diagnostics FILE`, `--strict` and `--allow-diagnostics FILE` to generation
|
|
65
|
+
and drift checks. Reports are deterministic JSON with stable identities;
|
|
66
|
+
malformed, duplicate and stale allowances fail before generated files change.
|
|
67
|
+
Repository drift checks use strict mode with reviewed TypeScript limitations.
|
|
68
|
+
- Generated Python `StreamParseError` exposes `operation_id`, `category`
|
|
69
|
+
(`invalid_json` or `invalid_event_schema`) and `validation_errors`. Structured
|
|
70
|
+
details exclude input values and validator context. Keep `.data`, exception
|
|
71
|
+
chaining and the existing constructor; raw data and chained causes can contain
|
|
72
|
+
sensitive values and are never logged automatically by the SDK.
|
|
73
|
+
- Add request/diagnostic/error regression tests for both Python HTTP backends and
|
|
74
|
+
installed-wheel consumers, plus a documented validation support matrix.
|
|
75
|
+
|
|
76
|
+
### Upgrade from 0.2.0
|
|
77
|
+
|
|
78
|
+
1. Upgrade the generator to 0.3.0 and regenerate with your existing HTTP backend.
|
|
79
|
+
2. Validate consumer call sites, including invalid requests, before adoption.
|
|
80
|
+
Ordinary Pydantic coercion applies; valid numeric serialization is preserved.
|
|
81
|
+
3. Generate JSON diagnostics and review exact allowances before enabling strict
|
|
82
|
+
CI. Changed locations, constraints, values, operations or targets need review.
|
|
83
|
+
4. Read stream failure categories and structured details through public fields.
|
|
84
|
+
Keep raw payloads and full exception chains out of routine logs.
|
|
85
|
+
|
|
6
86
|
## 0.2.0 - 2026-10-18
|
|
7
87
|
|
|
8
88
|
- Source preparation passed workspace CI, independent wheel matrices and the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-streams
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Typed FastAPI streams with generated TypeScript and Python clients.
|
|
5
5
|
Project-URL: Documentation, https://github.com/mathisarends/canon/tree/main/packages/fastapi-streams/docs
|
|
6
6
|
Project-URL: Source, https://github.com/mathisarends/canon/tree/main/packages/fastapi-streams
|
|
@@ -121,6 +121,47 @@ remain ordinary members of the user-defined union.
|
|
|
121
121
|
|
|
122
122
|
## HTTP error contracts
|
|
123
123
|
|
|
124
|
+
Keep schema validation at transport boundaries. Python prepares and validates
|
|
125
|
+
requests at the method call and validates received events during iteration.
|
|
126
|
+
Pass those parsed event models onward instead of serializing them to dictionaries
|
|
127
|
+
and validating the same schema again. Server request validation remains necessary
|
|
128
|
+
regardless of SDK checks; authentication, authorization and domain invariants have
|
|
129
|
+
separate responsibilities. Canon composition does not need an additional event
|
|
130
|
+
validator around Streams. TypeScript keeps its documented static-only behavior;
|
|
131
|
+
applications needing runtime event checks can validate once before using a value.
|
|
132
|
+
|
|
133
|
+
Python request parameters and JSON bodies are validated when calling the method;
|
|
134
|
+
catch Pydantic `ValidationError` around that call. Network and event errors occur
|
|
135
|
+
while iterating. See the [validation matrix and CI policy](codegen.md#validation-support-matrix).
|
|
136
|
+
|
|
137
|
+
Generated Python `StreamParseError` provides these public fields:
|
|
138
|
+
|
|
139
|
+
| Field | Meaning |
|
|
140
|
+
| --- | --- |
|
|
141
|
+
| `operation_id` | Source OpenAPI operation ID |
|
|
142
|
+
| `category` | `invalid_json` for malformed JSON; `invalid_event_schema` for invalid event shapes/constraints |
|
|
143
|
+
| `validation_errors` | Pydantic error dictionaries with `type`, `loc`, `msg`; input values, context and URLs are excluded |
|
|
144
|
+
| `data` | Original SSE data payload, retained for explicit inspection |
|
|
145
|
+
|
|
146
|
+
For errors raised by generated operations, category and operation ID are always
|
|
147
|
+
set. The existing `StreamParseError(data)` constructor remains valid; manually
|
|
148
|
+
constructed errors can have `None` provenance/category and empty details.
|
|
149
|
+
The original Pydantic `ValidationError` remains chained as `__cause__`.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
try:
|
|
153
|
+
async for event in client.runs.run_events(run_id="run_abc"):
|
|
154
|
+
handle(event)
|
|
155
|
+
except StreamParseError as error:
|
|
156
|
+
record_failure(error.operation_id, error.category, error.validation_errors)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The SDK does not log event payloads or exceptions automatically. Raw `.data` and
|
|
160
|
+
the full chained exception may contain sensitive values; log only reviewed fields.
|
|
161
|
+
Even structured messages and field names should follow the consumer's logging
|
|
162
|
+
policy. Malformed known events still fail with `unknown_events="ignore"`; only
|
|
163
|
+
valid JSON objects with unknown string discriminator tags can be skipped.
|
|
164
|
+
|
|
124
165
|
The SDK works with ordinary OpenAPI response declarations, including
|
|
125
166
|
`application/json`, `application/problem+json`, and other declared response media
|
|
126
167
|
types. No error registry or particular problem envelope is required. Declare
|
|
@@ -119,6 +119,15 @@ fail explicitly.
|
|
|
119
119
|
|
|
120
120
|
## Runtime validation and unknown variants
|
|
121
121
|
|
|
122
|
+
Python validates path, query and header parameters and JSON request bodies at the
|
|
123
|
+
method call, before serialization or HTTP I/O. Invalid arguments raise Pydantic
|
|
124
|
+
`ValidationError` immediately, even if the returned iterator is never consumed.
|
|
125
|
+
HTTP starts when iteration begins. Optional parameters supplied as `None` are
|
|
126
|
+
omitted, preserving existing omission semantics. Pydantic's normal coercion
|
|
127
|
+
applies; this is not Pydantic strict mode. Valid scalar encoding, integer values
|
|
128
|
+
of number parameters (`0` stays `0`), query array explosion and body aliases are
|
|
129
|
+
preserved. Adapters are cached in each generated API class.
|
|
130
|
+
|
|
122
131
|
Python validates generated event models and request bodies with Pydantic. Supported
|
|
123
132
|
scalar constraints are `pattern`, `minLength`, `maxLength`, `minimum`, `maximum`,
|
|
124
133
|
`exclusiveMinimum`, `exclusiveMaximum`, and `multipleOf`. They survive nested
|
|
@@ -150,6 +159,93 @@ unknown nested variants inside a known event fail too. The option adds no retry,
|
|
|
150
159
|
reconnect or replay. TypeScript continues to forward JSON-decoded values without
|
|
151
160
|
runtime union validation. Regenerate existing Python SDKs to obtain the option.
|
|
152
161
|
|
|
162
|
+
## Validation support matrix
|
|
163
|
+
|
|
164
|
+
| SDK target / boundary | Runtime shape validation | Supported scalar constraints | Timing |
|
|
165
|
+
| --- | --- | --- | --- |
|
|
166
|
+
| Python path/query/header | Yes | Yes, including referenced scalars and query array items | Method call, before serialization |
|
|
167
|
+
| Python JSON request body | Yes | Yes, including nested values | Method call |
|
|
168
|
+
| Python SSE event data | Yes | Yes, including nested values | Each event during iteration |
|
|
169
|
+
| Python declared HTTP error bodies | Explicit consumer TypeAdapter | When explicitly validated | Consumer-controlled |
|
|
170
|
+
| TypeScript parameters/body/events | Static types; events are JSON-decoded | No runtime enforcement | Generation diagnoses limitations |
|
|
171
|
+
|
|
172
|
+
Unsupported formats and collection/object constraints remain diagnostics rather
|
|
173
|
+
than implicit runtime validators. Optional `None` query/header parameters mean
|
|
174
|
+
omission, not JSON null. Server-side validation remains necessary for every client.
|
|
175
|
+
|
|
176
|
+
## Source diagnostics and CI policy
|
|
177
|
+
|
|
178
|
+
`generate`, `generate --check` and `check` accept these options:
|
|
179
|
+
|
|
180
|
+
```sh
|
|
181
|
+
uv run fastapi-streams check --schema openapi.json --language python \
|
|
182
|
+
--output generated --diagnostics reports/python.json --strict \
|
|
183
|
+
--allow-diagnostics reviewed-python.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Omit `--allow-diagnostics` when no limitations are accepted. The JSON report
|
|
187
|
+
(`version: 1`, `diagnostics: [...]`) includes each diagnostic's `id`, `code`,
|
|
188
|
+
`pointer`, `keyword`, `value`, `target`, `operation_id`, `method`, `path`, `message`
|
|
189
|
+
and `accepted` status. Pointers are RFC 6901 paths into the original document,
|
|
190
|
+
including inline schemas and referenced component definitions. Shared definitions
|
|
191
|
+
identify each affected operation; unused components do not generate warnings.
|
|
192
|
+
SSE framing metadata is outside generated event-model validation.
|
|
193
|
+
|
|
194
|
+
The version 1 report also carries additive `generation` and `validation` objects.
|
|
195
|
+
Consumers should tolerate additional object keys. `generation` records the
|
|
196
|
+
generator name/version, target, selected Python HTTP library (null for TypeScript)
|
|
197
|
+
and `schema_sha256`. The hash covers the complete parsed OpenAPI document,
|
|
198
|
+
including unused definitions. Its `canonical-json-v1` encoding is Python
|
|
199
|
+
`json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=False)`
|
|
200
|
+
encoded as UTF-8. Whitespace and object-key order in the input file do not affect
|
|
201
|
+
the hash; array order and values do. It is a content identifier, not a signature
|
|
202
|
+
or proof of schema enforcement.
|
|
203
|
+
|
|
204
|
+
`validation` distinguishes parameter/body validation at `method_call`, event
|
|
205
|
+
decoding at `iteration` and HTTP-error handling at `consumer_controlled`.
|
|
206
|
+
TypeScript parameter/body schema checks are `not_performed`. Each boundary has
|
|
207
|
+
a `schema_validation` boolean, with `json_decoding: true` for events. Python's
|
|
208
|
+
`runtime_scope` is `generated_types_and_supported_constraints`; TypeScript's is
|
|
209
|
+
`json_decoding_only`. Both set `full_json_schema: false`. These target capabilities
|
|
210
|
+
describe supported boundaries, even if an operation has no parameters or body;
|
|
211
|
+
source-specific limitations remain in `diagnostics` with their accepted status.
|
|
212
|
+
The report is generated only when requested and adds no SDK runtime checks.
|
|
213
|
+
|
|
214
|
+
`unsupported-constraint` identifies an unenforced rule for the selected target;
|
|
215
|
+
TypeScript also diagnoses Python-supported scalar constraints because TypeScript
|
|
216
|
+
has no runtime schema validator. Python's `optional-discriminator` diagnoses event
|
|
217
|
+
selection risks without changing field requiredness. Human-readable warnings
|
|
218
|
+
contain the same location, keyword/value, operation and limitation as the report.
|
|
219
|
+
|
|
220
|
+
Without `--strict`, unaccepted diagnostics remain warnings. With it, any
|
|
221
|
+
unaccepted diagnostic fails with exit code 2 before generated files change; drift
|
|
222
|
+
still uses exit code 1. Reports are written even on strict-policy failure. Keep
|
|
223
|
+
the report outside generated output and separate from schema/allowance inputs.
|
|
224
|
+
|
|
225
|
+
Review the report and explicitly record an allowance, for example:
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"allow": [
|
|
230
|
+
{
|
|
231
|
+
"id": "unsupported-constraint:<copy the exact ID from the report>",
|
|
232
|
+
"reason": "Reviewed upstream UUID validation; client remains a string."
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
IDs include code, source pointer, keyword/value, target and operation context;
|
|
239
|
+
message wording and schema filename are excluded. Changed constraints or contexts
|
|
240
|
+
need a new review. Empty reasons, duplicate IDs, malformed files and stale
|
|
241
|
+
allowances fail explicitly. Keep separate policies for each contract/target.
|
|
242
|
+
Repository drift checks use strict mode with reviewed TypeScript allowances.
|
|
243
|
+
|
|
244
|
+
Python generator API users can collect the same objects with
|
|
245
|
+
`build_ir(document, target="python", diagnostics=items)` where `items` is an empty
|
|
246
|
+
list. Omitting `diagnostics` emits warnings; the default target is Python. Select
|
|
247
|
+
the matching target explicitly before calling the corresponding generator.
|
|
248
|
+
|
|
153
249
|
## Existing native streams and mixed REST generation
|
|
154
250
|
|
|
155
251
|
See the [native transport and mixed-codegen recipe](native-transports.md) when
|
|
@@ -88,6 +88,35 @@ outside the checkout they invoke the installed `fastapi-streams` command, exerci
|
|
|
88
88
|
and consume an event through the generated Python SDK. Metadata and `__version__`
|
|
89
89
|
must match the release version in `pyproject.toml`.
|
|
90
90
|
|
|
91
|
+
## Generated SDK performance
|
|
92
|
+
|
|
93
|
+
From the workspace root, use the locked environment and installed Node tooling:
|
|
94
|
+
|
|
95
|
+
```console
|
|
96
|
+
uv run --locked python packages/fastapi-streams/scripts/benchmark_runtime.py --memory --output dist/streams-baseline.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The harness generates both SDKs from the same small discriminated-union contract.
|
|
100
|
+
It measures request preparation separately from decoding, framing and complete
|
|
101
|
+
HTTP/SSE consumption with an in-memory HTTPX transport or Fetch response. Cases
|
|
102
|
+
cover small token events, nested batches, seven-byte fragments and Python's
|
|
103
|
+
unknown-event ignore policy. Python validates generated models; TypeScript only
|
|
104
|
+
decodes JSON. Their decoding results do not represent equivalent guarantees.
|
|
105
|
+
|
|
106
|
+
Reports record source location, generator/runtime versions, platform, payload
|
|
107
|
+
sizes, individual samples, median CPU/wall time per request or input event, and
|
|
108
|
+
throughput. Ignore-policy throughput counts skipped input events too. `--memory`
|
|
109
|
+
adds a separate Python traced peak-memory sample; it does not instrument timings
|
|
110
|
+
or count total allocations. There are three warmup iterations per case. These
|
|
111
|
+
are warmed batch measurements, not individual-event latency percentiles, cold
|
|
112
|
+
startup measurements, real-network results or Hermes deployment evidence.
|
|
113
|
+
|
|
114
|
+
Use `--runtime python` or `--runtime typescript` for focused runs. Adjust
|
|
115
|
+
`--events`, `--iterations` and `--rounds` for longer samples. For a comparison,
|
|
116
|
+
run the same harness with `--source-dir` pointing to the other revision's Streams
|
|
117
|
+
package directory, keeping the interpreter, dependencies and arguments fixed.
|
|
118
|
+
Repeat runs to assess noise; ordinary CI does not enforce timing thresholds.
|
|
119
|
+
|
|
91
120
|
## Supported scope and roadmap
|
|
92
121
|
|
|
93
122
|
GET/POST JSON SSE data; scalar path and header parameters; scalar/array query parameters;
|
|
@@ -30,7 +30,7 @@ async def consume_with_errors(client: Client, run_id: str) -> None:
|
|
|
30
30
|
print("Unexpected HTTP response", error.status, error.details)
|
|
31
31
|
raise
|
|
32
32
|
except StreamParseError as error:
|
|
33
|
-
print("Invalid event data", error.
|
|
33
|
+
print("Invalid event data", error.operation_id, error.category)
|
|
34
34
|
raise
|
|
35
35
|
except TransportError as error:
|
|
36
36
|
print("Stream transport failed", str(error))
|
|
@@ -6,10 +6,17 @@ from collections.abc import AsyncGenerator
|
|
|
6
6
|
from pydantic import TypeAdapter
|
|
7
7
|
|
|
8
8
|
from .. import models as Models
|
|
9
|
-
from ..runtime import RequestOptions, Transport, append_query, encode_path
|
|
9
|
+
from ..runtime import RequestOptions, Transport, append_query, encode_path, validate_parameter as _validate_parameter
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class AgentRunsApi:
|
|
13
|
+
_stream_prompt_run_id_adapter = TypeAdapter(str)
|
|
14
|
+
_stream_prompt_body_adapter = TypeAdapter(Models.RunRequest)
|
|
15
|
+
_stream_prompt_event_adapter = TypeAdapter(Models.RunEvent)
|
|
16
|
+
_stream_run_events_run_id_adapter = TypeAdapter(str)
|
|
17
|
+
_stream_run_events_delay_adapter = TypeAdapter(Models.StreamRunEventsDelay)
|
|
18
|
+
_stream_run_events_event_adapter = TypeAdapter(Models.RunEvent)
|
|
19
|
+
|
|
13
20
|
def __init__(self, transport: Transport) -> None:
|
|
14
21
|
self._transport = transport
|
|
15
22
|
|
|
@@ -33,20 +40,26 @@ class AgentRunsApi:
|
|
|
33
40
|
|
|
34
41
|
Declared HTTP response bodies: StreamPromptErrorBody (422 application/json). HttpError.details may also contain undeclared JSON, text, or no body; validate it before accessing application fields.
|
|
35
42
|
|
|
43
|
+
Path, query and header parameters and JSON bodies are validated with Pydantic at the method call, before serialization or HTTP I/O. Optional None parameters are omitted. Pydantic's normal type coercion applies; invalid arguments raise ValidationError.
|
|
44
|
+
|
|
36
45
|
Use contextlib.aclosing for deterministic cleanup when exiting iteration early. Task cancellation closes the response and propagates normally.
|
|
37
46
|
|
|
38
47
|
Events are validated with Pydantic against generated shapes, variants and supported scalar constraints (string pattern/length, numeric bounds/multiples). Unsupported source constraints are diagnosed during generation; this is not full JSON Schema validation.
|
|
39
48
|
|
|
40
49
|
Unknown event variants fail by default. For a top-level discriminated union, RequestOptions(unknown_events='ignore') skips unknown string tags. Malformed known events, missing tags and invalid JSON still fail.
|
|
50
|
+
|
|
51
|
+
StreamParseError exposes operation_id, category (invalid_json or invalid_event_schema), and validation_errors without input values. Its data and chained cause may contain sensitive content; the SDK does not log them automatically.
|
|
41
52
|
"""
|
|
53
|
+
run_id = _validate_parameter(self._stream_prompt_run_id_adapter, run_id)
|
|
42
54
|
query: list[tuple[str, str]] = []
|
|
43
55
|
headers: dict[str, str] = {}
|
|
44
|
-
adapter =
|
|
56
|
+
adapter = self._stream_prompt_body_adapter
|
|
45
57
|
content = adapter.dump_json(adapter.validate_python(body), by_alias=True, exclude_unset=True)
|
|
46
58
|
return self._transport.stream(
|
|
47
59
|
'/v1/runs/' + encode_path(run_id) + '/events',
|
|
48
60
|
'POST',
|
|
49
|
-
|
|
61
|
+
self._stream_prompt_event_adapter,
|
|
62
|
+
operation_id='stream_prompt',
|
|
50
63
|
query=query,
|
|
51
64
|
headers=headers,
|
|
52
65
|
body=content,
|
|
@@ -73,19 +86,27 @@ class AgentRunsApi:
|
|
|
73
86
|
|
|
74
87
|
Declared HTTP response bodies: StreamRunEventsErrorBody (422 application/json). HttpError.details may also contain undeclared JSON, text, or no body; validate it before accessing application fields.
|
|
75
88
|
|
|
89
|
+
Path, query and header parameters and JSON bodies are validated with Pydantic at the method call, before serialization or HTTP I/O. Optional None parameters are omitted. Pydantic's normal type coercion applies; invalid arguments raise ValidationError.
|
|
90
|
+
|
|
76
91
|
Use contextlib.aclosing for deterministic cleanup when exiting iteration early. Task cancellation closes the response and propagates normally.
|
|
77
92
|
|
|
78
93
|
Events are validated with Pydantic against generated shapes, variants and supported scalar constraints (string pattern/length, numeric bounds/multiples). Unsupported source constraints are diagnosed during generation; this is not full JSON Schema validation.
|
|
79
94
|
|
|
80
95
|
Unknown event variants fail by default. For a top-level discriminated union, RequestOptions(unknown_events='ignore') skips unknown string tags. Malformed known events, missing tags and invalid JSON still fail.
|
|
96
|
+
|
|
97
|
+
StreamParseError exposes operation_id, category (invalid_json or invalid_event_schema), and validation_errors without input values. Its data and chained cause may contain sensitive content; the SDK does not log them automatically.
|
|
81
98
|
"""
|
|
99
|
+
run_id = _validate_parameter(self._stream_run_events_run_id_adapter, run_id)
|
|
100
|
+
if delay is not None:
|
|
101
|
+
delay = _validate_parameter(self._stream_run_events_delay_adapter, delay)
|
|
82
102
|
query: list[tuple[str, str]] = []
|
|
83
103
|
headers: dict[str, str] = {}
|
|
84
104
|
append_query(query, 'delay', delay, explode=True)
|
|
85
105
|
return self._transport.stream(
|
|
86
106
|
'/v1/runs/' + encode_path(run_id) + '/events',
|
|
87
107
|
'GET',
|
|
88
|
-
|
|
108
|
+
self._stream_run_events_event_adapter,
|
|
109
|
+
operation_id='stream_run_events',
|
|
89
110
|
query=query,
|
|
90
111
|
headers=headers,
|
|
91
112
|
options=options,
|
|
@@ -5,10 +5,14 @@ import codecs
|
|
|
5
5
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum
|
|
8
|
-
from
|
|
8
|
+
from functools import cache
|
|
9
|
+
from typing import Literal, cast
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
from pydantic import JsonValue, TypeAdapter, ValidationError
|
|
13
|
+
from pydantic_core import ErrorDetails
|
|
14
|
+
|
|
15
|
+
_JSON_ADAPTER = TypeAdapter[JsonValue](JsonValue)
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
class TransportError(Exception):
|
|
@@ -24,8 +28,20 @@ class HttpError(TransportError):
|
|
|
24
28
|
|
|
25
29
|
|
|
26
30
|
class StreamParseError(TransportError):
|
|
27
|
-
|
|
31
|
+
"""Invalid event data. Raw data and chained exceptions may contain secrets."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
data: str,
|
|
36
|
+
*,
|
|
37
|
+
operation_id: str | None = None,
|
|
38
|
+
category: Literal["invalid_json", "invalid_event_schema"] | None = None,
|
|
39
|
+
validation_errors: list[ErrorDetails] | None = None,
|
|
40
|
+
) -> None:
|
|
28
41
|
self.data = data
|
|
42
|
+
self.operation_id = operation_id
|
|
43
|
+
self.category = category
|
|
44
|
+
self.validation_errors = validation_errors or []
|
|
29
45
|
super().__init__("Invalid JSON or event schema in SSE data")
|
|
30
46
|
|
|
31
47
|
|
|
@@ -43,6 +59,11 @@ class RequestOptions:
|
|
|
43
59
|
type ParameterValue = str | int | float | bool | None
|
|
44
60
|
|
|
45
61
|
|
|
62
|
+
def validate_parameter[T](adapter: TypeAdapter[T], value: T) -> T:
|
|
63
|
+
"""Validate/coerce inputs while preserving valid numeric wire representations."""
|
|
64
|
+
return _preserve_numbers(value, adapter.validate_python(value))
|
|
65
|
+
|
|
66
|
+
|
|
46
67
|
def encode_path(value: str | int | float | bool | None) -> str:
|
|
47
68
|
from urllib.parse import quote
|
|
48
69
|
|
|
@@ -155,25 +176,18 @@ class Transport:
|
|
|
155
176
|
method: str,
|
|
156
177
|
adapter: TypeAdapter[T],
|
|
157
178
|
*,
|
|
179
|
+
operation_id: str | None = None,
|
|
158
180
|
query: list[tuple[str, str]],
|
|
159
181
|
headers: Mapping[str, str],
|
|
160
182
|
body: bytes | None = None,
|
|
161
183
|
options: RequestOptions | None = None,
|
|
162
184
|
) -> AsyncGenerator[T, None]:
|
|
163
185
|
discriminator: str | None = None
|
|
164
|
-
known_tags:
|
|
186
|
+
known_tags: frozenset[str] = frozenset()
|
|
165
187
|
if options and options.unknown_events not in {"error", "ignore"}:
|
|
166
188
|
raise ValueError("unknown_events must be 'error' or 'ignore'")
|
|
167
189
|
if options and options.unknown_events == "ignore":
|
|
168
|
-
|
|
169
|
-
policy = schema.get("discriminator", {})
|
|
170
|
-
discriminator = policy.get("propertyName")
|
|
171
|
-
known_tags = set(policy.get("mapping", {}))
|
|
172
|
-
if not isinstance(discriminator, str) or not known_tags:
|
|
173
|
-
raise ValueError(
|
|
174
|
-
"unknown_events='ignore' requires a top-level "
|
|
175
|
-
"discriminated event union"
|
|
176
|
-
)
|
|
190
|
+
discriminator, known_tags = _unknown_event_policy(adapter)
|
|
177
191
|
request_headers = self.headers.copy()
|
|
178
192
|
request_headers.update(headers)
|
|
179
193
|
if options and options.headers:
|
|
@@ -204,11 +218,7 @@ class Transport:
|
|
|
204
218
|
failure = exc
|
|
205
219
|
text = raw.decode("utf-8", errors="replace")
|
|
206
220
|
try:
|
|
207
|
-
details = (
|
|
208
|
-
TypeAdapter[JsonValue](JsonValue).validate_json(text)
|
|
209
|
-
if text
|
|
210
|
-
else None
|
|
211
|
-
)
|
|
221
|
+
details = _JSON_ADAPTER.validate_json(text) if text else None
|
|
212
222
|
except ValidationError:
|
|
213
223
|
details = text
|
|
214
224
|
raise HttpError(response, details) from failure
|
|
@@ -228,17 +238,67 @@ class Transport:
|
|
|
228
238
|
except ValidationError as exc:
|
|
229
239
|
if discriminator is not None:
|
|
230
240
|
try:
|
|
231
|
-
value =
|
|
232
|
-
payload
|
|
233
|
-
)
|
|
241
|
+
value = _JSON_ADAPTER.validate_json(payload)
|
|
234
242
|
except ValidationError:
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
pass
|
|
244
|
+
else:
|
|
245
|
+
if isinstance(value, dict):
|
|
246
|
+
tag = value.get(discriminator)
|
|
247
|
+
if isinstance(tag, str) and tag not in known_tags:
|
|
248
|
+
continue
|
|
249
|
+
# Exclude input and arbitrary validator context from public
|
|
250
|
+
# diagnostics. The original cause remains available explicitly.
|
|
251
|
+
details = exc.errors(
|
|
252
|
+
include_input=False,
|
|
253
|
+
include_context=False,
|
|
254
|
+
include_url=False,
|
|
255
|
+
)
|
|
256
|
+
category: Literal["invalid_json", "invalid_event_schema"] = (
|
|
257
|
+
"invalid_json"
|
|
258
|
+
if any(error["type"] == "json_invalid" for error in details)
|
|
259
|
+
else "invalid_event_schema"
|
|
260
|
+
)
|
|
261
|
+
raise StreamParseError(
|
|
262
|
+
payload,
|
|
263
|
+
operation_id=operation_id,
|
|
264
|
+
category=category,
|
|
265
|
+
validation_errors=details,
|
|
266
|
+
) from exc
|
|
241
267
|
except httpx.HTTPError as exc:
|
|
242
268
|
raise TransportError("SSE HTTP transport failed") from exc
|
|
243
269
|
except UnicodeDecodeError as exc:
|
|
244
270
|
raise TransportError("Invalid UTF-8 in SSE stream") from exc
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@cache
|
|
274
|
+
def _unknown_event_policy[T](adapter: TypeAdapter[T]) -> tuple[str, frozenset[str]]:
|
|
275
|
+
schema = adapter.json_schema()
|
|
276
|
+
policy = schema.get("discriminator", {})
|
|
277
|
+
discriminator = policy.get("propertyName")
|
|
278
|
+
known_tags = frozenset(policy.get("mapping", {}))
|
|
279
|
+
if not isinstance(discriminator, str) or not known_tags:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"unknown_events='ignore' requires a top-level discriminated event union"
|
|
282
|
+
)
|
|
283
|
+
return discriminator, known_tags
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _preserve_numbers[T](original: object, validated: T) -> T:
|
|
287
|
+
if (
|
|
288
|
+
type(original) is int
|
|
289
|
+
and isinstance(validated, float)
|
|
290
|
+
and validated.is_integer()
|
|
291
|
+
and original == int(validated)
|
|
292
|
+
):
|
|
293
|
+
return cast(T, original)
|
|
294
|
+
if isinstance(original, list | tuple) and isinstance(validated, list):
|
|
295
|
+
before = cast(Sequence[object], original)
|
|
296
|
+
after = cast(list[object], validated)
|
|
297
|
+
return cast(
|
|
298
|
+
T,
|
|
299
|
+
[
|
|
300
|
+
_preserve_numbers(old, new)
|
|
301
|
+
for old, new in zip(before, after, strict=True)
|
|
302
|
+
],
|
|
303
|
+
)
|
|
304
|
+
return validated
|