fastapi-streams 0.1.0__tar.gz → 0.2.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.1.0 → fastapi_streams-0.2.0}/CHANGELOG.md +48 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/PKG-INFO +6 -5
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/README.md +5 -4
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/README.md +2 -1
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/codegen.md +38 -0
- fastapi_streams-0.2.0/docs/native-transports.md +49 -0
- fastapi_streams-0.2.0/docs/releasing.md +13 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/agent_runs/api.py +7 -3
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/models.py +3 -1
- {fastapi_streams-0.1.0/fastapi_streams/codegen/templates/python → fastapi_streams-0.2.0/examples/agent_runs/generated_python}/runtime.py +27 -0
- fastapi_streams-0.2.0/examples/native_proxy.py +99 -0
- fastapi_streams-0.2.0/examples/rest_codegen.py +76 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/__init__.py +1 -1
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/diagnostics.py +4 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/documentation.py +8 -1
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/ir.py +73 -4
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/python.py +28 -3
- {fastapi_streams-0.1.0/tests/fixtures/generated_python → fastapi_streams-0.2.0/fastapi_streams/codegen/templates/python}/runtime.py +27 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/pyproject.toml +1 -1
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/scripts/smoke_package.py +38 -6
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_documentation.py +3 -1
- fastapi_streams-0.2.0/tests/codegen/test_native_contract.py +56 -0
- fastapi_streams-0.2.0/tests/codegen/test_scalar_constraints.py +65 -0
- fastapi_streams-0.2.0/tests/codegen/test_unknown_events.py +83 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/fixtures/api.py +3 -1
- {fastapi_streams-0.1.0/examples/agent_runs → fastapi_streams-0.2.0/tests/fixtures}/generated_python/runtime.py +27 -0
- fastapi_streams-0.1.0/docs/releasing.md +0 -160
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.gitattributes +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.github/workflows/ci.yml +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.github/workflows/compatibility.yml +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.github/workflows/release.yml +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.gitignore +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/.pre-commit-config.yaml +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/AGENTS.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/LICENSE +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/architecture.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/clients.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/development.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/docs/quickstart.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/consume.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/consume.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/errors.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/errors.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/agent-runs/api.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/agent-runs/index.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/client.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/index.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated/types.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/agent_runs/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/client.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/py.typed +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/cli.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/enums.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/names.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/openapi.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/schemas/LICENSE +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/schemas/README.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/schemas/openapi-3.2.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/errors.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/python/api.py.j2 +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/python/client.py.j2 +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/sse.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/typescript/api.ts.j2 +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templates/typescript/client.ts.j2 +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/templating.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/typescript.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/codegen/writer.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/py.typed +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/fastapi_streams/router.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/package-lock.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/package.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/scripts/check_package.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/scripts/check_release.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/scripts/codegen.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/scripts/test_compat.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/helpers.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_additional_properties.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_codegen.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_contract_boundaries.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_diagnostics.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_discriminators.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_error_contracts.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_field_names.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_http_backends.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_intersections.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_literal_fields.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_nested_models.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_python_client.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_python_typing.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_required_properties.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/codegen/test_schema_matrix.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/contracts/test_openapi.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/README.md +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/agent_runs.openapi.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/compat-client.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/.fastapi-streams.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/client.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/fixtures/api.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/fixtures/index.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/index.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/transport/errors.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/transport/sse.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated/types.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/.fastapi-streams.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/client.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/fixtures/__init__.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/models.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/generated_python/py.typed +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/integration_app.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/models.openapi.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/fixtures/models_app.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/integration/test_end_to_end.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/router/test_router.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/test_release.py +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/errors.test.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/live-client.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/models.test.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/narrowing.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/sse.test.ts +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typescript/tsconfig.json +0 -0
- {fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/tests/typing/client_usage.py +0 -0
|
@@ -3,6 +3,54 @@
|
|
|
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.2.0 - 2026-10-18
|
|
7
|
+
|
|
8
|
+
- Source preparation passed workspace CI, independent wheel matrices and the
|
|
9
|
+
package release dry run. See the [release preparation evidence](../../docs/releases.md#verified-follow-up-preparation-2026-10-18).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Preserve supported string patterns/lengths and numeric bounds/multiples in the
|
|
14
|
+
codegen IR and generated Python Pydantic annotations, including nested and
|
|
15
|
+
referenced scalars. Diagnose unsupported constraints and regex expressions.
|
|
16
|
+
- Add `RequestOptions(unknown_events="ignore")` to generated Python SDKs. It
|
|
17
|
+
skips unknown string tags in a top-level discriminated event union; the default
|
|
18
|
+
remains an error. Malformed known events, missing tags and invalid JSON fail.
|
|
19
|
+
Both HTTPX backends retain their validation and transport behavior.
|
|
20
|
+
- Add tested contract-first native proxy and private OpenAPI 3.1 REST-codegen
|
|
21
|
+
recipes. The authoritative OpenAPI 3.2 artifact remains unchanged.
|
|
22
|
+
|
|
23
|
+
### Breaking generated-code changes
|
|
24
|
+
|
|
25
|
+
- Regenerated Python models enforce scalar rules that 0.1.0 silently dropped.
|
|
26
|
+
Fix invalid payloads before adopting the new SDK. Unsupported constraints emit
|
|
27
|
+
`UnsupportedConstraintWarning`; constraints alongside enum/const/composition
|
|
28
|
+
and unsupported Python regex patterns fail explicitly. Move supported scalar
|
|
29
|
+
constraints onto scalar branches where possible. This is not full JSON Schema
|
|
30
|
+
validation. TypeScript remains static-only.
|
|
31
|
+
- Regenerate committed SDKs and run consumer validation tests. Pass HTTPX2
|
|
32
|
+
generation/check options consistently if used. No wire names change.
|
|
33
|
+
|
|
34
|
+
### Upgrade from 0.1.0
|
|
35
|
+
|
|
36
|
+
1. Update `fastapi-streams[codegen]` to 0.2.0 and regenerate both SDKs from the
|
|
37
|
+
authoritative OpenAPI document. Keep the selected Python HTTP backend on
|
|
38
|
+
generation and check commands.
|
|
39
|
+
2. Fix data violating preserved scalar constraints and review all unsupported-rule
|
|
40
|
+
diagnostics. Re-run consuming client validation and stream tests.
|
|
41
|
+
3. Keep the default unknown-event error policy, or opt in per request only where
|
|
42
|
+
the upstream intentionally emits additional top-level discriminator values.
|
|
43
|
+
|
|
44
|
+
### Release verification
|
|
45
|
+
|
|
46
|
+
- Extend installed-wheel HTTPX/HTTPX2 smoke consumers to verify scalar constraints,
|
|
47
|
+
default unknown-event rejection, opt-in skipping and malformed known events.
|
|
48
|
+
|
|
49
|
+
### Documentation
|
|
50
|
+
|
|
51
|
+
- Fix PyPI installation instructions and supported-scope wording. Replace the
|
|
52
|
+
predecessor release guide with the workspace release process.
|
|
53
|
+
|
|
6
54
|
## 0.1.0 - 2026-10-17
|
|
7
55
|
|
|
8
56
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastapi-streams
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.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
|
|
@@ -45,10 +45,10 @@ A CI check catches drift between the Python app, the contract, and generated cod
|
|
|
45
45
|
|
|
46
46
|
## 1. Decorate your FastAPI endpoint
|
|
47
47
|
|
|
48
|
-
Install from
|
|
48
|
+
Install from PyPI with the optional codegen extra:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
uv add 'fastapi-streams[codegen]
|
|
51
|
+
uv add 'fastapi-streams[codegen]'
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
Save this as `app.py`:
|
|
@@ -236,7 +236,8 @@ and regenerating committed clients.
|
|
|
236
236
|
| [Development](docs/development.md) | Tests, supported scope, and roadmap. |
|
|
237
237
|
| [Releasing](docs/releasing.md) | Tag-based releases and credential-free PyPI publishing. |
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
contract for your existing REST SDK. See the
|
|
239
|
+
The current release supports GET/POST JSON SSE streams. Ordinary REST endpoints
|
|
240
|
+
stay in the contract for your existing REST SDK. See the
|
|
241
|
+
[supported scope](docs/development.md#supported-scope-and-roadmap).
|
|
241
242
|
|
|
242
243
|
Python 3.12+ · FastAPI 0.143.3+ · Pydantic v2 · [MIT](LICENSE)
|
|
@@ -27,10 +27,10 @@ A CI check catches drift between the Python app, the contract, and generated cod
|
|
|
27
27
|
|
|
28
28
|
## 1. Decorate your FastAPI endpoint
|
|
29
29
|
|
|
30
|
-
Install from
|
|
30
|
+
Install from PyPI with the optional codegen extra:
|
|
31
31
|
|
|
32
32
|
```sh
|
|
33
|
-
uv add 'fastapi-streams[codegen]
|
|
33
|
+
uv add 'fastapi-streams[codegen]'
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Save this as `app.py`:
|
|
@@ -218,7 +218,8 @@ and regenerating committed clients.
|
|
|
218
218
|
| [Development](docs/development.md) | Tests, supported scope, and roadmap. |
|
|
219
219
|
| [Releasing](docs/releasing.md) | Tag-based releases and credential-free PyPI publishing. |
|
|
220
220
|
|
|
221
|
-
|
|
222
|
-
contract for your existing REST SDK. See the
|
|
221
|
+
The current release supports GET/POST JSON SSE streams. Ordinary REST endpoints
|
|
222
|
+
stay in the contract for your existing REST SDK. See the
|
|
223
|
+
[supported scope](docs/development.md#supported-scope-and-roadmap).
|
|
223
224
|
|
|
224
225
|
Python 3.12+ · FastAPI 0.143.3+ · Pydantic v2 · [MIT](LICENSE)
|
|
@@ -7,9 +7,10 @@ TypeScript Fetch and Python HTTPX clients from the same definitions.
|
|
|
7
7
|
| --- | --- |
|
|
8
8
|
| [Quickstart](quickstart.md) | Run the complete example and define your first stream. |
|
|
9
9
|
| [Contracts and codegen](codegen.md) | Export OpenAPI, generate both clients, and detect drift in CI. |
|
|
10
|
+
| [Native transports](native-transports.md) | Existing SSE proxies, contract-first generation and private REST codegen views. |
|
|
10
11
|
| [Client usage](clients.md) | Named event values, typed iteration, authentication, cancellation, and errors. |
|
|
11
12
|
| [Development](development.md) | Local checks, test coverage, supported features, and roadmap. |
|
|
12
|
-
| [Releasing](releasing.md) |
|
|
13
|
+
| [Releasing](releasing.md) | Workspace release preparation, PyPI publishing, and recovery. |
|
|
13
14
|
| [Architecture](architecture.md) | Server integration, shared IR, generation, and transport boundaries. |
|
|
14
15
|
|
|
15
16
|
Start with the [quickstart](quickstart.md), then use the
|
|
@@ -116,3 +116,41 @@ Generation is deterministic. A manifest tracks owned files and cleans up stale
|
|
|
116
116
|
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
|
+
|
|
120
|
+
## Runtime validation and unknown variants
|
|
121
|
+
|
|
122
|
+
Python validates generated event models and request bodies with Pydantic. Supported
|
|
123
|
+
scalar constraints are `pattern`, `minLength`, `maxLength`, `minimum`, `maximum`,
|
|
124
|
+
`exclusiveMinimum`, `exclusiveMaximum`, and `multipleOf`. They survive nested
|
|
125
|
+
models, arrays, nullable types and referenced scalar aliases. Patterns use
|
|
126
|
+
Pydantic's Rust regex engine; unsupported expressions fail generation. Numeric
|
|
127
|
+
multiples follow Pydantic's floating-point tolerance. This is not a complete JSON
|
|
128
|
+
Schema validator. Formats, collection/object limits and other unsupported rules
|
|
129
|
+
emit `UnsupportedConstraintWarning`; promote it to an error in CI when required.
|
|
130
|
+
Constraints alongside enum/const or composition fail explicitly; constraints on
|
|
131
|
+
scalar branches are supported. TypeScript provides static shapes and does not
|
|
132
|
+
validate incoming JSON or enforce scalar constraints at runtime.
|
|
133
|
+
|
|
134
|
+
Python event unions are closed by default: unknown variants raise
|
|
135
|
+
`StreamParseError`. For an upstream that emits more variants than your declared
|
|
136
|
+
subset, explicitly choose a per-request policy:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from generated import RequestOptions
|
|
140
|
+
|
|
141
|
+
options = RequestOptions(unknown_events="ignore")
|
|
142
|
+
async for event in client.runs.run_events(run_id="run_abc", options=options):
|
|
143
|
+
...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Ignoring requires a top-level discriminated union with known string tags. Only
|
|
147
|
+
valid JSON objects with an unknown string discriminator are skipped. Invalid
|
|
148
|
+
JSON, missing/non-string discriminators and malformed known variants still fail;
|
|
149
|
+
unknown nested variants inside a known event fail too. The option adds no retry,
|
|
150
|
+
reconnect or replay. TypeScript continues to forward JSON-decoded values without
|
|
151
|
+
runtime union validation. Regenerate existing Python SDKs to obtain the option.
|
|
152
|
+
|
|
153
|
+
## Existing native streams and mixed REST generation
|
|
154
|
+
|
|
155
|
+
See the [native transport and mixed-codegen recipe](native-transports.md) when
|
|
156
|
+
an existing generator, native server or byte-preserving proxy owns the stream.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Native SSE transports and mixed client generation
|
|
2
|
+
|
|
3
|
+
An authoritative OpenAPI 3.2 contract can describe an existing SSE endpoint.
|
|
4
|
+
`fastapi-streams generate --schema` does not require `StreamRouter`, a running
|
|
5
|
+
server or replacement of the application's stream implementation.
|
|
6
|
+
|
|
7
|
+
The executable [native proxy example](../examples/native_proxy.py) declares an
|
|
8
|
+
explicit event union through `itemSchema.properties.data.contentSchema` and
|
|
9
|
+
registers its referenced models. Its `StreamingResponse` forwards upstream raw
|
|
10
|
+
bytes, status and selected response headers. Keepalives and unknown event frames
|
|
11
|
+
are not decoded or reserialized. The response closes the upstream connection;
|
|
12
|
+
the FastAPI lifespan owns the HTTP client. Application authentication, allowed
|
|
13
|
+
headers, run identity, recovery and replay remain application responsibilities.
|
|
14
|
+
The declared 200 contract must match the reviewed upstream; other upstream status
|
|
15
|
+
codes are preserved and generated SDKs apply their usual HTTP error handling.
|
|
16
|
+
|
|
17
|
+
From this package directory:
|
|
18
|
+
|
|
19
|
+
```console
|
|
20
|
+
uv run fastapi-streams schema --app examples.native_proxy:app --output openapi.json
|
|
21
|
+
uv run fastapi-streams generate --schema openapi.json --language python --python-http-client httpx2 --output generated/sse
|
|
22
|
+
uv run fastapi-streams check --schema openapi.json --language python --python-http-client httpx2 --output generated/sse
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Keep `openapi.json` as the single authoritative artifact for REST and SSE. If an
|
|
26
|
+
existing REST generator accepts only OpenAPI 3.1, derive a private codegen view:
|
|
27
|
+
|
|
28
|
+
```console
|
|
29
|
+
uv run python -m examples.rest_codegen openapi.json build/rest.openapi.json
|
|
30
|
+
uv run python -m examples.rest_codegen openapi.json build/rest.openapi.json --check
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The [adaptation example](../examples/rest_codegen.py) deep-copies the contract,
|
|
34
|
+
sets the private view to 3.1 and replaces each inline JSON SSE `itemSchema` with
|
|
35
|
+
a media `schema` referencing decoded event data. This keeps event models reachable
|
|
36
|
+
for REST generators that use reachability or schema-tag filtering. That schema
|
|
37
|
+
is a private model-discovery aid, not the SSE wire body. Filter SSE operations out
|
|
38
|
+
of ordinary HTTP method generation and configure your generator's model/tag
|
|
39
|
+
selection explicitly. All components and REST operations remain available.
|
|
40
|
+
|
|
41
|
+
This recipe handles ordinary REST operations and inline JSON SSE envelopes. It is
|
|
42
|
+
not a general OpenAPI 3.2 downgrade: review additional 3.2 features against the
|
|
43
|
+
chosen REST generator. Unsupported SSE envelope shapes fail explicitly. Never
|
|
44
|
+
publish the private view as the service contract or edit it by hand. Generate it
|
|
45
|
+
from the authoritative artifact before REST generation/drift checks; generate
|
|
46
|
+
and check the SSE SDK directly from the authoritative artifact. Keep HTTP and
|
|
47
|
+
SSE outputs in separate directories. Run both clients' consumer tests.
|
|
48
|
+
|
|
49
|
+
[Back to code generation](codegen.md)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Releasing fastapi-streams
|
|
2
|
+
|
|
3
|
+
Use the [workspace release process](../../../docs/releases.md). Only root
|
|
4
|
+
workflows execute in this repository; nested workflows and the predecessor
|
|
5
|
+
package-local release script are historical configuration.
|
|
6
|
+
|
|
7
|
+
Releases use independent `fastapi-streams-vX.Y.Z` tags and the root lockfile.
|
|
8
|
+
Prepare a new version, update the changelog and generated clients, pass the
|
|
9
|
+
workspace and installed-artifact checks, and wait for CI on the exact commit.
|
|
10
|
+
Run the root release workflow with `dry_run=true` before publication. A real
|
|
11
|
+
publication requires explicit authorization.
|
|
12
|
+
|
|
13
|
+
[Back to documentation](README.md)
|
|
@@ -35,7 +35,9 @@ class AgentRunsApi:
|
|
|
35
35
|
|
|
36
36
|
Use contextlib.aclosing for deterministic cleanup when exiting iteration early. Task cancellation closes the response and propagates normally.
|
|
37
37
|
|
|
38
|
-
Events are validated with Pydantic.
|
|
38
|
+
Events are validated with Pydantic against generated shapes, variants and supported scalar constraints (string pattern/length, numeric bounds/multiples). Unsupported source constraints are diagnosed during generation; this is not full JSON Schema validation.
|
|
39
|
+
|
|
40
|
+
Unknown event variants fail by default. For a top-level discriminated union, RequestOptions(unknown_events='ignore') skips unknown string tags. Malformed known events, missing tags and invalid JSON still fail.
|
|
39
41
|
"""
|
|
40
42
|
query: list[tuple[str, str]] = []
|
|
41
43
|
headers: dict[str, str] = {}
|
|
@@ -55,7 +57,7 @@ class AgentRunsApi:
|
|
|
55
57
|
self,
|
|
56
58
|
*,
|
|
57
59
|
run_id: str,
|
|
58
|
-
delay:
|
|
60
|
+
delay: Models.StreamRunEventsDelay | None = None,
|
|
59
61
|
options: RequestOptions | None = None,
|
|
60
62
|
) -> AsyncGenerator[Models.RunEvent, None]:
|
|
61
63
|
"""
|
|
@@ -73,7 +75,9 @@ class AgentRunsApi:
|
|
|
73
75
|
|
|
74
76
|
Use contextlib.aclosing for deterministic cleanup when exiting iteration early. Task cancellation closes the response and propagates normally.
|
|
75
77
|
|
|
76
|
-
Events are validated with Pydantic.
|
|
78
|
+
Events are validated with Pydantic against generated shapes, variants and supported scalar constraints (string pattern/length, numeric bounds/multiples). Unsupported source constraints are diagnosed during generation; this is not full JSON Schema validation.
|
|
79
|
+
|
|
80
|
+
Unknown event variants fail by default. For a top-level discriminated union, RequestOptions(unknown_events='ignore') skips unknown string tags. Malformed known events, missing tags and invalid JSON still fail.
|
|
77
81
|
"""
|
|
78
82
|
query: list[tuple[str, str]] = []
|
|
79
83
|
headers: dict[str, str] = {}
|
{fastapi_streams-0.1.0 → fastapi_streams-0.2.0}/examples/agent_runs/generated_python/models.py
RENAMED
|
@@ -38,7 +38,7 @@ class RunFailed(BaseModel):
|
|
|
38
38
|
|
|
39
39
|
class RunRequest(BaseModel):
|
|
40
40
|
model_config = ConfigDict(populate_by_name=True)
|
|
41
|
-
prompt: str
|
|
41
|
+
prompt: Annotated[str, Field(max_length=500, min_length=1)]
|
|
42
42
|
|
|
43
43
|
class RunStarted(BaseModel):
|
|
44
44
|
model_config = ConfigDict(populate_by_name=True)
|
|
@@ -49,6 +49,8 @@ type StreamPromptError = HTTPValidationError
|
|
|
49
49
|
|
|
50
50
|
type StreamPromptErrorBody = StreamPromptError
|
|
51
51
|
|
|
52
|
+
type StreamRunEventsDelay = Annotated[float, Field(le=1, ge=0)]
|
|
53
|
+
|
|
52
54
|
type StreamRunEventsError = HTTPValidationError
|
|
53
55
|
|
|
54
56
|
type StreamRunEventsErrorBody = StreamRunEventsError
|
|
@@ -5,6 +5,7 @@ import codecs
|
|
|
5
5
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from typing import Literal
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
10
11
|
from pydantic import JsonValue, TypeAdapter, ValidationError
|
|
@@ -36,6 +37,7 @@ class _Default(Enum):
|
|
|
36
37
|
class RequestOptions:
|
|
37
38
|
headers: Mapping[str, str] | None = None
|
|
38
39
|
timeout: float | None | _Default = _Default.TIMEOUT
|
|
40
|
+
unknown_events: Literal["error", "ignore"] = "error"
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
type ParameterValue = str | int | float | bool | None
|
|
@@ -158,6 +160,20 @@ class Transport:
|
|
|
158
160
|
body: bytes | None = None,
|
|
159
161
|
options: RequestOptions | None = None,
|
|
160
162
|
) -> AsyncGenerator[T, None]:
|
|
163
|
+
discriminator: str | None = None
|
|
164
|
+
known_tags: set[str] = set()
|
|
165
|
+
if options and options.unknown_events not in {"error", "ignore"}:
|
|
166
|
+
raise ValueError("unknown_events must be 'error' or 'ignore'")
|
|
167
|
+
if options and options.unknown_events == "ignore":
|
|
168
|
+
schema = adapter.json_schema()
|
|
169
|
+
policy = schema.get("discriminator", {})
|
|
170
|
+
discriminator = policy.get("propertyName")
|
|
171
|
+
known_tags = set(policy.get("mapping", {}))
|
|
172
|
+
if not isinstance(discriminator, str) or not known_tags:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
"unknown_events='ignore' requires a top-level "
|
|
175
|
+
"discriminated event union"
|
|
176
|
+
)
|
|
161
177
|
request_headers = self.headers.copy()
|
|
162
178
|
request_headers.update(headers)
|
|
163
179
|
if options and options.headers:
|
|
@@ -210,6 +226,17 @@ class Transport:
|
|
|
210
226
|
try:
|
|
211
227
|
yield adapter.validate_json(payload)
|
|
212
228
|
except ValidationError as exc:
|
|
229
|
+
if discriminator is not None:
|
|
230
|
+
try:
|
|
231
|
+
value = TypeAdapter[JsonValue](JsonValue).validate_json(
|
|
232
|
+
payload
|
|
233
|
+
)
|
|
234
|
+
except ValidationError:
|
|
235
|
+
raise StreamParseError(payload) from exc
|
|
236
|
+
if isinstance(value, dict):
|
|
237
|
+
tag = value.get(discriminator)
|
|
238
|
+
if isinstance(tag, str) and tag not in known_tags:
|
|
239
|
+
continue
|
|
213
240
|
raise StreamParseError(payload) from exc
|
|
214
241
|
except httpx.HTTPError as exc:
|
|
215
242
|
raise TransportError("SSE HTTP transport failed") from exc
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from contextlib import asynccontextmanager
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from pydantic import BaseModel, Field, TypeAdapter
|
|
8
|
+
from starlette.background import BackgroundTask
|
|
9
|
+
from starlette.responses import StreamingResponse
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Started(BaseModel):
|
|
13
|
+
type: Literal["started"]
|
|
14
|
+
run_id: str = Field(pattern="^run_[A-Za-z0-9_-]+$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Done(BaseModel):
|
|
18
|
+
type: Literal["done"]
|
|
19
|
+
run_id: str = Field(pattern="^run_[A-Za-z0-9_-]+$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
type Event = Annotated[Started | Done, Field(discriminator="type")]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@asynccontextmanager
|
|
26
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
27
|
+
async with httpx.AsyncClient(
|
|
28
|
+
base_url="https://upstream.example.test", timeout=None
|
|
29
|
+
) as client:
|
|
30
|
+
app.state.upstream = client
|
|
31
|
+
yield
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_app() -> FastAPI:
|
|
35
|
+
app = FastAPI(lifespan=lifespan)
|
|
36
|
+
payload = TypeAdapter(Event).json_schema(
|
|
37
|
+
ref_template="#/components/schemas/{model}"
|
|
38
|
+
)
|
|
39
|
+
definitions = payload.pop("$defs", {})
|
|
40
|
+
|
|
41
|
+
@app.get(
|
|
42
|
+
"/runs/{run_id}/events",
|
|
43
|
+
operation_id="run_events",
|
|
44
|
+
tags=["runs"],
|
|
45
|
+
response_class=StreamingResponse,
|
|
46
|
+
openapi_extra={
|
|
47
|
+
"responses": {
|
|
48
|
+
"200": {
|
|
49
|
+
"content": {
|
|
50
|
+
"text/event-stream": {
|
|
51
|
+
"itemSchema": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"data": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"contentMediaType": "application/json",
|
|
57
|
+
"contentSchema": payload,
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": ["data"],
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
async def events(run_id: str, request: Request) -> StreamingResponse:
|
|
69
|
+
upstream = request.app.state.upstream
|
|
70
|
+
# The application's boundary owns authentication and allowed upstream headers.
|
|
71
|
+
response = await upstream.send(
|
|
72
|
+
upstream.build_request("GET", f"/runs/{run_id}/events"), stream=True
|
|
73
|
+
)
|
|
74
|
+
headers = {
|
|
75
|
+
name: value
|
|
76
|
+
for name, value in response.headers.items()
|
|
77
|
+
if name.lower() in {"content-type", "content-encoding", "cache-control"}
|
|
78
|
+
}
|
|
79
|
+
return StreamingResponse(
|
|
80
|
+
response.aiter_raw(),
|
|
81
|
+
status_code=response.status_code,
|
|
82
|
+
headers=headers,
|
|
83
|
+
background=BackgroundTask(response.aclose),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
original_openapi = app.openapi
|
|
87
|
+
|
|
88
|
+
def openapi() -> dict[str, Any]:
|
|
89
|
+
document = original_openapi()
|
|
90
|
+
document.setdefault("components", {}).setdefault("schemas", {}).update(
|
|
91
|
+
definitions
|
|
92
|
+
)
|
|
93
|
+
return document
|
|
94
|
+
|
|
95
|
+
app.openapi = openapi
|
|
96
|
+
return app
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
app = create_app()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from fastapi_streams.codegen.openapi import ContractError, Document, validate_contract
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def rest_codegen_document(document: Document) -> Document:
|
|
10
|
+
"""Derive a private 3.1 codegen view of ordinary REST plus JSON SSE contracts.
|
|
11
|
+
|
|
12
|
+
This example covers itemSchema/data/contentSchema, not arbitrary OAS 3.2.
|
|
13
|
+
The SSE schema denotes decoded event data only in this private view.
|
|
14
|
+
"""
|
|
15
|
+
validate_contract(document)
|
|
16
|
+
result = deepcopy(document)
|
|
17
|
+
result["openapi"] = "3.1.0"
|
|
18
|
+
result.pop("jsonSchemaDialect", None)
|
|
19
|
+
for path in result.get("paths", {}).values():
|
|
20
|
+
for method, operation in path.items():
|
|
21
|
+
if method not in {
|
|
22
|
+
"get",
|
|
23
|
+
"post",
|
|
24
|
+
"put",
|
|
25
|
+
"patch",
|
|
26
|
+
"delete",
|
|
27
|
+
"head",
|
|
28
|
+
"options",
|
|
29
|
+
"trace",
|
|
30
|
+
}:
|
|
31
|
+
continue
|
|
32
|
+
for response in operation.get("responses", {}).values():
|
|
33
|
+
for media_type, media in response.get("content", {}).items():
|
|
34
|
+
if "itemSchema" not in media:
|
|
35
|
+
continue
|
|
36
|
+
if media_type != "text/event-stream":
|
|
37
|
+
raise ContractError(
|
|
38
|
+
"This recipe only adapts JSON SSE itemSchema"
|
|
39
|
+
)
|
|
40
|
+
envelope = media.pop("itemSchema")
|
|
41
|
+
try:
|
|
42
|
+
data = envelope["properties"]["data"]
|
|
43
|
+
if data["contentMediaType"] != "application/json":
|
|
44
|
+
raise KeyError("contentMediaType")
|
|
45
|
+
media["schema"] = data["contentSchema"]
|
|
46
|
+
except (KeyError, TypeError) as exc:
|
|
47
|
+
raise ContractError(
|
|
48
|
+
"Expected inline JSON SSE envelope"
|
|
49
|
+
) from exc
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> None:
|
|
54
|
+
parser = argparse.ArgumentParser(description="Derive a private REST codegen view")
|
|
55
|
+
parser.add_argument("source", type=Path)
|
|
56
|
+
parser.add_argument("output", type=Path)
|
|
57
|
+
parser.add_argument("--check", action="store_true")
|
|
58
|
+
args = parser.parse_args()
|
|
59
|
+
content = (
|
|
60
|
+
json.dumps(
|
|
61
|
+
rest_codegen_document(json.loads(args.source.read_text("utf-8"))),
|
|
62
|
+
indent=2,
|
|
63
|
+
sort_keys=True,
|
|
64
|
+
)
|
|
65
|
+
+ "\n"
|
|
66
|
+
)
|
|
67
|
+
if args.check:
|
|
68
|
+
if not args.output.is_file() or args.output.read_text("utf-8") != content:
|
|
69
|
+
raise SystemExit("Private REST codegen view is out of date")
|
|
70
|
+
else:
|
|
71
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
args.output.write_text(content, encoding="utf-8", newline="\n")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -15,6 +15,10 @@ class OptionalDiscriminatorWarning(UserWarning):
|
|
|
15
15
|
"""An event union may be missing the field used to select its variant."""
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class UnsupportedConstraintWarning(UserWarning):
|
|
19
|
+
"""A source validation keyword cannot be enforced by generated clients."""
|
|
20
|
+
|
|
21
|
+
|
|
18
22
|
def warn_optional_discriminators(contract: Contract) -> None:
|
|
19
23
|
"""Diagnose event discriminators without changing the declared requiredness."""
|
|
20
24
|
declarations = {item.name: item.type for item in contract.declarations}
|
|
@@ -45,7 +45,14 @@ def operation_doc(
|
|
|
45
45
|
"Use contextlib.aclosing for deterministic cleanup when exiting "
|
|
46
46
|
"iteration early. Task cancellation closes the response and "
|
|
47
47
|
"propagates normally.",
|
|
48
|
-
"Events are validated with Pydantic
|
|
48
|
+
"Events are validated with Pydantic against generated shapes, "
|
|
49
|
+
"variants and supported scalar constraints (string pattern/length, "
|
|
50
|
+
"numeric bounds/multiples). Unsupported source constraints are "
|
|
51
|
+
"diagnosed during generation; this is not full JSON Schema validation.",
|
|
52
|
+
"Unknown event variants fail by default. For a top-level discriminated "
|
|
53
|
+
"union, RequestOptions(unknown_events='ignore') skips unknown string "
|
|
54
|
+
"tags. Malformed known events, missing tags and invalid JSON "
|
|
55
|
+
"still fail.",
|
|
49
56
|
]
|
|
50
57
|
)
|
|
51
58
|
if operation.deprecated:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
+
import warnings
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from typing import Any, Literal, cast
|
|
6
7
|
|
|
@@ -33,6 +34,7 @@ class Scalar:
|
|
|
33
34
|
name: Literal[
|
|
34
35
|
"string", "integer", "number", "boolean", "null", "never", "JsonValue"
|
|
35
36
|
]
|
|
37
|
+
constraints: tuple[tuple[str, str | int | float], ...] = ()
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
@dataclass(frozen=True)
|
|
@@ -402,6 +404,56 @@ class _Types:
|
|
|
402
404
|
self.names[name] = ref
|
|
403
405
|
self.pending[name] = target
|
|
404
406
|
return Reference(name)
|
|
407
|
+
unsupported = sorted(
|
|
408
|
+
set(node)
|
|
409
|
+
& {
|
|
410
|
+
"format",
|
|
411
|
+
"minItems",
|
|
412
|
+
"maxItems",
|
|
413
|
+
"uniqueItems",
|
|
414
|
+
"contains",
|
|
415
|
+
"minContains",
|
|
416
|
+
"maxContains",
|
|
417
|
+
"minProperties",
|
|
418
|
+
"maxProperties",
|
|
419
|
+
"patternProperties",
|
|
420
|
+
"propertyNames",
|
|
421
|
+
"dependentRequired",
|
|
422
|
+
"dependentSchemas",
|
|
423
|
+
"unevaluatedProperties",
|
|
424
|
+
"unevaluatedItems",
|
|
425
|
+
"not",
|
|
426
|
+
"if",
|
|
427
|
+
"then",
|
|
428
|
+
"else",
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
if unsupported:
|
|
432
|
+
from fastapi_streams.codegen.diagnostics import UnsupportedConstraintWarning
|
|
433
|
+
|
|
434
|
+
warnings.warn(
|
|
435
|
+
f"Schema {node.get('title', '<inline>')!r}: constraints "
|
|
436
|
+
f"{', '.join(unsupported)} are not enforced by generated clients",
|
|
437
|
+
UnsupportedConstraintWarning,
|
|
438
|
+
stacklevel=2,
|
|
439
|
+
)
|
|
440
|
+
constraints = {
|
|
441
|
+
"minLength": "min_length",
|
|
442
|
+
"maxLength": "max_length",
|
|
443
|
+
"pattern": "pattern",
|
|
444
|
+
"minimum": "ge",
|
|
445
|
+
"maximum": "le",
|
|
446
|
+
"exclusiveMinimum": "gt",
|
|
447
|
+
"exclusiveMaximum": "lt",
|
|
448
|
+
"multipleOf": "multiple_of",
|
|
449
|
+
}
|
|
450
|
+
if set(node) & constraints.keys() and any(
|
|
451
|
+
key in node for key in ("const", "enum", "oneOf", "anyOf", "allOf")
|
|
452
|
+
):
|
|
453
|
+
raise ContractError(
|
|
454
|
+
"Scalar constraints alongside enum/const or composition are not "
|
|
455
|
+
"supported; put constraints on scalar branches instead"
|
|
456
|
+
)
|
|
405
457
|
if "const" in node:
|
|
406
458
|
return self.literal(node["const"])
|
|
407
459
|
if "enum" in node:
|
|
@@ -436,10 +488,27 @@ class _Types:
|
|
|
436
488
|
self.parse({**node, "type": part}) for part in cast(list[str], kind)
|
|
437
489
|
)
|
|
438
490
|
)
|
|
439
|
-
if kind in ("string", "boolean", "null"):
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
491
|
+
if kind in ("string", "boolean", "null", "number", "integer"):
|
|
492
|
+
applicable: set[str] = (
|
|
493
|
+
{"minLength", "maxLength", "pattern"}
|
|
494
|
+
if kind == "string"
|
|
495
|
+
else {
|
|
496
|
+
"minimum",
|
|
497
|
+
"maximum",
|
|
498
|
+
"exclusiveMinimum",
|
|
499
|
+
"exclusiveMaximum",
|
|
500
|
+
"multipleOf",
|
|
501
|
+
}
|
|
502
|
+
if kind in ("number", "integer")
|
|
503
|
+
else set()
|
|
504
|
+
)
|
|
505
|
+
return Scalar(
|
|
506
|
+
kind,
|
|
507
|
+
tuple(
|
|
508
|
+
(constraints[key], node[key])
|
|
509
|
+
for key in sorted(applicable & node.keys())
|
|
510
|
+
),
|
|
511
|
+
)
|
|
443
512
|
if kind == "array":
|
|
444
513
|
if "prefixItems" in node:
|
|
445
514
|
raise ContractError("Tuple schemas (prefixItems) are not supported")
|