fastapi-streams 0.2.0__tar.gz → 0.3.0__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.
Files changed (136) hide show
  1. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/CHANGELOG.md +45 -0
  2. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/PKG-INFO +1 -1
  3. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/clients.md +32 -0
  4. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/codegen.md +76 -0
  5. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/errors.py +1 -1
  6. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/agent_runs/api.py +25 -4
  7. {fastapi_streams-0.2.0/tests/fixtures → fastapi_streams-0.3.0/examples/agent_runs}/generated_python/runtime.py +66 -4
  8. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/__init__.py +1 -1
  9. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/cli.py +75 -2
  10. fastapi_streams-0.3.0/fastapi_streams/codegen/diagnostics.py +333 -0
  11. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/documentation.py +8 -0
  12. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/ir.py +22 -38
  13. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/python.py +2 -0
  14. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/python/api.py.j2 +21 -2
  15. {fastapi_streams-0.2.0/examples/agent_runs/generated_python → fastapi_streams-0.3.0/fastapi_streams/codegen/templates/python}/runtime.py +66 -4
  16. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/pyproject.toml +1 -1
  17. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/scripts/codegen.py +7 -0
  18. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/scripts/smoke_package.py +43 -7
  19. fastapi_streams-0.3.0/tests/codegen/test_request_validation.py +226 -0
  20. fastapi_streams-0.3.0/tests/codegen/test_stream_validation_details.py +82 -0
  21. fastapi_streams-0.3.0/tests/codegen/test_structured_diagnostics.py +209 -0
  22. fastapi_streams-0.3.0/tests/fixtures/agent_runs.typescript.diagnostics.json +20 -0
  23. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/fixtures/api.py +17 -2
  24. {fastapi_streams-0.2.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.3.0/tests/fixtures/generated_python}/runtime.py +66 -4
  25. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/integration/test_end_to_end.py +8 -8
  26. fastapi_streams-0.2.0/fastapi_streams/codegen/diagnostics.py +0 -83
  27. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.gitattributes +0 -0
  28. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.github/workflows/ci.yml +0 -0
  29. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.github/workflows/compatibility.yml +0 -0
  30. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.github/workflows/release.yml +0 -0
  31. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.gitignore +0 -0
  32. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/.pre-commit-config.yaml +0 -0
  33. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/AGENTS.md +0 -0
  34. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/LICENSE +0 -0
  35. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/README.md +0 -0
  36. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/README.md +0 -0
  37. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/architecture.md +0 -0
  38. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/development.md +0 -0
  39. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/native-transports.md +0 -0
  40. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/quickstart.md +0 -0
  41. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/docs/releasing.md +0 -0
  42. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/__init__.py +0 -0
  43. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/__init__.py +0 -0
  44. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/consume.py +0 -0
  45. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/consume.ts +0 -0
  46. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/errors.ts +0 -0
  47. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/.fastapi-streams.json +0 -0
  48. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
  49. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
  50. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/client.ts +0 -0
  51. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/index.ts +0 -0
  52. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/transport/errors.ts +0 -0
  53. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/transport/sse.ts +0 -0
  54. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated/types.ts +0 -0
  55. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/.fastapi-streams.json +0 -0
  56. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/__init__.py +0 -0
  57. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
  58. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/client.py +0 -0
  59. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/models.py +0 -0
  60. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/agent_runs/generated_python/py.typed +0 -0
  61. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/native_proxy.py +0 -0
  62. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/examples/rest_codegen.py +0 -0
  63. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/__init__.py +0 -0
  64. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/enums.py +0 -0
  65. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/names.py +0 -0
  66. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/openapi.py +0 -0
  67. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/schemas/LICENSE +0 -0
  68. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/schemas/README.md +0 -0
  69. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
  70. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/errors.ts +0 -0
  71. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
  72. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/sse.ts +0 -0
  73. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
  74. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
  75. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/templating.py +0 -0
  76. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/typescript.py +0 -0
  77. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/codegen/writer.py +0 -0
  78. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/py.typed +0 -0
  79. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/fastapi_streams/router.py +0 -0
  80. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/package-lock.json +0 -0
  81. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/package.json +0 -0
  82. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/scripts/check_package.py +0 -0
  83. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/scripts/check_release.py +0 -0
  84. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/scripts/test_compat.py +0 -0
  85. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/__init__.py +0 -0
  86. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/helpers.py +0 -0
  87. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_additional_properties.py +0 -0
  88. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_codegen.py +0 -0
  89. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_contract_boundaries.py +0 -0
  90. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_diagnostics.py +0 -0
  91. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_discriminators.py +0 -0
  92. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_documentation.py +0 -0
  93. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_error_contracts.py +0 -0
  94. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_field_names.py +0 -0
  95. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_http_backends.py +0 -0
  96. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_intersections.py +0 -0
  97. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_literal_fields.py +0 -0
  98. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_native_contract.py +0 -0
  99. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_nested_models.py +0 -0
  100. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_python_client.py +0 -0
  101. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_python_typing.py +0 -0
  102. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_required_properties.py +0 -0
  103. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_scalar_constraints.py +0 -0
  104. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_schema_matrix.py +0 -0
  105. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/codegen/test_unknown_events.py +0 -0
  106. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/contracts/test_openapi.py +0 -0
  107. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/README.md +0 -0
  108. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/__init__.py +0 -0
  109. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/agent_runs.openapi.json +0 -0
  110. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/compat-client.ts +0 -0
  111. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/.fastapi-streams.json +0 -0
  112. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/client.ts +0 -0
  113. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/fixtures/api.ts +0 -0
  114. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/fixtures/index.ts +0 -0
  115. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/index.ts +0 -0
  116. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/transport/errors.ts +0 -0
  117. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/transport/sse.ts +0 -0
  118. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated/types.ts +0 -0
  119. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/.fastapi-streams.json +0 -0
  120. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/__init__.py +0 -0
  121. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/client.py +0 -0
  122. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
  123. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/models.py +0 -0
  124. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/generated_python/py.typed +0 -0
  125. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/integration_app.py +0 -0
  126. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/models.openapi.json +0 -0
  127. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/fixtures/models_app.py +0 -0
  128. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/router/test_router.py +0 -0
  129. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/test_release.py +0 -0
  130. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/errors.test.ts +0 -0
  131. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/live-client.ts +0 -0
  132. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/models.test.ts +0 -0
  133. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/narrowing.ts +0 -0
  134. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/sse.test.ts +0 -0
  135. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typescript/tsconfig.json +0 -0
  136. {fastapi_streams-0.2.0 → fastapi_streams-0.3.0}/tests/typing/client_usage.py +0 -0
@@ -3,6 +3,51 @@
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.0 - 2026-10-18
7
+
8
+ ### Breaking changes
9
+
10
+ - Regenerated Python SDKs validate path, query and header parameters at the
11
+ method call, before serialization and HTTP I/O. Invalid types or supported
12
+ scalar constraints now raise Pydantic `ValidationError` locally. Regenerate
13
+ SDKs, fix rejected arguments, and catch request validation around the method
14
+ call rather than only around stream iteration. Request bodies retain their
15
+ existing validation timing; HTTP still starts when iteration begins.
16
+ - TypeScript generation now diagnoses scalar constraints it cannot enforce.
17
+ Review the selected target's diagnostics before promoting warnings to errors.
18
+ Existing known limitations can be accepted by exact diagnostic ID with a
19
+ review reason; do not suppress the warning category globally.
20
+
21
+ ### Added
22
+
23
+ - Cache generated parameter, body and event TypeAdapters. Keep valid parameter
24
+ encoding, integer representations of number parameters, query array explosion,
25
+ optional omission and JSON body aliases unchanged.
26
+ - Source-located diagnostics include JSON Pointer, operation ID, method/path,
27
+ keyword/value, target and enforcement limitation. Referenced and inline schemas
28
+ are located in the original document, with each affected operation identified.
29
+ - Add `--diagnostics FILE`, `--strict` and `--allow-diagnostics FILE` to generation
30
+ and drift checks. Reports are deterministic JSON with stable identities;
31
+ malformed, duplicate and stale allowances fail before generated files change.
32
+ Repository drift checks use strict mode with reviewed TypeScript limitations.
33
+ - Generated Python `StreamParseError` exposes `operation_id`, `category`
34
+ (`invalid_json` or `invalid_event_schema`) and `validation_errors`. Structured
35
+ details exclude input values and validator context. Keep `.data`, exception
36
+ chaining and the existing constructor; raw data and chained causes can contain
37
+ sensitive values and are never logged automatically by the SDK.
38
+ - Add request/diagnostic/error regression tests for both Python HTTP backends and
39
+ installed-wheel consumers, plus a documented validation support matrix.
40
+
41
+ ### Upgrade from 0.2.0
42
+
43
+ 1. Upgrade the generator to 0.3.0 and regenerate with your existing HTTP backend.
44
+ 2. Validate consumer call sites, including invalid requests, before adoption.
45
+ Ordinary Pydantic coercion applies; valid numeric serialization is preserved.
46
+ 3. Generate JSON diagnostics and review exact allowances before enabling strict
47
+ CI. Changed locations, constraints, values, operations or targets need review.
48
+ 4. Read stream failure categories and structured details through public fields.
49
+ Keep raw payloads and full exception chains out of routine logs.
50
+
6
51
  ## 0.2.0 - 2026-10-18
7
52
 
8
53
  - 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.2.0
3
+ Version: 0.3.0
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,38 @@ remain ordinary members of the user-defined union.
121
121
 
122
122
  ## HTTP error contracts
123
123
 
124
+ Python request parameters and JSON bodies are validated when calling the method;
125
+ catch Pydantic `ValidationError` around that call. Network and event errors occur
126
+ while iterating. See the [validation matrix and CI policy](codegen.md#validation-support-matrix).
127
+
128
+ Generated Python `StreamParseError` provides these public fields:
129
+
130
+ | Field | Meaning |
131
+ | --- | --- |
132
+ | `operation_id` | Source OpenAPI operation ID |
133
+ | `category` | `invalid_json` for malformed JSON; `invalid_event_schema` for invalid event shapes/constraints |
134
+ | `validation_errors` | Pydantic error dictionaries with `type`, `loc`, `msg`; input values, context and URLs are excluded |
135
+ | `data` | Original SSE data payload, retained for explicit inspection |
136
+
137
+ For errors raised by generated operations, category and operation ID are always
138
+ set. The existing `StreamParseError(data)` constructor remains valid; manually
139
+ constructed errors can have `None` provenance/category and empty details.
140
+ The original Pydantic `ValidationError` remains chained as `__cause__`.
141
+
142
+ ```python
143
+ try:
144
+ async for event in client.runs.run_events(run_id="run_abc"):
145
+ handle(event)
146
+ except StreamParseError as error:
147
+ record_failure(error.operation_id, error.category, error.validation_errors)
148
+ ```
149
+
150
+ The SDK does not log event payloads or exceptions automatically. Raw `.data` and
151
+ the full chained exception may contain sensitive values; log only reviewed fields.
152
+ Even structured messages and field names should follow the consumer's logging
153
+ policy. Malformed known events still fail with `unknown_events="ignore"`; only
154
+ valid JSON objects with unknown string discriminator tags can be skipped.
155
+
124
156
  The SDK works with ordinary OpenAPI response declarations, including
125
157
  `application/json`, `application/problem+json`, and other declared response media
126
158
  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,73 @@ 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
+ `unsupported-constraint` identifies an unenforced rule for the selected target;
195
+ TypeScript also diagnoses Python-supported scalar constraints because TypeScript
196
+ has no runtime schema validator. Python's `optional-discriminator` diagnoses event
197
+ selection risks without changing field requiredness. Human-readable warnings
198
+ contain the same location, keyword/value, operation and limitation as the report.
199
+
200
+ Without `--strict`, unaccepted diagnostics remain warnings. With it, any
201
+ unaccepted diagnostic fails with exit code 2 before generated files change; drift
202
+ still uses exit code 1. Reports are written even on strict-policy failure. Keep
203
+ the report outside generated output and separate from schema/allowance inputs.
204
+
205
+ Review the report and explicitly record an allowance, for example:
206
+
207
+ ```json
208
+ {
209
+ "allow": [
210
+ {
211
+ "id": "unsupported-constraint:<copy the exact ID from the report>",
212
+ "reason": "Reviewed upstream UUID validation; client remains a string."
213
+ }
214
+ ]
215
+ }
216
+ ```
217
+
218
+ IDs include code, source pointer, keyword/value, target and operation context;
219
+ message wording and schema filename are excluded. Changed constraints or contexts
220
+ need a new review. Empty reasons, duplicate IDs, malformed files and stale
221
+ allowances fail explicitly. Keep separate policies for each contract/target.
222
+ Repository drift checks use strict mode with reviewed TypeScript allowances.
223
+
224
+ Python generator API users can collect the same objects with
225
+ `build_ir(document, target="python", diagnostics=items)` where `items` is an empty
226
+ list. Omitting `diagnostics` emits warnings; the default target is Python. Select
227
+ the matching target explicitly before calling the corresponding generator.
228
+
153
229
  ## Existing native streams and mixed REST generation
154
230
 
155
231
  See the [native transport and mixed-codegen recipe](native-transports.md) when
@@ -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.data)
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 = TypeAdapter(Models.RunRequest)
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
- TypeAdapter(Models.RunEvent),
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
- TypeAdapter(Models.RunEvent),
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,11 @@ 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 typing import Literal
8
+ from typing import Literal, cast
9
9
 
10
10
  import httpx
11
11
  from pydantic import JsonValue, TypeAdapter, ValidationError
12
+ from pydantic_core import ErrorDetails
12
13
 
13
14
 
14
15
  class TransportError(Exception):
@@ -24,8 +25,20 @@ class HttpError(TransportError):
24
25
 
25
26
 
26
27
  class StreamParseError(TransportError):
27
- def __init__(self, data: str) -> None:
28
+ """Invalid event data. Raw data and chained exceptions may contain secrets."""
29
+
30
+ def __init__(
31
+ self,
32
+ data: str,
33
+ *,
34
+ operation_id: str | None = None,
35
+ category: Literal["invalid_json", "invalid_event_schema"] | None = None,
36
+ validation_errors: list[ErrorDetails] | None = None,
37
+ ) -> None:
28
38
  self.data = data
39
+ self.operation_id = operation_id
40
+ self.category = category
41
+ self.validation_errors = validation_errors or []
29
42
  super().__init__("Invalid JSON or event schema in SSE data")
30
43
 
31
44
 
@@ -43,6 +56,11 @@ class RequestOptions:
43
56
  type ParameterValue = str | int | float | bool | None
44
57
 
45
58
 
59
+ def validate_parameter[T](adapter: TypeAdapter[T], value: T) -> T:
60
+ """Validate/coerce inputs while preserving valid numeric wire representations."""
61
+ return _preserve_numbers(value, adapter.validate_python(value))
62
+
63
+
46
64
  def encode_path(value: str | int | float | bool | None) -> str:
47
65
  from urllib.parse import quote
48
66
 
@@ -155,6 +173,7 @@ class Transport:
155
173
  method: str,
156
174
  adapter: TypeAdapter[T],
157
175
  *,
176
+ operation_id: str | None = None,
158
177
  query: list[tuple[str, str]],
159
178
  headers: Mapping[str, str],
160
179
  body: bytes | None = None,
@@ -226,19 +245,62 @@ class Transport:
226
245
  try:
227
246
  yield adapter.validate_json(payload)
228
247
  except ValidationError as exc:
248
+ # Exclude input and arbitrary validator context from public
249
+ # diagnostics. The original cause remains available explicitly.
250
+ details = exc.errors(
251
+ include_input=False,
252
+ include_context=False,
253
+ include_url=False,
254
+ )
255
+ category: Literal["invalid_json", "invalid_event_schema"] = (
256
+ "invalid_json"
257
+ if any(error["type"] == "json_invalid" for error in details)
258
+ else "invalid_event_schema"
259
+ )
229
260
  if discriminator is not None:
230
261
  try:
231
262
  value = TypeAdapter[JsonValue](JsonValue).validate_json(
232
263
  payload
233
264
  )
234
265
  except ValidationError:
235
- raise StreamParseError(payload) from exc
266
+ raise StreamParseError(
267
+ payload,
268
+ operation_id=operation_id,
269
+ category="invalid_json",
270
+ validation_errors=details,
271
+ ) from exc
236
272
  if isinstance(value, dict):
237
273
  tag = value.get(discriminator)
238
274
  if isinstance(tag, str) and tag not in known_tags:
239
275
  continue
240
- raise StreamParseError(payload) from exc
276
+ raise StreamParseError(
277
+ payload,
278
+ operation_id=operation_id,
279
+ category=category,
280
+ validation_errors=details,
281
+ ) from exc
241
282
  except httpx.HTTPError as exc:
242
283
  raise TransportError("SSE HTTP transport failed") from exc
243
284
  except UnicodeDecodeError as exc:
244
285
  raise TransportError("Invalid UTF-8 in SSE stream") from exc
286
+
287
+
288
+ def _preserve_numbers[T](original: object, validated: T) -> T:
289
+ if (
290
+ type(original) is int
291
+ and isinstance(validated, float)
292
+ and validated.is_integer()
293
+ and original == int(validated)
294
+ ):
295
+ return cast(T, original)
296
+ if isinstance(original, list | tuple) and isinstance(validated, list):
297
+ before = cast(Sequence[object], original)
298
+ after = cast(list[object], validated)
299
+ return cast(
300
+ T,
301
+ [
302
+ _preserve_numbers(old, new)
303
+ for old, new in zip(before, after, strict=True)
304
+ ],
305
+ )
306
+ return validated
@@ -1,4 +1,4 @@
1
1
  from fastapi_streams.router import StreamRouter
2
2
 
3
3
  __all__ = ["StreamRouter"]
4
- __version__ = "0.2.0"
4
+ __version__ = "0.3.0"
@@ -1,15 +1,20 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
2
4
  import importlib
3
5
  import json
4
6
  import sys
5
7
  from pathlib import Path
6
- from typing import cast
8
+ from typing import TYPE_CHECKING, cast
7
9
 
8
10
  from fastapi import FastAPI
9
11
 
10
12
  from fastapi_streams.codegen.openapi import ContractError, Document, export_schema
11
13
  from fastapi_streams.codegen.writer import write_files
12
14
 
15
+ if TYPE_CHECKING:
16
+ from fastapi_streams.codegen.diagnostics import GenerationDiagnostic
17
+
13
18
 
14
19
  def main(argv: list[str] | None = None) -> int:
15
20
  parser = argparse.ArgumentParser(prog="fastapi-streams")
@@ -30,6 +35,15 @@ def main(argv: list[str] | None = None) -> int:
30
35
  "--language", choices=["typescript", "python"], default="typescript"
31
36
  )
32
37
  command.add_argument("--output", required=True, type=Path)
38
+ command.add_argument("--diagnostics", type=Path, help="Write JSON diagnostics")
39
+ command.add_argument(
40
+ "--strict", action="store_true", help="Fail on unaccepted diagnostics"
41
+ )
42
+ command.add_argument(
43
+ "--allow-diagnostics",
44
+ type=Path,
45
+ help="JSON allowances with exact IDs and review reasons",
46
+ )
33
47
  command.add_argument(
34
48
  "--python-http-client",
35
49
  choices=["httpx", "httpx2"],
@@ -65,8 +79,23 @@ def main(argv: list[str] | None = None) -> int:
65
79
  output.parent.mkdir(parents=True, exist_ok=True)
66
80
  output.write_text(content, encoding="utf-8", newline="\n")
67
81
  else:
82
+ if args.diagnostics:
83
+ report = args.diagnostics.resolve()
84
+ output = args.output.resolve()
85
+ inputs = {args.schema.resolve()}
86
+ if args.allow_diagnostics:
87
+ inputs.add(args.allow_diagnostics.resolve())
88
+ if report in inputs or report == output or output in report.parents:
89
+ raise ContractError(
90
+ "--diagnostics must be outside the generated output "
91
+ "and input files"
92
+ )
68
93
  if args.language != "python" and args.python_http_client is not None:
69
94
  raise ContractError("--python-http-client requires --language python")
95
+ from fastapi_streams.codegen.diagnostics import (
96
+ accepted_diagnostics,
97
+ emit_diagnostics,
98
+ )
70
99
  from fastapi_streams.codegen.ir import build_ir
71
100
  from fastapi_streams.codegen.python import generate_python
72
101
  from fastapi_streams.codegen.typescript import generate_typescript
@@ -82,7 +111,29 @@ def main(argv: list[str] | None = None) -> int:
82
111
  file=sys.stderr,
83
112
  )
84
113
  return 1
85
- contract = build_ir(cast(Document, document))
114
+ diagnostics: list[GenerationDiagnostic] = []
115
+ contract = build_ir(
116
+ cast(Document, document), target=args.language, diagnostics=diagnostics
117
+ )
118
+ # Reports precede strict-policy failures, so CI can review new IDs.
119
+ if args.diagnostics:
120
+ _write_diagnostics(args.diagnostics, diagnostics, set())
121
+ accepted: set[str] = (
122
+ accepted_diagnostics(
123
+ diagnostics, json.loads(args.allow_diagnostics.read_text("utf-8"))
124
+ )
125
+ if args.allow_diagnostics
126
+ else set()
127
+ )
128
+ if args.diagnostics:
129
+ _write_diagnostics(args.diagnostics, diagnostics, accepted)
130
+ pending = [item for item in diagnostics if item.id not in accepted]
131
+ emit_diagnostics(pending)
132
+ if args.strict and pending:
133
+ raise ContractError(
134
+ f"{len(pending)} unaccepted generation diagnostics; "
135
+ "review --diagnostics and provide explicit --allow-diagnostics"
136
+ )
86
137
  generated = (
87
138
  generate_python(
88
139
  contract, http_client=args.python_http_client or "httpx"
@@ -116,5 +167,27 @@ def _load_app(value: str) -> FastAPI:
116
167
  return app
117
168
 
118
169
 
170
+ def _write_diagnostics(
171
+ path: Path, diagnostics: list[GenerationDiagnostic], accepted: set[str]
172
+ ) -> None:
173
+ path.parent.mkdir(parents=True, exist_ok=True)
174
+ path.write_text(
175
+ json.dumps(
176
+ {
177
+ "version": 1,
178
+ "diagnostics": [
179
+ item.to_dict(accepted=item.id in accepted) for item in diagnostics
180
+ ],
181
+ },
182
+ indent=2,
183
+ sort_keys=True,
184
+ ensure_ascii=False,
185
+ )
186
+ + "\n",
187
+ encoding="utf-8",
188
+ newline="\n",
189
+ )
190
+
191
+
119
192
  if __name__ == "__main__":
120
193
  sys.exit(main())