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.
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/CHANGELOG.md +67 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/PKG-INFO +1 -1
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/clients.md +68 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/codegen.md +49 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/development.md +29 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/errors.py +3 -3
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/errors.ts +9 -2
- fastapi_streams-0.4.0/examples/agent_runs/generated/.fastapi-streams.json +19 -0
- fastapi_streams-0.4.0/examples/agent_runs/generated_python/.fastapi-streams.json +19 -0
- {fastapi_streams-0.3.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.4.0/examples/agent_runs/generated_python}/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/__init__.py +1 -1
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/cli.py +71 -7
- {fastapi_streams-0.3.0/examples/agent_runs/generated_python → fastapi_streams-0.4.0/fastapi_streams/codegen/templates/python}/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/writer.py +9 -2
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/pyproject.toml +1 -1
- fastapi_streams-0.4.0/scripts/benchmark_runtime.mjs +93 -0
- fastapi_streams-0.4.0/scripts/benchmark_runtime.py +293 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/smoke_package.py +23 -7
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_codegen.py +38 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_nested_models.py +20 -10
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_python_client.py +14 -4
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_structured_diagnostics.py +77 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_unknown_events.py +50 -0
- fastapi_streams-0.4.0/tests/fixtures/generated/.fastapi-streams.json +19 -0
- fastapi_streams-0.4.0/tests/fixtures/generated_python/.fastapi-streams.json +19 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/runtime.py +29 -31
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/errors.test.ts +16 -1
- fastapi_streams-0.3.0/examples/agent_runs/generated/.fastapi-streams.json +0 -11
- fastapi_streams-0.3.0/examples/agent_runs/generated_python/.fastapi-streams.json +0 -11
- fastapi_streams-0.3.0/tests/fixtures/generated/.fastapi-streams.json +0 -11
- fastapi_streams-0.3.0/tests/fixtures/generated_python/.fastapi-streams.json +0 -11
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.gitattributes +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/ci.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/compatibility.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.github/workflows/release.yml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.gitignore +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/.pre-commit-config.yaml +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/AGENTS.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/LICENSE +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/architecture.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/native-transports.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/quickstart.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/docs/releasing.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/consume.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/consume.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated/types.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/agent_runs/api.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/client.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/models.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/agent_runs/generated_python/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/native_proxy.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/examples/rest_codegen.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/diagnostics.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/documentation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/enums.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/ir.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/names.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/openapi.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/python.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/LICENSE +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/api.py.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/templating.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/codegen/typescript.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/fastapi_streams/router.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/package-lock.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/package.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/check_package.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/check_release.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/codegen.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/scripts/test_compat.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/helpers.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_additional_properties.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_contract_boundaries.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_diagnostics.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_discriminators.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_documentation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_error_contracts.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_field_names.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_http_backends.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_intersections.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_literal_fields.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_native_contract.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_python_typing.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_request_validation.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_required_properties.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_scalar_constraints.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_schema_matrix.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/codegen/test_stream_validation_details.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/contracts/test_openapi.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/README.md +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.openapi.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/agent_runs.typescript.diagnostics.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/compat-client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/api.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/fixtures/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/index.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated/types.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/client.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/fixtures/api.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/models.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/generated_python/py.typed +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/integration_app.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/models.openapi.json +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/fixtures/models_app.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/integration/test_end_to_end.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/router/test_router.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/test_release.py +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/live-client.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/models.test.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/narrowing.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/sse.test.ts +0 -0
- {fastapi_streams-0.3.0 → fastapi_streams-0.4.0}/tests/typescript/tsconfig.json +0 -0
- {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
|
+
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 =
|
|
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
|
-
|
|
19
|
-
|
|
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:
|
|
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
|
-
|
|
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,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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
],
|