fastapi-streams 0.3.1__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.1 → fastapi_streams-0.4.0}/CHANGELOG.md +32 -0
  2. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/PKG-INFO +1 -1
  3. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/clients.md +59 -0
  4. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/codegen.md +29 -0
  5. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/errors.py +3 -3
  6. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/errors.ts +9 -2
  7. fastapi_streams-0.4.0/examples/agent_runs/generated/.fastapi-streams.json +19 -0
  8. fastapi_streams-0.4.0/examples/agent_runs/generated_python/.fastapi-streams.json +19 -0
  9. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/__init__.py +1 -1
  10. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/cli.py +28 -23
  11. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/writer.py +9 -2
  12. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/pyproject.toml +1 -1
  13. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/smoke_package.py +5 -0
  14. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_codegen.py +38 -0
  15. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_nested_models.py +20 -10
  16. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_python_client.py +14 -4
  17. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_structured_diagnostics.py +11 -1
  18. fastapi_streams-0.4.0/tests/fixtures/generated/.fastapi-streams.json +19 -0
  19. fastapi_streams-0.4.0/tests/fixtures/generated_python/.fastapi-streams.json +19 -0
  20. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/errors.test.ts +16 -1
  21. fastapi_streams-0.3.1/examples/agent_runs/generated/.fastapi-streams.json +0 -11
  22. fastapi_streams-0.3.1/examples/agent_runs/generated_python/.fastapi-streams.json +0 -11
  23. fastapi_streams-0.3.1/tests/fixtures/generated/.fastapi-streams.json +0 -11
  24. fastapi_streams-0.3.1/tests/fixtures/generated_python/.fastapi-streams.json +0 -11
  25. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.gitattributes +0 -0
  26. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.github/workflows/ci.yml +0 -0
  27. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.github/workflows/compatibility.yml +0 -0
  28. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.github/workflows/release.yml +0 -0
  29. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.gitignore +0 -0
  30. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/.pre-commit-config.yaml +0 -0
  31. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/AGENTS.md +0 -0
  32. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/LICENSE +0 -0
  33. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/README.md +0 -0
  34. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/README.md +0 -0
  35. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/architecture.md +0 -0
  36. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/development.md +0 -0
  37. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/native-transports.md +0 -0
  38. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/quickstart.md +0 -0
  39. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/docs/releasing.md +0 -0
  40. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/__init__.py +0 -0
  41. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/__init__.py +0 -0
  42. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/consume.py +0 -0
  43. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/consume.ts +0 -0
  44. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
  45. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
  46. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/client.ts +0 -0
  47. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/index.ts +0 -0
  48. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/errors.ts +0 -0
  49. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/sse.ts +0 -0
  50. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated/types.ts +0 -0
  51. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/__init__.py +0 -0
  52. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
  53. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/api.py +0 -0
  54. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/client.py +0 -0
  55. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/models.py +0 -0
  56. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/py.typed +0 -0
  57. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/runtime.py +0 -0
  58. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/native_proxy.py +0 -0
  59. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/examples/rest_codegen.py +0 -0
  60. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/__init__.py +0 -0
  61. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/diagnostics.py +0 -0
  62. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/documentation.py +0 -0
  63. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/enums.py +0 -0
  64. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/ir.py +0 -0
  65. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/names.py +0 -0
  66. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/openapi.py +0 -0
  67. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/python.py +0 -0
  68. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/LICENSE +0 -0
  69. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/README.md +0 -0
  70. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
  71. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/errors.ts +0 -0
  72. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/api.py.j2 +0 -0
  73. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
  74. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/runtime.py +0 -0
  75. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/sse.ts +0 -0
  76. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
  77. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
  78. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templating.py +0 -0
  79. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/codegen/typescript.py +0 -0
  80. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/py.typed +0 -0
  81. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/fastapi_streams/router.py +0 -0
  82. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/package-lock.json +0 -0
  83. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/package.json +0 -0
  84. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/benchmark_runtime.mjs +0 -0
  85. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/benchmark_runtime.py +0 -0
  86. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/check_package.py +0 -0
  87. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/check_release.py +0 -0
  88. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/codegen.py +0 -0
  89. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/scripts/test_compat.py +0 -0
  90. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/__init__.py +0 -0
  91. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/helpers.py +0 -0
  92. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_additional_properties.py +0 -0
  93. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_contract_boundaries.py +0 -0
  94. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_diagnostics.py +0 -0
  95. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_discriminators.py +0 -0
  96. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_documentation.py +0 -0
  97. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_error_contracts.py +0 -0
  98. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_field_names.py +0 -0
  99. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_http_backends.py +0 -0
  100. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_intersections.py +0 -0
  101. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_literal_fields.py +0 -0
  102. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_native_contract.py +0 -0
  103. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_python_typing.py +0 -0
  104. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_request_validation.py +0 -0
  105. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_required_properties.py +0 -0
  106. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_scalar_constraints.py +0 -0
  107. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_schema_matrix.py +0 -0
  108. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_stream_validation_details.py +0 -0
  109. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/codegen/test_unknown_events.py +0 -0
  110. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/contracts/test_openapi.py +0 -0
  111. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/README.md +0 -0
  112. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/__init__.py +0 -0
  113. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.openapi.json +0 -0
  114. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.typescript.diagnostics.json +0 -0
  115. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/compat-client.ts +0 -0
  116. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/client.ts +0 -0
  117. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/api.ts +0 -0
  118. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/index.ts +0 -0
  119. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/index.ts +0 -0
  120. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/errors.ts +0 -0
  121. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/sse.ts +0 -0
  122. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated/types.ts +0 -0
  123. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/__init__.py +0 -0
  124. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/client.py +0 -0
  125. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
  126. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/api.py +0 -0
  127. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/models.py +0 -0
  128. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/py.typed +0 -0
  129. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/runtime.py +0 -0
  130. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/integration_app.py +0 -0
  131. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/models.openapi.json +0 -0
  132. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/fixtures/models_app.py +0 -0
  133. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/integration/test_end_to_end.py +0 -0
  134. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/router/test_router.py +0 -0
  135. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/test_release.py +0 -0
  136. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/live-client.ts +0 -0
  137. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/models.test.ts +0 -0
  138. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/narrowing.ts +0 -0
  139. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/sse.test.ts +0 -0
  140. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typescript/tsconfig.json +0 -0
  141. {fastapi_streams-0.3.1 → fastapi_streams-0.4.0}/tests/typing/client_usage.py +0 -0
@@ -3,6 +3,38 @@
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
+
6
38
  ## 0.3.1 - 2026-10-18
7
39
 
8
40
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fastapi-streams
3
- Version: 0.3.1
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
@@ -195,6 +195,65 @@ the original status, response, and diagnostics. Schema unions with permissive
195
195
  variants may accept a broad range of bodies; inspect the parsed variant before
196
196
  using its fields. Existing limits on bounded HTTP diagnostics still apply.
197
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
+
198
257
  The checked consumer examples show HTTP validation errors, fallback handling,
199
258
  stream parsing/transport errors, and ordinary application error events:
200
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
@@ -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
+ }
@@ -1,4 +1,4 @@
1
1
  from fastapi_streams.router import StreamRouter
2
2
 
3
3
  __all__ = ["StreamRouter"]
4
- __version__ = "0.3.1"
4
+ __version__ = "0.4.0"
@@ -106,6 +106,9 @@ def main(argv: list[str] | None = None) -> int:
106
106
  if not isinstance(document, dict):
107
107
  raise ContractError("The OpenAPI document must be an object")
108
108
  document = cast(Document, document)
109
+ generation = _generation_metadata(
110
+ document, args.language, args.python_http_client or "httpx"
111
+ )
109
112
  if args.command == "check" and args.app:
110
113
  expected = export_schema(_load_app(args.app))
111
114
  if expected != document:
@@ -122,9 +125,7 @@ def main(argv: list[str] | None = None) -> int:
122
125
  args.diagnostics,
123
126
  diagnostics,
124
127
  set(),
125
- document,
126
- args.language,
127
- args.python_http_client or "httpx",
128
+ generation,
128
129
  )
129
130
  accepted: set[str] = (
130
131
  accepted_diagnostics(
@@ -138,9 +139,7 @@ def main(argv: list[str] | None = None) -> int:
138
139
  args.diagnostics,
139
140
  diagnostics,
140
141
  accepted,
141
- document,
142
- args.language,
143
- args.python_http_client or "httpx",
142
+ generation,
144
143
  )
145
144
  pending = [item for item in diagnostics if item.id not in accepted]
146
145
  emit_diagnostics(pending)
@@ -156,7 +155,9 @@ def main(argv: list[str] | None = None) -> int:
156
155
  if args.language == "python"
157
156
  else generate_typescript(contract)
158
157
  )
159
- changed = write_files(args.output, generated, check=args.check)
158
+ changed = write_files(
159
+ args.output, generated, check=args.check, generation=generation
160
+ )
160
161
  if args.check and changed:
161
162
  for path in changed:
162
163
  print(f"Out of date: {path}", file=sys.stderr)
@@ -182,33 +183,37 @@ def _load_app(value: str) -> FastAPI:
182
183
  return app
183
184
 
184
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
+
185
203
  def _write_diagnostics(
186
204
  path: Path,
187
205
  diagnostics: list[GenerationDiagnostic],
188
206
  accepted: set[str],
189
- document: Document,
190
- target: str,
191
- http_client: str,
207
+ generation: dict[str, str | None],
192
208
  ) -> None:
193
- # Canonical JSON binds reports to schema content, independent of file layout.
194
- encoded = json.dumps(
195
- document, sort_keys=True, separators=(",", ":"), ensure_ascii=False
196
- ).encode("utf-8")
197
- python = target == "python"
209
+ python = generation["target"] == "python"
198
210
  request_phase = "method_call" if python else "not_performed"
199
211
  path.parent.mkdir(parents=True, exist_ok=True)
200
212
  path.write_text(
201
213
  json.dumps(
202
214
  {
203
215
  "version": 1,
204
- "generation": {
205
- "generator": "fastapi-streams",
206
- "generator_version": __version__,
207
- "schema_sha256": hashlib.sha256(encoded).hexdigest(),
208
- "schema_hash_encoding": "canonical-json-v1",
209
- "target": target,
210
- "python_http_client": http_client if python else None,
211
- },
216
+ "generation": generation,
212
217
  "validation": {
213
218
  "full_json_schema": False,
214
219
  "runtime_scope": (
@@ -8,7 +8,11 @@ MANIFEST = ".fastapi-streams.json"
8
8
 
9
9
 
10
10
  def write_files(
11
- output: Path, files: Mapping[str, str], *, check: bool = False
11
+ output: Path,
12
+ files: Mapping[str, str],
13
+ *,
14
+ check: bool = False,
15
+ generation: Mapping[str, str | None] | None = None,
12
16
  ) -> tuple[Path, ...]:
13
17
  """Write deterministic output; only remove stale files owned by the manifest.
14
18
 
@@ -17,7 +21,10 @@ def write_files(
17
21
  expected = dict(files)
18
22
  if MANIFEST in expected:
19
23
  raise ValueError(f"{MANIFEST} is reserved")
20
- expected[MANIFEST] = json.dumps({"files": sorted(files)}, indent=2) + "\n"
24
+ metadata: dict[str, Any] = {"files": sorted(files)}
25
+ if generation is not None:
26
+ metadata["generation"] = dict(generation)
27
+ expected[MANIFEST] = json.dumps(metadata, indent=2, sort_keys=True) + "\n"
21
28
  manifest = _owned_path(output, MANIFEST)
22
29
  previous: set[str] = set()
23
30
  if manifest.exists():
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fastapi-streams"
3
- version = "0.3.1"
3
+ version = "0.4.0"
4
4
  description = "Typed FastAPI streams with generated TypeScript and Python clients."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -133,6 +133,11 @@ def main() -> None:
133
133
  args.http_client if language == "python" else None
134
134
  )
135
135
  assert len(report["generation"]["schema_sha256"]) == 64
136
+ manifest = json.loads(
137
+ (working / output / ".fastapi-streams.json").read_text()
138
+ )
139
+ assert manifest["generation"] == report["generation"]
140
+ assert client_file in manifest["files"]
136
141
  assert report["validation"]["full_json_schema"] is False
137
142
  assert report["validation"]["events"] == {
138
143
  "phase": "iteration",
@@ -11,6 +11,44 @@ from fastapi_streams.codegen.typescript import generate_typescript
11
11
  from fastapi_streams.codegen.writer import MANIFEST, write_files
12
12
 
13
13
 
14
+ @pytest.mark.parametrize(
15
+ "target,backend", [("python", "httpx"), ("python", "httpx2"), ("typescript", None)]
16
+ )
17
+ def test_cli_upgrades_legacy_manifest_without_a_diagnostic_report(
18
+ tmp_path, target, backend
19
+ ):
20
+ schema, output = tmp_path / "schema.json", tmp_path / "sdk"
21
+ document = export_schema(app)
22
+ schema.write_text(json.dumps(document), encoding="utf-8")
23
+ write_files(output, {"stale.txt": "old"})
24
+ handwritten = output / "handwritten.txt"
25
+ handwritten.write_text("keep", encoding="utf-8")
26
+ flags = ["--schema", str(schema), "--language", target, "--output", str(output)]
27
+ if backend:
28
+ flags += ["--python-http-client", backend]
29
+ legacy = (output / MANIFEST).read_bytes()
30
+ assert main(["check", *flags]) == 1
31
+ assert (output / MANIFEST).read_bytes() == legacy
32
+ assert main(["generate", *flags]) == 0
33
+ manifest = json.loads((output / MANIFEST).read_bytes())
34
+ assert manifest["generation"]["target"] == target
35
+ assert manifest["generation"]["python_http_client"] == backend
36
+ assert manifest["files"] == sorted(
37
+ path.relative_to(output).as_posix()
38
+ for path in output.rglob("*")
39
+ if path.is_file() and path.name not in {MANIFEST, "handwritten.txt"}
40
+ )
41
+ assert not (output / "stale.txt").exists()
42
+ assert handwritten.read_text() == "keep"
43
+ assert main(["check", *flags]) == 0
44
+ # A different generator version is drift even if source files are identical.
45
+ manifest["generation"]["generator_version"] = "0.0.1"
46
+ (output / MANIFEST).write_text(json.dumps(manifest), encoding="utf-8")
47
+ stale = (output / MANIFEST).read_bytes()
48
+ assert main(["check", *flags]) == 1
49
+ assert (output / MANIFEST).read_bytes() == stale
50
+
51
+
14
52
  def test_ir_and_types_are_typed_and_streams_only():
15
53
  contract = build_ir(export_schema(app))
16
54
  assert {op.operation_id for op in contract.operations} == {
@@ -5,15 +5,13 @@ import httpx
5
5
  import pytest
6
6
  from pydantic import TypeAdapter, ValidationError
7
7
 
8
+ from fastapi_streams.codegen.cli import main
8
9
  from fastapi_streams.codegen.ir import build_ir
9
10
  from fastapi_streams.codegen.openapi import (
10
11
  ContractError,
11
12
  export_schema,
12
13
  validate_contract,
13
14
  )
14
- from fastapi_streams.codegen.python import generate_python
15
- from fastapi_streams.codegen.typescript import generate_typescript
16
- from fastapi_streams.codegen.writer import write_files
17
15
  from tests.fixtures.generated_python import Client
18
16
  from tests.fixtures.generated_python.models import (
19
17
  FixtureEvent,
@@ -76,13 +74,25 @@ def test_fixture_generation_is_current():
76
74
  document = export_schema(app)
77
75
  snapshot = Path("tests/fixtures/models.openapi.json")
78
76
  assert json.loads(snapshot.read_text()) == document
79
- contract = build_ir(document)
80
- assert not write_files(
81
- Path("tests/fixtures/generated"), generate_typescript(contract), check=True
82
- )
83
- assert not write_files(
84
- Path("tests/fixtures/generated_python"), generate_python(contract), check=True
85
- )
77
+ for language, directory in (
78
+ ("typescript", "generated"),
79
+ ("python", "generated_python"),
80
+ ):
81
+ assert (
82
+ main(
83
+ [
84
+ "check",
85
+ "--schema",
86
+ str(snapshot),
87
+ "--language",
88
+ language,
89
+ "--output",
90
+ f"tests/fixtures/{directory}",
91
+ "--strict",
92
+ ]
93
+ )
94
+ == 0
95
+ )
86
96
 
87
97
 
88
98
  def test_refs_in_properties_named_like_schema_annotations_are_checked():
@@ -2,7 +2,6 @@ import asyncio
2
2
  import importlib.util
3
3
  import sys
4
4
  from enum import StrEnum
5
- from pathlib import Path
6
5
 
7
6
  import httpx
8
7
  import pytest
@@ -22,7 +21,6 @@ from fastapi_streams.codegen.cli import main
22
21
  from fastapi_streams.codegen.ir import build_ir
23
22
  from fastapi_streams.codegen.openapi import export_schema
24
23
  from fastapi_streams.codegen.python import generate_python
25
- from fastapi_streams.codegen.writer import write_files
26
24
 
27
25
 
28
26
  class Chunks(httpx.AsyncByteStream):
@@ -379,8 +377,20 @@ def test_python_codegen_cli_and_snapshot(tmp_path):
379
377
  contract = build_ir(export_schema(app))
380
378
  files = generate_python(contract)
381
379
  assert files == generate_python(contract)
382
- assert not write_files(
383
- Path("examples/agent_runs/generated_python"), files, check=True
380
+ assert (
381
+ main(
382
+ [
383
+ "check",
384
+ "--schema",
385
+ "tests/fixtures/agent_runs.openapi.json",
386
+ "--language",
387
+ "python",
388
+ "--output",
389
+ "examples/agent_runs/generated_python",
390
+ "--strict",
391
+ ]
392
+ )
393
+ == 0
384
394
  )
385
395
  schema = tmp_path / "schema.json"
386
396
  assert (
@@ -237,6 +237,9 @@ def test_report_describes_actual_target_phases_and_stable_schema_content(
237
237
  original = report.read_bytes()
238
238
  result = json.loads(original)
239
239
  generation = result["generation"]
240
+ manifest_path = tmp_path / "sdk" / ".fastapi-streams.json"
241
+ manifest_bytes = manifest_path.read_bytes()
242
+ assert json.loads(manifest_bytes)["generation"] == generation
240
243
  assert generation["generator"] == "fastapi-streams"
241
244
  assert generation["generator_version"] == __version__
242
245
  assert generation["target"] == target
@@ -266,11 +269,18 @@ def test_report_describes_actual_target_phases_and_stable_schema_content(
266
269
  )
267
270
  assert main(["check", *flags]) == 0
268
271
  assert report.read_bytes() == original
272
+ assert manifest_path.read_bytes() == manifest_bytes
269
273
  # The complete document participates, including components not emitted by SDKs.
270
274
  document["info"]["description"] = "Changed contract provenance"
271
275
  schema.write_text(json.dumps(document), encoding="utf-8")
272
- assert main(["check", *flags]) == 0
276
+ assert main(["check", *flags]) == 1
277
+ assert manifest_path.read_bytes() == manifest_bytes
273
278
  assert (
274
279
  json.loads(report.read_bytes())["generation"]["schema_sha256"]
275
280
  != generation["schema_sha256"]
276
281
  )
282
+ assert main(["generate", *flags]) == 0
283
+ assert (
284
+ json.loads(manifest_path.read_bytes())["generation"]
285
+ == json.loads(report.read_bytes())["generation"]
286
+ )
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ "client.ts",
4
+ "fixtures/api.ts",
5
+ "fixtures/index.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": "b12c8dd481b9c592222dc45a26586aecc8899702e42ccc0deed834d91a7cfe41",
17
+ "target": "typescript"
18
+ }
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "files": [
3
+ "__init__.py",
4
+ "client.py",
5
+ "fixtures/__init__.py",
6
+ "fixtures/api.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": "b12c8dd481b9c592222dc45a26586aecc8899702e42ccc0deed834d91a7cfe41",
17
+ "target": "python"
18
+ }
19
+ }
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
3
  import { createClient, HttpError, isHttpError, TransportError } from "../../examples/agent_runs/generated/index.js";
4
- import { consumeWithErrors } from "../../examples/agent_runs/errors.js";
4
+ import { consumeWithErrors, decodeValidationError } from "../../examples/agent_runs/errors.js";
5
5
 
6
6
  type MissingProblem = {
7
7
  code: "missing";
@@ -67,3 +67,18 @@ test("consumer example handles validated HTTP errors and rethrows unexpected bod
67
67
  await assert.rejects(() => consumeWithErrors(makeClient(unexpected), "demo"), HttpError);
68
68
  }
69
69
  });
70
+
71
+ test("consumer decoder retains generic failures and checks the actual status", () => {
72
+ const body = { detail: [{ loc: ["run_id", 0], msg: "Invalid", type: "value_error" }] };
73
+ const response = new Response(null, { status: 422 });
74
+ const expected = new HttpError(422, body, response);
75
+ assert.equal(decodeValidationError(expected), body);
76
+ for (const details of ["Bad gateway", undefined, null, [], { detail: [{}] }]) {
77
+ const error = new HttpError(422, details, response);
78
+ assert.equal(decodeValidationError(error), undefined);
79
+ assert.equal(error.details, details);
80
+ assert.equal(error.response, response);
81
+ }
82
+ assert.equal(decodeValidationError(new HttpError(502, body, response)), undefined);
83
+ assert.equal(decodeValidationError(new TransportError("offline")), undefined);
84
+ });
@@ -1,11 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
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
- }
@@ -1,11 +0,0 @@
1
- {
2
- "files": [
3
- "client.ts",
4
- "fixtures/api.ts",
5
- "fixtures/index.ts",
6
- "index.ts",
7
- "transport/errors.ts",
8
- "transport/sse.ts",
9
- "types.ts"
10
- ]
11
- }
@@ -1,11 +0,0 @@
1
- {
2
- "files": [
3
- "__init__.py",
4
- "client.py",
5
- "fixtures/__init__.py",
6
- "fixtures/api.py",
7
- "models.py",
8
- "py.typed",
9
- "runtime.py"
10
- ]
11
- }
File without changes