fastapi-streams 0.3.0__tar.gz → 0.4.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 (141) hide show
  1. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/CHANGELOG.md +67 -0
  2. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/PKG-INFO +1 -1
  3. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/clients.md +68 -0
  4. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/codegen.md +49 -0
  5. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/development.md +29 -0
  6. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/errors.py +3 -3
  7. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/errors.ts +9 -2
  8. fastapi_streams-0.4.0/examples/agent_runs/generated/.fastapi-streams.json +19 -0
  9. fastapi_streams-0.4.0/examples/agent_runs/generated_python/.fastapi-streams.json +19 -0
  10. {fastapi_streams-0.3.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.4.0/examples/agent_runs/generated_python}/runtime.py +29 -31
  11. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/__init__.py +1 -1
  12. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/cli.py +71 -7
  13. {fastapi_streams-0.3.0/examples/agent_runs/generated_python → fastapi_streams-0.4.0/fastapi_streams/codegen/templates/python}/runtime.py +29 -31
  14. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/writer.py +9 -2
  15. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/pyproject.toml +1 -1
  16. fastapi_streams-0.4.0/scripts/benchmark_runtime.mjs +93 -0
  17. fastapi_streams-0.4.0/scripts/benchmark_runtime.py +293 -0
  18. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/smoke_package.py +23 -7
  19. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_codegen.py +38 -0
  20. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_nested_models.py +20 -10
  21. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_python_client.py +14 -4
  22. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_structured_diagnostics.py +77 -0
  23. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_unknown_events.py +50 -0
  24. fastapi_streams-0.4.0/tests/fixtures/generated/.fastapi-streams.json +19 -0
  25. fastapi_streams-0.4.0/tests/fixtures/generated_python/.fastapi-streams.json +19 -0
  26. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/runtime.py +29 -31
  27. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/errors.test.ts +16 -1
  28. fastapi_streams-0.3.0/examples/agent_runs/generated/.fastapi-streams.json +0 -11
  29. fastapi_streams-0.3.0/examples/agent_runs/generated_python/.fastapi-streams.json +0 -11
  30. fastapi_streams-0.3.0/tests/fixtures/generated/.fastapi-streams.json +0 -11
  31. fastapi_streams-0.3.0/tests/fixtures/generated_python/.fastapi-streams.json +0 -11
  32. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.gitattributes +0 -0
  33. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/ci.yml +0 -0
  34. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/compatibility.yml +0 -0
  35. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/release.yml +0 -0
  36. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.gitignore +0 -0
  37. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.pre-commit-config.yaml +0 -0
  38. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/AGENTS.md +0 -0
  39. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/LICENSE +0 -0
  40. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/README.md +0 -0
  41. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/README.md +0 -0
  42. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/architecture.md +0 -0
  43. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/native-transports.md +0 -0
  44. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/quickstart.md +0 -0
  45. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/releasing.md +0 -0
  46. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/__init__.py +0 -0
  47. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/__init__.py +0 -0
  48. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/consume.py +0 -0
  49. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/consume.ts +0 -0
  50. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
  51. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
  52. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/client.ts +0 -0
  53. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/index.ts +0 -0
  54. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/errors.ts +0 -0
  55. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/sse.ts +0 -0
  56. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/types.ts +0 -0
  57. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/__init__.py +0 -0
  58. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
  59. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/api.py +0 -0
  60. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/client.py +0 -0
  61. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/models.py +0 -0
  62. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/py.typed +0 -0
  63. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/native_proxy.py +0 -0
  64. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/rest_codegen.py +0 -0
  65. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/__init__.py +0 -0
  66. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/diagnostics.py +0 -0
  67. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/documentation.py +0 -0
  68. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/enums.py +0 -0
  69. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/ir.py +0 -0
  70. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/names.py +0 -0
  71. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/openapi.py +0 -0
  72. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/python.py +0 -0
  73. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/LICENSE +0 -0
  74. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/README.md +0 -0
  75. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
  76. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/errors.ts +0 -0
  77. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/api.py.j2 +0 -0
  78. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
  79. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/sse.ts +0 -0
  80. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
  81. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
  82. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templating.py +0 -0
  83. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/typescript.py +0 -0
  84. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/py.typed +0 -0
  85. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/router.py +0 -0
  86. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/package-lock.json +0 -0
  87. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/package.json +0 -0
  88. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/check_package.py +0 -0
  89. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/check_release.py +0 -0
  90. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/codegen.py +0 -0
  91. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/test_compat.py +0 -0
  92. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/__init__.py +0 -0
  93. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/helpers.py +0 -0
  94. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_additional_properties.py +0 -0
  95. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_contract_boundaries.py +0 -0
  96. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_diagnostics.py +0 -0
  97. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_discriminators.py +0 -0
  98. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_documentation.py +0 -0
  99. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_error_contracts.py +0 -0
  100. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_field_names.py +0 -0
  101. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_http_backends.py +0 -0
  102. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_intersections.py +0 -0
  103. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_literal_fields.py +0 -0
  104. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_native_contract.py +0 -0
  105. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_python_typing.py +0 -0
  106. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_request_validation.py +0 -0
  107. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_required_properties.py +0 -0
  108. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_scalar_constraints.py +0 -0
  109. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_schema_matrix.py +0 -0
  110. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_stream_validation_details.py +0 -0
  111. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/contracts/test_openapi.py +0 -0
  112. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/README.md +0 -0
  113. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/__init__.py +0 -0
  114. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.openapi.json +0 -0
  115. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.typescript.diagnostics.json +0 -0
  116. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/compat-client.ts +0 -0
  117. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/client.ts +0 -0
  118. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/api.ts +0 -0
  119. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/index.ts +0 -0
  120. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/index.ts +0 -0
  121. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/errors.ts +0 -0
  122. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/sse.ts +0 -0
  123. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/types.ts +0 -0
  124. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/__init__.py +0 -0
  125. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/client.py +0 -0
  126. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
  127. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/api.py +0 -0
  128. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/models.py +0 -0
  129. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/py.typed +0 -0
  130. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/integration_app.py +0 -0
  131. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/models.openapi.json +0 -0
  132. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/models_app.py +0 -0
  133. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/integration/test_end_to_end.py +0 -0
  134. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/router/test_router.py +0 -0
  135. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/test_release.py +0 -0
  136. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/live-client.ts +0 -0
  137. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/models.test.ts +0 -0
  138. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/narrowing.ts +0 -0
  139. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/sse.test.ts +0 -0
  140. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/tsconfig.json +0 -0
  141. {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typing/client_usage.py +0 -0
@@ -3,6 +3,73 @@
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.4.0 - 2026-10-18
7
+
8
+ ### Breaking changes
9
+
10
+ - CLI drift checks now include SDK provenance in `.fastapi-streams.json`.
11
+ Existing files-only manifests, a different generator version, or a changed
12
+ source schema now fail `check`, including source changes that leave generated
13
+ code identical. Regenerate once with the existing language and HTTP backend,
14
+ review and commit the updated manifest. This stricter drift check requires
15
+ a minor release under the pre-1.0 version policy.
16
+
17
+ ### Added
18
+
19
+ - Always record generator/version, canonical schema SHA-256, target and Python
20
+ HTTP backend in CLI-generated manifests, sharing metadata with diagnostic
21
+ reports. Preserve `files` ownership, safe cleanup and read-only checks.
22
+ Low-level file writers can supply optional `generation` metadata.
23
+ - Document optional operation/status-specific error decoders using the existing
24
+ TypeScript guard hook and cached Python TypeAdapters. Add a checked consumer
25
+ decoder example; unexpected JSON/text/proxy errors retain the generic fallback.
26
+ Generated request/event APIs, HTTP/SSE contracts and runtime dependencies stay
27
+ unchanged.
28
+
29
+ ### Upgrade from 0.3.1
30
+
31
+ 1. Upgrade the generator to 0.4.0 and regenerate with the same language and
32
+ Python HTTP backend. Commit `.fastapi-streams.json` with the generated SDK.
33
+ 2. Expect a one-time manifest diff. CI checks now detect schema or generator
34
+ provenance changes even when generated source code is otherwise unchanged.
35
+ 3. Existing callers, HTTP/SSE contracts and exact diagnostic allowances require
36
+ no migration. Optional error decoders remain consumer-controlled.
37
+
38
+ ## 0.3.1 - 2026-10-18
39
+
40
+ ### Added
41
+
42
+ - Repeatable Python and Node benchmarks for generated SDK request preparation,
43
+ decoding/validation, SSE framing, complete consumption, fragmented chunks and
44
+ Python unknown-event ignoring. Reports include individual timing samples and
45
+ optional separate Python peak-memory measurements; no CI timing thresholds.
46
+ - Add generation provenance and validation capabilities to version 1 diagnostic
47
+ reports: canonical schema SHA-256, generator version, target/HTTP backend and
48
+ request/event/error validation phases. Existing diagnostic fields, IDs and
49
+ exact reviewed allowances remain unchanged. Additional report object keys are
50
+ additive; the metadata does not introduce runtime checks or full JSON Schema
51
+ guarantees.
52
+
53
+ ### Performance
54
+
55
+ - Reuse the JSON-value adapter for skipped unknown events and HTTP-error JSON
56
+ decoding. Cache top-level discriminator metadata per generated event adapter
57
+ instead of rebuilding its JSON Schema for every ignore-policy stream.
58
+ - Prepare sanitized validation details only for errors that are actually raised.
59
+ Known events still use direct Pydantic JSON validation; malformed known events,
60
+ missing/non-string tags, invalid JSON and unknown nested variants still fail.
61
+ Public signatures, coercion, validation timing and wire encoding are unchanged.
62
+
63
+ ### Upgrade from 0.3.0
64
+
65
+ 1. Upgrade the generator to 0.3.1 and regenerate with the existing HTTP backend
66
+ to obtain the Python runtime improvements. Existing generated SDKs keep working.
67
+ 2. Diagnostic-report readers should tolerate the new `generation` and `validation`
68
+ keys. Existing exact diagnostic allowances need no migration.
69
+ 3. Continue validating server requests and domain invariants at their own
70
+ boundaries; consumers should reuse parsed events instead of validating the
71
+ same schema again. TypeScript remains static-only with no added dependency.
72
+
6
73
  ## 0.3.0 - 2026-10-18
7
74
 
8
75
  ### Breaking changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fastapi-streams
3
- Version: 0.3.0
3
+ Version: 0.4.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,15 @@ 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
+
124
133
  Python request parameters and JSON bodies are validated when calling the method;
125
134
  catch Pydantic `ValidationError` around that call. Network and event errors occur
126
135
  while iterating. See the [validation matrix and CI policy](codegen.md#validation-support-matrix).
@@ -186,6 +195,65 @@ the original status, response, and diagnostics. Schema unions with permissive
186
195
  variants may accept a broad range of bodies; inspect the parsed variant before
187
196
  using its fields. Existing limits on bounded HTTP diagnostics still apply.
188
197
 
198
+ ### Optional consumer decoders
199
+
200
+ Build a decoder once per operation/status around the existing guard hook when
201
+ several callers need the same declared error body. For example, this adapter
202
+ accepts a consumer-provided validator with no additional SDK dependency:
203
+
204
+ ```typescript
205
+ import { isHttpError, type StreamRunEventsErrorBody } from "./generated/index.js";
206
+
207
+ function decodeValidationError(
208
+ error: unknown,
209
+ isBody: (value: unknown) => value is StreamRunEventsErrorBody,
210
+ ): StreamRunEventsErrorBody | undefined {
211
+ if (isHttpError(error, isBody) && error.status === 422) return error.details;
212
+ return undefined;
213
+ }
214
+ ```
215
+
216
+ Supply a handwritten guard or an existing validator that checks every field of
217
+ the generated alias. A type assertion alone does not validate a response. Guards
218
+ must validate the original value: if a decoder coerces or transforms data, use
219
+ its successfully parsed output instead of narrowing the original `error.details`.
220
+ Use this helper only for the operation whose errors the alias describes.
221
+
222
+ If decoding returns `undefined`, continue handling the original `HttpError`
223
+ generically or rethrow it. Its status, response and JSON/text details remain
224
+ available, including unexpected proxy responses. Validators run only for SDK
225
+ HTTP errors; `isHttpError` leaves network/stream failures alone. Status checks
226
+ remain explicit because a matching body does not establish an HTTP status.
227
+ The [TypeScript example](../examples/agent_runs/errors.ts) includes a complete
228
+ guard and reusable decoder with this fallback behavior.
229
+
230
+ In Python, construct and reuse an adapter rather than rebuilding it for each
231
+ exception:
232
+
233
+ ```python
234
+ from pydantic import TypeAdapter, ValidationError
235
+ from myapp.generated import HttpError
236
+ from myapp.generated.models import StreamRunEventsErrorBody
237
+
238
+ error_body = TypeAdapter(StreamRunEventsErrorBody)
239
+
240
+
241
+ def decode_validation_error(error: HttpError) -> StreamRunEventsErrorBody | None:
242
+ if error.status != 422:
243
+ return None
244
+ try:
245
+ return error_body.validate_python(error.details)
246
+ except ValidationError:
247
+ return None
248
+ ```
249
+
250
+ Use the returned parsed body on success and retain the original exception for
251
+ fallback handling. This alias describes an object response, so `None` can
252
+ represent failure here; for a nullable body alias, use a separate success flag
253
+ or sentinel. Neither recipe changes SDK defaults or automatically validates all
254
+ HTTP failures. Diagnostic reports correctly retain `consumer_controlled` for
255
+ HTTP-error body validation.
256
+
189
257
  The checked consumer examples show HTTP validation errors, fallback handling,
190
258
  stream parsing/transport errors, and ordinary application error events:
191
259
 
@@ -117,6 +117,35 @@ generated files without deleting neighboring handwritten files. Use a dedicated
117
117
  output directory for each language. Unsafe paths and generated-name collisions
118
118
  fail explicitly.
119
119
 
120
+ ### SDK provenance
121
+
122
+ CLI generation always writes `.fastapi-streams.json` alongside the SDK. Commit
123
+ this file with the generated sources. Its existing `files` list still controls
124
+ ownership and cleanup; the additional `generation` object contains the same
125
+ provenance as an optional diagnostic report:
126
+
127
+ | Field | Meaning |
128
+ | --- | --- |
129
+ | `generator`, `generator_version` | Generator distribution and version |
130
+ | `schema_sha256` | SHA-256 of the complete source OpenAPI document |
131
+ | `schema_hash_encoding` | `canonical-json-v1`: JSON with recursively sorted keys, compact separators and UTF-8 without ASCII escaping |
132
+ | `target` | `python` or `typescript` |
133
+ | `python_http_client` | `httpx` / `httpx2` for Python, JSON null for TypeScript |
134
+
135
+ There are no timestamps or local paths. Formatting and object-key order do not
136
+ affect the schema hash; changes to any schema content do, including descriptions
137
+ and components unused by the SDK. This identifies the input and configuration;
138
+ it is not a signature or an integrity check of the generated sources.
139
+
140
+ `check` compares the manifest as well as SDK sources. A different generator
141
+ version or source hash is drift even when the emitted code is identical.
142
+ Regenerate once with the chosen language and existing HTTP backend to upgrade a
143
+ legacy files-only manifest, then commit the manifest diff. Checks remain
144
+ read-only; generation retains neighboring handwritten files and only removes
145
+ previously owned stale files. No diagnostic report is required for provenance.
146
+ The low-level `write_files` API accepts optional `generation` metadata; without
147
+ it, its files-only behavior remains available for callers writing raw file maps.
148
+
120
149
  ## Runtime validation and unknown variants
121
150
 
122
151
  Python validates path, query and header parameters and JSON request bodies at the
@@ -191,6 +220,26 @@ including inline schemas and referenced component definitions. Shared definition
191
220
  identify each affected operation; unused components do not generate warnings.
192
221
  SSE framing metadata is outside generated event-model validation.
193
222
 
223
+ The version 1 report also carries additive `generation` and `validation` objects.
224
+ Consumers should tolerate additional object keys. `generation` records the
225
+ generator name/version, target, selected Python HTTP library (null for TypeScript)
226
+ and `schema_sha256`. The hash covers the complete parsed OpenAPI document,
227
+ including unused definitions. Its `canonical-json-v1` encoding is Python
228
+ `json.dumps(document, sort_keys=True, separators=(",", ":"), ensure_ascii=False)`
229
+ encoded as UTF-8. Whitespace and object-key order in the input file do not affect
230
+ the hash; array order and values do. It is a content identifier, not a signature
231
+ or proof of schema enforcement.
232
+
233
+ `validation` distinguishes parameter/body validation at `method_call`, event
234
+ decoding at `iteration` and HTTP-error handling at `consumer_controlled`.
235
+ TypeScript parameter/body schema checks are `not_performed`. Each boundary has
236
+ a `schema_validation` boolean, with `json_decoding: true` for events. Python's
237
+ `runtime_scope` is `generated_types_and_supported_constraints`; TypeScript's is
238
+ `json_decoding_only`. Both set `full_json_schema: false`. These target capabilities
239
+ describe supported boundaries, even if an operation has no parameters or body;
240
+ source-specific limitations remain in `diagnostics` with their accepted status.
241
+ The report is generated only when requested and adds no SDK runtime checks.
242
+
194
243
  `unsupported-constraint` identifies an unenforced rule for the selected target;
195
244
  TypeScript also diagnoses Python-supported scalar constraints because TypeScript
196
245
  has no runtime schema validator. Python's `optional-discriminator` diagnoses event
@@ -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;
@@ -8,6 +8,8 @@ from examples.agent_runs.generated_python import (
8
8
  )
9
9
  from examples.agent_runs.generated_python.models import StreamRunEventsErrorBody
10
10
 
11
+ _ERROR_BODY = TypeAdapter(StreamRunEventsErrorBody)
12
+
11
13
 
12
14
  async def consume_with_errors(client: Client, run_id: str) -> None:
13
15
  try:
@@ -19,9 +21,7 @@ async def consume_with_errors(client: Client, run_id: str) -> None:
19
21
  except HttpError as error:
20
22
  if error.status == 422:
21
23
  try:
22
- body = TypeAdapter(StreamRunEventsErrorBody).validate_python(
23
- error.details
24
- )
24
+ body = _ERROR_BODY.validate_python(error.details)
25
25
  except ValidationError:
26
26
  pass
27
27
  else:
@@ -15,8 +15,9 @@ export async function consumeWithErrors(
15
15
  }
16
16
  } catch (error: unknown) {
17
17
  if (options.signal?.aborted) throw error;
18
- if (isHttpError(error, isValidationErrorBody) && error.status === 422) {
19
- console.error("Invalid stream parameters", error.details.detail ?? []);
18
+ const body = decodeValidationError(error);
19
+ if (body !== undefined) {
20
+ console.error("Invalid stream parameters", body.detail ?? []);
20
21
  return;
21
22
  }
22
23
  if (error instanceof HttpError) {
@@ -30,6 +31,12 @@ export async function consumeWithErrors(
30
31
  }
31
32
  }
32
33
 
34
+ // Consumer-local decoder: scope the generated body alias to this operation/status.
35
+ export function decodeValidationError(error: unknown): StreamRunEventsErrorBody | undefined {
36
+ if (isHttpError(error, isValidationErrorBody) && error.status === 422) return error.details;
37
+ return undefined;
38
+ }
39
+
33
40
  function isValidationErrorBody(value: unknown): value is StreamRunEventsErrorBody {
34
41
  if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
35
42
  if (!("detail" in value)) return true;
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ "agent-runs/api.ts",
4
+ "agent-runs/index.ts",
5
+ "client.ts",
6
+ "index.ts",
7
+ "transport/errors.ts",
8
+ "transport/sse.ts",
9
+ "types.ts"
10
+ ],
11
+ "generation": {
12
+ "generator": "fastapi-streams",
13
+ "generator_version": "0.4.0",
14
+ "python_http_client": null,
15
+ "schema_hash_encoding": "canonical-json-v1",
16
+ "schema_sha256": "f23def53557e9d381bada00de07054f0a105eceaefdf5f47eb3168a0e8a05c7f",
17
+ "target": "typescript"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ "__init__.py",
4
+ "agent_runs/__init__.py",
5
+ "agent_runs/api.py",
6
+ "client.py",
7
+ "models.py",
8
+ "py.typed",
9
+ "runtime.py"
10
+ ],
11
+ "generation": {
12
+ "generator": "fastapi-streams",
13
+ "generator_version": "0.4.0",
14
+ "python_http_client": "httpx",
15
+ "schema_hash_encoding": "canonical-json-v1",
16
+ "schema_sha256": "f23def53557e9d381bada00de07054f0a105eceaefdf5f47eb3168a0e8a05c7f",
17
+ "target": "python"
18
+ }
19
+ }
@@ -5,12 +5,15 @@ 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 functools import cache
8
9
  from typing import Literal, cast
9
10
 
10
11
  import httpx
11
12
  from pydantic import JsonValue, TypeAdapter, ValidationError
12
13
  from pydantic_core import ErrorDetails
13
14
 
15
+ _JSON_ADAPTER = TypeAdapter[JsonValue](JsonValue)
16
+
14
17
 
15
18
  class TransportError(Exception):
16
19
  pass
@@ -180,19 +183,11 @@ class Transport:
180
183
  options: RequestOptions | None = None,
181
184
  ) -> AsyncGenerator[T, None]:
182
185
  discriminator: str | None = None
183
- known_tags: set[str] = set()
186
+ known_tags: frozenset[str] = frozenset()
184
187
  if options and options.unknown_events not in {"error", "ignore"}:
185
188
  raise ValueError("unknown_events must be 'error' or 'ignore'")
186
189
  if options and options.unknown_events == "ignore":
187
- schema = adapter.json_schema()
188
- policy = schema.get("discriminator", {})
189
- discriminator = policy.get("propertyName")
190
- known_tags = set(policy.get("mapping", {}))
191
- if not isinstance(discriminator, str) or not known_tags:
192
- raise ValueError(
193
- "unknown_events='ignore' requires a top-level "
194
- "discriminated event union"
195
- )
190
+ discriminator, known_tags = _unknown_event_policy(adapter)
196
191
  request_headers = self.headers.copy()
197
192
  request_headers.update(headers)
198
193
  if options and options.headers:
@@ -223,11 +218,7 @@ class Transport:
223
218
  failure = exc
224
219
  text = raw.decode("utf-8", errors="replace")
225
220
  try:
226
- details = (
227
- TypeAdapter[JsonValue](JsonValue).validate_json(text)
228
- if text
229
- else None
230
- )
221
+ details = _JSON_ADAPTER.validate_json(text) if text else None
231
222
  except ValidationError:
232
223
  details = text
233
224
  raise HttpError(response, details) from failure
@@ -245,6 +236,16 @@ class Transport:
245
236
  try:
246
237
  yield adapter.validate_json(payload)
247
238
  except ValidationError as exc:
239
+ if discriminator is not None:
240
+ try:
241
+ value = _JSON_ADAPTER.validate_json(payload)
242
+ except ValidationError:
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
248
249
  # Exclude input and arbitrary validator context from public
249
250
  # diagnostics. The original cause remains available explicitly.
250
251
  details = exc.errors(
@@ -257,22 +258,6 @@ class Transport:
257
258
  if any(error["type"] == "json_invalid" for error in details)
258
259
  else "invalid_event_schema"
259
260
  )
260
- if discriminator is not None:
261
- try:
262
- value = TypeAdapter[JsonValue](JsonValue).validate_json(
263
- payload
264
- )
265
- except ValidationError:
266
- raise StreamParseError(
267
- payload,
268
- operation_id=operation_id,
269
- category="invalid_json",
270
- validation_errors=details,
271
- ) from exc
272
- if isinstance(value, dict):
273
- tag = value.get(discriminator)
274
- if isinstance(tag, str) and tag not in known_tags:
275
- continue
276
261
  raise StreamParseError(
277
262
  payload,
278
263
  operation_id=operation_id,
@@ -285,6 +270,19 @@ class Transport:
285
270
  raise TransportError("Invalid UTF-8 in SSE stream") from exc
286
271
 
287
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
+
288
286
  def _preserve_numbers[T](original: object, validated: T) -> T:
289
287
  if (
290
288
  type(original) is int
@@ -1,4 +1,4 @@
1
1
  from fastapi_streams.router import StreamRouter
2
2
 
3
3
  __all__ = ["StreamRouter"]
4
- __version__ = "0.3.0"
4
+ __version__ = "0.4.0"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import hashlib
4
5
  import importlib
5
6
  import json
6
7
  import sys
@@ -9,6 +10,7 @@ from typing import TYPE_CHECKING, cast
9
10
 
10
11
  from fastapi import FastAPI
11
12
 
13
+ from fastapi_streams import __version__
12
14
  from fastapi_streams.codegen.openapi import ContractError, Document, export_schema
13
15
  from fastapi_streams.codegen.writer import write_files
14
16
 
@@ -103,6 +105,10 @@ def main(argv: list[str] | None = None) -> int:
103
105
  document = json.loads(args.schema.read_text(encoding="utf-8"))
104
106
  if not isinstance(document, dict):
105
107
  raise ContractError("The OpenAPI document must be an object")
108
+ document = cast(Document, document)
109
+ generation = _generation_metadata(
110
+ document, args.language, args.python_http_client or "httpx"
111
+ )
106
112
  if args.command == "check" and args.app:
107
113
  expected = export_schema(_load_app(args.app))
108
114
  if expected != document:
@@ -112,12 +118,15 @@ def main(argv: list[str] | None = None) -> int:
112
118
  )
113
119
  return 1
114
120
  diagnostics: list[GenerationDiagnostic] = []
115
- contract = build_ir(
116
- cast(Document, document), target=args.language, diagnostics=diagnostics
117
- )
121
+ contract = build_ir(document, target=args.language, diagnostics=diagnostics)
118
122
  # Reports precede strict-policy failures, so CI can review new IDs.
119
123
  if args.diagnostics:
120
- _write_diagnostics(args.diagnostics, diagnostics, set())
124
+ _write_diagnostics(
125
+ args.diagnostics,
126
+ diagnostics,
127
+ set(),
128
+ generation,
129
+ )
121
130
  accepted: set[str] = (
122
131
  accepted_diagnostics(
123
132
  diagnostics, json.loads(args.allow_diagnostics.read_text("utf-8"))
@@ -126,7 +135,12 @@ def main(argv: list[str] | None = None) -> int:
126
135
  else set()
127
136
  )
128
137
  if args.diagnostics:
129
- _write_diagnostics(args.diagnostics, diagnostics, accepted)
138
+ _write_diagnostics(
139
+ args.diagnostics,
140
+ diagnostics,
141
+ accepted,
142
+ generation,
143
+ )
130
144
  pending = [item for item in diagnostics if item.id not in accepted]
131
145
  emit_diagnostics(pending)
132
146
  if args.strict and pending:
@@ -141,7 +155,9 @@ def main(argv: list[str] | None = None) -> int:
141
155
  if args.language == "python"
142
156
  else generate_typescript(contract)
143
157
  )
144
- changed = write_files(args.output, generated, check=args.check)
158
+ changed = write_files(
159
+ args.output, generated, check=args.check, generation=generation
160
+ )
145
161
  if args.check and changed:
146
162
  for path in changed:
147
163
  print(f"Out of date: {path}", file=sys.stderr)
@@ -167,14 +183,62 @@ def _load_app(value: str) -> FastAPI:
167
183
  return app
168
184
 
169
185
 
186
+ def _generation_metadata(
187
+ document: Document, target: str, http_client: str
188
+ ) -> dict[str, str | None]:
189
+ # Share canonical provenance between manifests and optional reports.
190
+ encoded = json.dumps(
191
+ document, sort_keys=True, separators=(",", ":"), ensure_ascii=False
192
+ ).encode("utf-8")
193
+ return {
194
+ "generator": "fastapi-streams",
195
+ "generator_version": __version__,
196
+ "schema_sha256": hashlib.sha256(encoded).hexdigest(),
197
+ "schema_hash_encoding": "canonical-json-v1",
198
+ "target": target,
199
+ "python_http_client": http_client if target == "python" else None,
200
+ }
201
+
202
+
170
203
  def _write_diagnostics(
171
- path: Path, diagnostics: list[GenerationDiagnostic], accepted: set[str]
204
+ path: Path,
205
+ diagnostics: list[GenerationDiagnostic],
206
+ accepted: set[str],
207
+ generation: dict[str, str | None],
172
208
  ) -> None:
209
+ python = generation["target"] == "python"
210
+ request_phase = "method_call" if python else "not_performed"
173
211
  path.parent.mkdir(parents=True, exist_ok=True)
174
212
  path.write_text(
175
213
  json.dumps(
176
214
  {
177
215
  "version": 1,
216
+ "generation": generation,
217
+ "validation": {
218
+ "full_json_schema": False,
219
+ "runtime_scope": (
220
+ "generated_types_and_supported_constraints"
221
+ if python
222
+ else "json_decoding_only"
223
+ ),
224
+ "parameters": {
225
+ "phase": request_phase,
226
+ "schema_validation": python,
227
+ },
228
+ "request_body": {
229
+ "phase": request_phase,
230
+ "schema_validation": python,
231
+ },
232
+ "events": {
233
+ "phase": "iteration",
234
+ "schema_validation": python,
235
+ "json_decoding": True,
236
+ },
237
+ "http_error_bodies": {
238
+ "phase": "consumer_controlled",
239
+ "schema_validation": False,
240
+ },
241
+ },
178
242
  "diagnostics": [
179
243
  item.to_dict(accepted=item.id in accepted) for item in diagnostics
180
244
  ],