fastapi-streams 0.0.1__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.0.1/.gitattributes +1 -0
- fastapi_streams-0.0.1/.github/workflows/ci.yml +50 -0
- fastapi_streams-0.0.1/.github/workflows/compatibility.yml +47 -0
- fastapi_streams-0.0.1/.gitignore +14 -0
- fastapi_streams-0.0.1/.pre-commit-config.yaml +40 -0
- fastapi_streams-0.0.1/AGENTS.md +11 -0
- fastapi_streams-0.0.1/LICENSE +21 -0
- fastapi_streams-0.0.1/PKG-INFO +238 -0
- fastapi_streams-0.0.1/README.md +220 -0
- fastapi_streams-0.0.1/docs/README.md +27 -0
- fastapi_streams-0.0.1/docs/architecture.md +89 -0
- fastapi_streams-0.0.1/docs/clients.md +129 -0
- fastapi_streams-0.0.1/docs/codegen.md +80 -0
- fastapi_streams-0.0.1/docs/development.md +112 -0
- fastapi_streams-0.0.1/docs/quickstart.md +94 -0
- fastapi_streams-0.0.1/examples/__init__.py +0 -0
- fastapi_streams-0.0.1/examples/agent_runs/__init__.py +83 -0
- fastapi_streams-0.0.1/examples/agent_runs/consume.py +26 -0
- fastapi_streams-0.0.1/examples/agent_runs/consume.ts +27 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/.fastapi-streams.json +11 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/agent-runs/api.ts +49 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/agent-runs/index.ts +2 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/client.ts +11 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/index.ts +8 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/transport/errors.ts +26 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/transport/sse.ts +264 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated/types.ts +43 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/.fastapi-streams.json +11 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/__init__.py +7 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/agent_runs/__init__.py +2 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/agent_runs/api.py +54 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/client.py +36 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/models.py +86 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/py.typed +0 -0
- fastapi_streams-0.0.1/examples/agent_runs/generated_python/runtime.py +217 -0
- fastapi_streams-0.0.1/fastapi_streams/__init__.py +4 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/__init__.py +0 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/cli.py +120 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/enums.py +172 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/ir.py +524 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/names.py +88 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/openapi.py +154 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/python.py +473 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/schemas/LICENSE +201 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/schemas/README.md +8 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/schemas/openapi-3.2.json +1729 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/errors.ts +26 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/python/api.py.j2 +52 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/python/client.py.j2 +40 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/python/runtime.py +217 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/sse.ts +264 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/typescript/api.ts.j2 +52 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templates/typescript/client.ts.j2 +15 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/templating.py +19 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/typescript.py +220 -0
- fastapi_streams-0.0.1/fastapi_streams/codegen/writer.py +64 -0
- fastapi_streams-0.0.1/fastapi_streams/py.typed +0 -0
- fastapi_streams-0.0.1/fastapi_streams/router.py +133 -0
- fastapi_streams-0.0.1/package-lock.json +45 -0
- fastapi_streams-0.0.1/package.json +14 -0
- fastapi_streams-0.0.1/pyproject.toml +86 -0
- fastapi_streams-0.0.1/scripts/check_package.py +63 -0
- fastapi_streams-0.0.1/scripts/codegen.py +47 -0
- fastapi_streams-0.0.1/scripts/smoke_package.py +171 -0
- fastapi_streams-0.0.1/scripts/test_compat.py +67 -0
- fastapi_streams-0.0.1/tests/__init__.py +0 -0
- fastapi_streams-0.0.1/tests/codegen/helpers.py +72 -0
- fastapi_streams-0.0.1/tests/codegen/test_additional_properties.py +88 -0
- fastapi_streams-0.0.1/tests/codegen/test_codegen.py +128 -0
- fastapi_streams-0.0.1/tests/codegen/test_contract_boundaries.py +200 -0
- fastapi_streams-0.0.1/tests/codegen/test_discriminators.py +42 -0
- fastapi_streams-0.0.1/tests/codegen/test_field_names.py +40 -0
- fastapi_streams-0.0.1/tests/codegen/test_http_backends.py +242 -0
- fastapi_streams-0.0.1/tests/codegen/test_intersections.py +73 -0
- fastapi_streams-0.0.1/tests/codegen/test_nested_models.py +117 -0
- fastapi_streams-0.0.1/tests/codegen/test_python_client.py +408 -0
- fastapi_streams-0.0.1/tests/codegen/test_python_typing.py +49 -0
- fastapi_streams-0.0.1/tests/codegen/test_required_properties.py +46 -0
- fastapi_streams-0.0.1/tests/codegen/test_schema_matrix.py +362 -0
- fastapi_streams-0.0.1/tests/contracts/test_openapi.py +48 -0
- fastapi_streams-0.0.1/tests/fixtures/README.md +16 -0
- fastapi_streams-0.0.1/tests/fixtures/__init__.py +0 -0
- fastapi_streams-0.0.1/tests/fixtures/agent_runs.openapi.json +366 -0
- fastapi_streams-0.0.1/tests/fixtures/compat-client.ts +65 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/.fastapi-streams.json +11 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/client.ts +11 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/fixtures/api.ts +31 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/fixtures/index.ts +2 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/index.ts +8 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/transport/errors.ts +26 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/transport/sse.ts +264 -0
- fastapi_streams-0.0.1/tests/fixtures/generated/types.ts +45 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/.fastapi-streams.json +11 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/__init__.py +8 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/client.py +36 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/fixtures/__init__.py +2 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/fixtures/api.py +35 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/models.py +97 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/py.typed +0 -0
- fastapi_streams-0.0.1/tests/fixtures/generated_python/runtime.py +217 -0
- fastapi_streams-0.0.1/tests/fixtures/integration_app.py +77 -0
- fastapi_streams-0.0.1/tests/fixtures/models.openapi.json +275 -0
- fastapi_streams-0.0.1/tests/fixtures/models_app.py +53 -0
- fastapi_streams-0.0.1/tests/integration/test_end_to_end.py +295 -0
- fastapi_streams-0.0.1/tests/router/test_router.py +250 -0
- fastapi_streams-0.0.1/tests/typescript/live-client.ts +26 -0
- fastapi_streams-0.0.1/tests/typescript/models.test.ts +36 -0
- fastapi_streams-0.0.1/tests/typescript/narrowing.ts +31 -0
- fastapi_streams-0.0.1/tests/typescript/sse.test.ts +233 -0
- fastapi_streams-0.0.1/tests/typescript/tsconfig.json +16 -0
- fastapi_streams-0.0.1/tests/typing/client_usage.py +27 -0
- fastapi_streams-0.0.1/uv.lock +1218 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
verify:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
timeout-minutes: 15
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
python: ["3.12", "3.13", "3.14"]
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python }}
|
|
23
|
+
- uses: actions/setup-node@v4
|
|
24
|
+
with:
|
|
25
|
+
node-version: "24"
|
|
26
|
+
cache: npm
|
|
27
|
+
- uses: astral-sh/setup-uv@v6
|
|
28
|
+
with:
|
|
29
|
+
version: "0.9.2"
|
|
30
|
+
enable-cache: true
|
|
31
|
+
- run: uv sync --frozen --all-extras
|
|
32
|
+
- run: npm ci --ignore-scripts
|
|
33
|
+
- run: uv run ruff check .
|
|
34
|
+
- run: uv run ruff format --check .
|
|
35
|
+
- run: uv run pyright
|
|
36
|
+
- run: uv run pylint fastapi_streams
|
|
37
|
+
- run: uv run pre-commit run --all-files
|
|
38
|
+
- run: npm run typecheck
|
|
39
|
+
- run: npm run test:coverage
|
|
40
|
+
- run: uv run pytest -q --cov --cov-report=term-missing --cov-report=xml:coverage-python.xml
|
|
41
|
+
- uses: actions/upload-artifact@v4
|
|
42
|
+
if: always()
|
|
43
|
+
with:
|
|
44
|
+
name: coverage-python-${{ matrix.python }}
|
|
45
|
+
path: |
|
|
46
|
+
coverage-python.xml
|
|
47
|
+
coverage-typescript.lcov
|
|
48
|
+
- run: uv run python scripts/codegen.py --check
|
|
49
|
+
- run: uv build
|
|
50
|
+
- run: uv run python scripts/check_package.py
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Dependency compatibility
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: '17 7 * * 1'
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
compatibility:
|
|
15
|
+
name: ${{ matrix.os }} / Python ${{ matrix.python }} / ${{ matrix.dependencies }}
|
|
16
|
+
runs-on: ${{ matrix.os }}
|
|
17
|
+
timeout-minutes: 15
|
|
18
|
+
strategy:
|
|
19
|
+
fail-fast: false
|
|
20
|
+
matrix:
|
|
21
|
+
os: [ubuntu-latest, windows-latest]
|
|
22
|
+
python: ['3.12', '3.13', '3.14']
|
|
23
|
+
dependencies: [lowest, latest]
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python }}
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: '24'
|
|
32
|
+
cache: npm
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
version: '0.9.2'
|
|
36
|
+
enable-cache: true
|
|
37
|
+
- run: npm ci --ignore-scripts
|
|
38
|
+
- name: Resolve dependencies and run native FastAPI and client integration tests
|
|
39
|
+
run: python scripts/test_compat.py --python ${{ matrix.python }} --dependencies ${{ matrix.dependencies }}
|
|
40
|
+
- uses: actions/upload-artifact@v4
|
|
41
|
+
if: always()
|
|
42
|
+
with:
|
|
43
|
+
name: compatibility-${{ matrix.os }}-${{ matrix.python }}-${{ matrix.dependencies }}
|
|
44
|
+
include-hidden-files: true
|
|
45
|
+
path: |
|
|
46
|
+
.pytest_cache/dependencies-*.txt
|
|
47
|
+
.pytest_cache/results-*.xml
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
minimum_pre_commit_version: '4.0.0'
|
|
2
|
+
repos:
|
|
3
|
+
- repo: local
|
|
4
|
+
hooks:
|
|
5
|
+
- id: ruff-check
|
|
6
|
+
name: Ruff lint
|
|
7
|
+
entry: uv run --frozen ruff check
|
|
8
|
+
language: system
|
|
9
|
+
types: [python]
|
|
10
|
+
exclude: ^(examples/agent_runs/generated_python|tests/fixtures/generated_python)/
|
|
11
|
+
- id: ruff-format
|
|
12
|
+
name: Ruff format
|
|
13
|
+
entry: uv run --frozen ruff format --check
|
|
14
|
+
language: system
|
|
15
|
+
types: [python]
|
|
16
|
+
exclude: ^(examples/agent_runs/generated_python|tests/fixtures/generated_python)/
|
|
17
|
+
- id: pyright
|
|
18
|
+
name: Pyright
|
|
19
|
+
entry: uv run --frozen pyright
|
|
20
|
+
language: system
|
|
21
|
+
pass_filenames: false
|
|
22
|
+
types: [python]
|
|
23
|
+
- id: pylint
|
|
24
|
+
name: Pylint
|
|
25
|
+
entry: uv run --frozen pylint fastapi_streams
|
|
26
|
+
language: system
|
|
27
|
+
pass_filenames: false
|
|
28
|
+
files: ^fastapi_streams/.*\.py$
|
|
29
|
+
- id: typescript
|
|
30
|
+
name: TypeScript
|
|
31
|
+
entry: npm run typecheck
|
|
32
|
+
language: system
|
|
33
|
+
pass_filenames: false
|
|
34
|
+
files: \.(ts|json)$
|
|
35
|
+
- id: codegen
|
|
36
|
+
name: Generated contract and client drift
|
|
37
|
+
entry: uv run --frozen python scripts/codegen.py --check
|
|
38
|
+
language: system
|
|
39
|
+
pass_filenames: false
|
|
40
|
+
files: ^(fastapi_streams/|examples/|tests/fixtures/|scripts/codegen\.py|pyproject\.toml|uv\.lock)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Repository guidance
|
|
2
|
+
|
|
3
|
+
## Python module layout
|
|
4
|
+
|
|
5
|
+
In new or edited Python source modules, put the public API (public classes and
|
|
6
|
+
functions) first, immediately after imports and module constants. Define private
|
|
7
|
+
implementation helpers below the public API. Keep the executable `__main__`
|
|
8
|
+
entrypoint at the end of the module.
|
|
9
|
+
|
|
10
|
+
Generated client files are owned by the generators; update their templates rather
|
|
11
|
+
than editing generated output by hand.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mathis Arends
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastapi-streams
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Typed FastAPI streams with generated TypeScript and Python clients.
|
|
5
|
+
Project-URL: Documentation, https://github.com/mathisarends/fastapi-streams/tree/main/docs
|
|
6
|
+
Project-URL: Source, https://github.com/mathisarends/fastapi-streams
|
|
7
|
+
Project-URL: Issues, https://github.com/mathisarends/fastapi-streams/issues
|
|
8
|
+
Author: Mathis Arends
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: fastapi<1.0,>=0.143.3
|
|
13
|
+
Requires-Dist: pydantic<3,>=2.11.2
|
|
14
|
+
Provides-Extra: codegen
|
|
15
|
+
Requires-Dist: jinja2<4,>=3.1; extra == 'codegen'
|
|
16
|
+
Requires-Dist: jsonschema<5,>=4.23; extra == 'codegen'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# fastapi-streams
|
|
20
|
+
|
|
21
|
+
[](https://github.com/mathisarends/fastapi-streams/actions/workflows/ci.yml)
|
|
22
|
+
[](pyproject.toml)
|
|
23
|
+
[](LICENSE)
|
|
24
|
+
|
|
25
|
+
**Decorate a FastAPI endpoint. Get typed streaming clients in TypeScript and Python.**
|
|
26
|
+
|
|
27
|
+
Define your events with Pydantic and add `@router.stream()`. `fastapi-streams`
|
|
28
|
+
turns that definition into an OpenAPI 3.2 stream contract and generated SDKs with
|
|
29
|
+
named event values, typed async iteration, and shared SSE transport code.
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
FastAPI decorator → OpenAPI 3.2 → shared HTTP IR → TypeScript + Python clients
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Why this exists
|
|
36
|
+
|
|
37
|
+
An endpoint labeled `text/event-stream` does not give consumers a typed SDK.
|
|
38
|
+
They still need to know which JSON events arrive, how to parse a fragmented
|
|
39
|
+
stream, and how to stop it cleanly.
|
|
40
|
+
|
|
41
|
+
OpenAPI 3.2 can describe each SSE event with `itemSchema` and its JSON payload
|
|
42
|
+
with `contentSchema`. `fastapi-streams` connects that contract to both clients,
|
|
43
|
+
so event models, endpoint parameters, and client types come from one definition.
|
|
44
|
+
A CI check catches drift between the Python app, the contract, and generated code.
|
|
45
|
+
|
|
46
|
+
## 1. Decorate your FastAPI endpoint
|
|
47
|
+
|
|
48
|
+
Install from GitHub with the optional codegen extra:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
uv add 'fastapi-streams[codegen] @ git+https://github.com/mathisarends/fastapi-streams.git'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Save this as `app.py`:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from collections.abc import AsyncIterator
|
|
58
|
+
from typing import Annotated, Literal
|
|
59
|
+
|
|
60
|
+
from fastapi import FastAPI
|
|
61
|
+
from pydantic import BaseModel, Field
|
|
62
|
+
|
|
63
|
+
from fastapi_streams import StreamRouter
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TokenDelta(BaseModel):
|
|
67
|
+
type: Literal["token.delta"]
|
|
68
|
+
text: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class RunCompleted(BaseModel):
|
|
72
|
+
type: Literal["run.completed"]
|
|
73
|
+
run_id: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
type RunEvent = Annotated[TokenDelta | RunCompleted, Field(discriminator="type")]
|
|
77
|
+
|
|
78
|
+
router = StreamRouter(prefix="/runs", tags=["agentRuns"])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.stream("/{run_id}/events", operation_id="stream_run_events")
|
|
82
|
+
async def stream_run_events(run_id: str) -> AsyncIterator[RunEvent]:
|
|
83
|
+
yield TokenDelta(type="token.delta", text="Hello")
|
|
84
|
+
yield RunCompleted(type="run.completed", run_id=run_id)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
app = FastAPI()
|
|
88
|
+
app.include_router(router, prefix="/v1")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The result is `GET /v1/runs/{run_id}/events`. `StreamRouter` extends `APIRouter`:
|
|
92
|
+
normal `Depends`, security dependencies, validation, middleware, and nested
|
|
93
|
+
routers keep working. FastAPI handles SSE framing, serialization, and heartbeats.
|
|
94
|
+
Use `methods=["POST"]` for request-body-driven streams.
|
|
95
|
+
|
|
96
|
+
## 2. Export once, generate both clients
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
uv run fastapi-streams schema \
|
|
100
|
+
--app app:app --output openapi.json
|
|
101
|
+
|
|
102
|
+
uv run fastapi-streams generate \
|
|
103
|
+
--schema openapi.json --language typescript --output generated/typescript
|
|
104
|
+
|
|
105
|
+
uv run fastapi-streams generate \
|
|
106
|
+
--schema openapi.json --language python --output generated/python
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
These commands import the app and read its contract; no running server is needed.
|
|
110
|
+
The exporter validates OpenAPI 3.2 and the event schemas. Both generators consume
|
|
111
|
+
one HTTP intermediate representation (IR), carrying operations, parameters, and
|
|
112
|
+
event types into the generated SDKs.
|
|
113
|
+
|
|
114
|
+
## 3. Consume typed events
|
|
115
|
+
|
|
116
|
+
The `RunEvent` union produces `RunEventType`, with the same named values in both
|
|
117
|
+
languages. The discriminator narrows each event to its matching model.
|
|
118
|
+
|
|
119
|
+
**TypeScript — Fetch, `AsyncIterable<RunEvent>`, no runtime dependencies:**
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { createClient, RunEventType } from "./generated/typescript/index.js";
|
|
123
|
+
|
|
124
|
+
const client = createClient({ baseUrl: "http://localhost:8000" });
|
|
125
|
+
|
|
126
|
+
for await (const event of client.agentRuns.streamRunEvents({ runId: "demo" })) {
|
|
127
|
+
switch (event.type) {
|
|
128
|
+
case RunEventType.TOKEN_DELTA:
|
|
129
|
+
console.log(event.text); // TokenDelta
|
|
130
|
+
break;
|
|
131
|
+
case RunEventType.RUN_COMPLETED:
|
|
132
|
+
console.log(event.run_id); // RunCompleted
|
|
133
|
+
break;
|
|
134
|
+
default: {
|
|
135
|
+
const exhaustive: never = event;
|
|
136
|
+
throw new Error(`Unexpected event: ${exhaustive}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Python — HTTPX, Pydantic models, and `StrEnum` values:**
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import asyncio
|
|
146
|
+
from typing import assert_never
|
|
147
|
+
|
|
148
|
+
from generated.python import Client, RunEventType
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
async def main() -> None:
|
|
152
|
+
async with Client("http://localhost:8000") as client:
|
|
153
|
+
async for event in client.agent_runs.stream_run_events(run_id="demo"):
|
|
154
|
+
match event.type:
|
|
155
|
+
case RunEventType.TOKEN_DELTA:
|
|
156
|
+
print(event.text)
|
|
157
|
+
case RunEventType.RUN_COMPLETED:
|
|
158
|
+
print(event.run_id)
|
|
159
|
+
case _:
|
|
160
|
+
assert_never(event)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
asyncio.run(main())
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Install `httpx` and `pydantic` in a Python consumer (`uv add httpx pydantic`).
|
|
167
|
+
HTTPX remains the default; opt in to HTTPX2 at generation time with
|
|
168
|
+
`--python-http-client httpx2` and install `httpx2` instead. See
|
|
169
|
+
[Python HTTP library selection](docs/clients.md#python-http-library).
|
|
170
|
+
Both SDKs handle fragmented SSE frames, UTF-8 decoding, HTTP errors, and resource
|
|
171
|
+
cleanup. TypeScript supports `AbortSignal`; Python supports task cancellation and
|
|
172
|
+
explicit async-generator closure. See [client usage](docs/clients.md).
|
|
173
|
+
|
|
174
|
+
## 4. Catch contract drift in CI
|
|
175
|
+
|
|
176
|
+
Commit the exported schema and generated clients. Then check the complete chain:
|
|
177
|
+
|
|
178
|
+
```sh
|
|
179
|
+
uv run fastapi-streams check \
|
|
180
|
+
--app app:app --schema openapi.json \
|
|
181
|
+
--language typescript --output generated/typescript
|
|
182
|
+
|
|
183
|
+
uv run fastapi-streams check \
|
|
184
|
+
--app app:app --schema openapi.json \
|
|
185
|
+
--language python --output generated/python
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`check` fails if the app's contract changed or generated files are stale. It
|
|
189
|
+
writes nothing and needs no running server. Pair it with TypeScript's compiler
|
|
190
|
+
and Pyright to catch invalid parameters and event access in consumer code.
|
|
191
|
+
|
|
192
|
+
| Check | What it verifies |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| Contract validation | OpenAPI structure, JSON Schemas, and local references. |
|
|
195
|
+
| `fastapi-streams check` | App → committed contract → generated client consistency. |
|
|
196
|
+
| TypeScript / Pyright | Parameter types, event fields, and exhaustive narrowing. |
|
|
197
|
+
|
|
198
|
+
[Codegen and CI reference →](docs/codegen.md)
|
|
199
|
+
|
|
200
|
+
## Run the complete example
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
git clone https://github.com/mathisarends/fastapi-streams.git
|
|
204
|
+
cd fastapi-streams
|
|
205
|
+
uv sync --all-extras
|
|
206
|
+
npm ci
|
|
207
|
+
uv run uvicorn examples.agent_runs:app --reload
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
In another terminal:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
# TypeScript
|
|
214
|
+
npx tsc -p tests/typescript/tsconfig.json
|
|
215
|
+
node tests/typescript/dist/examples/agent_runs/consume.js
|
|
216
|
+
|
|
217
|
+
# Python
|
|
218
|
+
uv run python -m examples.agent_runs.consume
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The example includes GET and authenticated POST streams, both generated SDKs,
|
|
222
|
+
and committed OpenAPI regression fixtures in `tests/fixtures/`. Verify all generated artifacts with
|
|
223
|
+
`uv run python scripts/codegen.py --check`.
|
|
224
|
+
|
|
225
|
+
## Documentation
|
|
226
|
+
|
|
227
|
+
| Guide | Topics |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| [Quickstart](docs/quickstart.md) | Setup, event models, and FastAPI integration. |
|
|
230
|
+
| [Contracts and codegen](docs/codegen.md) | OpenAPI 3.2, both generators, and drift checks. |
|
|
231
|
+
| [Client usage](docs/clients.md) | Enums, authentication, cancellation, and errors. |
|
|
232
|
+
| [Architecture](docs/architecture.md) | Native SSE, shared IR, templates, and transports. |
|
|
233
|
+
| [Development](docs/development.md) | Tests, supported scope, and roadmap. |
|
|
234
|
+
|
|
235
|
+
v0.0.1 supports GET/POST JSON SSE streams. Ordinary REST endpoints stay in the
|
|
236
|
+
contract for your existing REST SDK. See the [supported scope](docs/development.md#supported-scope-and-roadmap).
|
|
237
|
+
|
|
238
|
+
Python 3.12+ · FastAPI 0.143.3+ · Pydantic v2 · [MIT](LICENSE)
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# fastapi-streams
|
|
2
|
+
|
|
3
|
+
[](https://github.com/mathisarends/fastapi-streams/actions/workflows/ci.yml)
|
|
4
|
+
[](pyproject.toml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**Decorate a FastAPI endpoint. Get typed streaming clients in TypeScript and Python.**
|
|
8
|
+
|
|
9
|
+
Define your events with Pydantic and add `@router.stream()`. `fastapi-streams`
|
|
10
|
+
turns that definition into an OpenAPI 3.2 stream contract and generated SDKs with
|
|
11
|
+
named event values, typed async iteration, and shared SSE transport code.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
FastAPI decorator → OpenAPI 3.2 → shared HTTP IR → TypeScript + Python clients
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Why this exists
|
|
18
|
+
|
|
19
|
+
An endpoint labeled `text/event-stream` does not give consumers a typed SDK.
|
|
20
|
+
They still need to know which JSON events arrive, how to parse a fragmented
|
|
21
|
+
stream, and how to stop it cleanly.
|
|
22
|
+
|
|
23
|
+
OpenAPI 3.2 can describe each SSE event with `itemSchema` and its JSON payload
|
|
24
|
+
with `contentSchema`. `fastapi-streams` connects that contract to both clients,
|
|
25
|
+
so event models, endpoint parameters, and client types come from one definition.
|
|
26
|
+
A CI check catches drift between the Python app, the contract, and generated code.
|
|
27
|
+
|
|
28
|
+
## 1. Decorate your FastAPI endpoint
|
|
29
|
+
|
|
30
|
+
Install from GitHub with the optional codegen extra:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv add 'fastapi-streams[codegen] @ git+https://github.com/mathisarends/fastapi-streams.git'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Save this as `app.py`:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from collections.abc import AsyncIterator
|
|
40
|
+
from typing import Annotated, Literal
|
|
41
|
+
|
|
42
|
+
from fastapi import FastAPI
|
|
43
|
+
from pydantic import BaseModel, Field
|
|
44
|
+
|
|
45
|
+
from fastapi_streams import StreamRouter
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TokenDelta(BaseModel):
|
|
49
|
+
type: Literal["token.delta"]
|
|
50
|
+
text: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class RunCompleted(BaseModel):
|
|
54
|
+
type: Literal["run.completed"]
|
|
55
|
+
run_id: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
type RunEvent = Annotated[TokenDelta | RunCompleted, Field(discriminator="type")]
|
|
59
|
+
|
|
60
|
+
router = StreamRouter(prefix="/runs", tags=["agentRuns"])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.stream("/{run_id}/events", operation_id="stream_run_events")
|
|
64
|
+
async def stream_run_events(run_id: str) -> AsyncIterator[RunEvent]:
|
|
65
|
+
yield TokenDelta(type="token.delta", text="Hello")
|
|
66
|
+
yield RunCompleted(type="run.completed", run_id=run_id)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
app = FastAPI()
|
|
70
|
+
app.include_router(router, prefix="/v1")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The result is `GET /v1/runs/{run_id}/events`. `StreamRouter` extends `APIRouter`:
|
|
74
|
+
normal `Depends`, security dependencies, validation, middleware, and nested
|
|
75
|
+
routers keep working. FastAPI handles SSE framing, serialization, and heartbeats.
|
|
76
|
+
Use `methods=["POST"]` for request-body-driven streams.
|
|
77
|
+
|
|
78
|
+
## 2. Export once, generate both clients
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
uv run fastapi-streams schema \
|
|
82
|
+
--app app:app --output openapi.json
|
|
83
|
+
|
|
84
|
+
uv run fastapi-streams generate \
|
|
85
|
+
--schema openapi.json --language typescript --output generated/typescript
|
|
86
|
+
|
|
87
|
+
uv run fastapi-streams generate \
|
|
88
|
+
--schema openapi.json --language python --output generated/python
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
These commands import the app and read its contract; no running server is needed.
|
|
92
|
+
The exporter validates OpenAPI 3.2 and the event schemas. Both generators consume
|
|
93
|
+
one HTTP intermediate representation (IR), carrying operations, parameters, and
|
|
94
|
+
event types into the generated SDKs.
|
|
95
|
+
|
|
96
|
+
## 3. Consume typed events
|
|
97
|
+
|
|
98
|
+
The `RunEvent` union produces `RunEventType`, with the same named values in both
|
|
99
|
+
languages. The discriminator narrows each event to its matching model.
|
|
100
|
+
|
|
101
|
+
**TypeScript — Fetch, `AsyncIterable<RunEvent>`, no runtime dependencies:**
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { createClient, RunEventType } from "./generated/typescript/index.js";
|
|
105
|
+
|
|
106
|
+
const client = createClient({ baseUrl: "http://localhost:8000" });
|
|
107
|
+
|
|
108
|
+
for await (const event of client.agentRuns.streamRunEvents({ runId: "demo" })) {
|
|
109
|
+
switch (event.type) {
|
|
110
|
+
case RunEventType.TOKEN_DELTA:
|
|
111
|
+
console.log(event.text); // TokenDelta
|
|
112
|
+
break;
|
|
113
|
+
case RunEventType.RUN_COMPLETED:
|
|
114
|
+
console.log(event.run_id); // RunCompleted
|
|
115
|
+
break;
|
|
116
|
+
default: {
|
|
117
|
+
const exhaustive: never = event;
|
|
118
|
+
throw new Error(`Unexpected event: ${exhaustive}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Python — HTTPX, Pydantic models, and `StrEnum` values:**
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
import asyncio
|
|
128
|
+
from typing import assert_never
|
|
129
|
+
|
|
130
|
+
from generated.python import Client, RunEventType
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def main() -> None:
|
|
134
|
+
async with Client("http://localhost:8000") as client:
|
|
135
|
+
async for event in client.agent_runs.stream_run_events(run_id="demo"):
|
|
136
|
+
match event.type:
|
|
137
|
+
case RunEventType.TOKEN_DELTA:
|
|
138
|
+
print(event.text)
|
|
139
|
+
case RunEventType.RUN_COMPLETED:
|
|
140
|
+
print(event.run_id)
|
|
141
|
+
case _:
|
|
142
|
+
assert_never(event)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
asyncio.run(main())
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Install `httpx` and `pydantic` in a Python consumer (`uv add httpx pydantic`).
|
|
149
|
+
HTTPX remains the default; opt in to HTTPX2 at generation time with
|
|
150
|
+
`--python-http-client httpx2` and install `httpx2` instead. See
|
|
151
|
+
[Python HTTP library selection](docs/clients.md#python-http-library).
|
|
152
|
+
Both SDKs handle fragmented SSE frames, UTF-8 decoding, HTTP errors, and resource
|
|
153
|
+
cleanup. TypeScript supports `AbortSignal`; Python supports task cancellation and
|
|
154
|
+
explicit async-generator closure. See [client usage](docs/clients.md).
|
|
155
|
+
|
|
156
|
+
## 4. Catch contract drift in CI
|
|
157
|
+
|
|
158
|
+
Commit the exported schema and generated clients. Then check the complete chain:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
uv run fastapi-streams check \
|
|
162
|
+
--app app:app --schema openapi.json \
|
|
163
|
+
--language typescript --output generated/typescript
|
|
164
|
+
|
|
165
|
+
uv run fastapi-streams check \
|
|
166
|
+
--app app:app --schema openapi.json \
|
|
167
|
+
--language python --output generated/python
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`check` fails if the app's contract changed or generated files are stale. It
|
|
171
|
+
writes nothing and needs no running server. Pair it with TypeScript's compiler
|
|
172
|
+
and Pyright to catch invalid parameters and event access in consumer code.
|
|
173
|
+
|
|
174
|
+
| Check | What it verifies |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| Contract validation | OpenAPI structure, JSON Schemas, and local references. |
|
|
177
|
+
| `fastapi-streams check` | App → committed contract → generated client consistency. |
|
|
178
|
+
| TypeScript / Pyright | Parameter types, event fields, and exhaustive narrowing. |
|
|
179
|
+
|
|
180
|
+
[Codegen and CI reference →](docs/codegen.md)
|
|
181
|
+
|
|
182
|
+
## Run the complete example
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
git clone https://github.com/mathisarends/fastapi-streams.git
|
|
186
|
+
cd fastapi-streams
|
|
187
|
+
uv sync --all-extras
|
|
188
|
+
npm ci
|
|
189
|
+
uv run uvicorn examples.agent_runs:app --reload
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
In another terminal:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
# TypeScript
|
|
196
|
+
npx tsc -p tests/typescript/tsconfig.json
|
|
197
|
+
node tests/typescript/dist/examples/agent_runs/consume.js
|
|
198
|
+
|
|
199
|
+
# Python
|
|
200
|
+
uv run python -m examples.agent_runs.consume
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The example includes GET and authenticated POST streams, both generated SDKs,
|
|
204
|
+
and committed OpenAPI regression fixtures in `tests/fixtures/`. Verify all generated artifacts with
|
|
205
|
+
`uv run python scripts/codegen.py --check`.
|
|
206
|
+
|
|
207
|
+
## Documentation
|
|
208
|
+
|
|
209
|
+
| Guide | Topics |
|
|
210
|
+
| --- | --- |
|
|
211
|
+
| [Quickstart](docs/quickstart.md) | Setup, event models, and FastAPI integration. |
|
|
212
|
+
| [Contracts and codegen](docs/codegen.md) | OpenAPI 3.2, both generators, and drift checks. |
|
|
213
|
+
| [Client usage](docs/clients.md) | Enums, authentication, cancellation, and errors. |
|
|
214
|
+
| [Architecture](docs/architecture.md) | Native SSE, shared IR, templates, and transports. |
|
|
215
|
+
| [Development](docs/development.md) | Tests, supported scope, and roadmap. |
|
|
216
|
+
|
|
217
|
+
v0.0.1 supports GET/POST JSON SSE streams. Ordinary REST endpoints stay in the
|
|
218
|
+
contract for your existing REST SDK. See the [supported scope](docs/development.md#supported-scope-and-roadmap).
|
|
219
|
+
|
|
220
|
+
Python 3.12+ · FastAPI 0.143.3+ · Pydantic v2 · [MIT](LICENSE)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
Define typed events in FastAPI, export their OpenAPI 3.2 contract, and generate
|
|
4
|
+
TypeScript Fetch and Python HTTPX clients from the same definitions.
|
|
5
|
+
|
|
6
|
+
| Guide | What you will find |
|
|
7
|
+
| --- | --- |
|
|
8
|
+
| [Quickstart](quickstart.md) | Run the complete example and define your first stream. |
|
|
9
|
+
| [Contracts and codegen](codegen.md) | Export OpenAPI, generate both clients, and detect drift in CI. |
|
|
10
|
+
| [Client usage](clients.md) | Named event values, typed iteration, authentication, cancellation, and errors. |
|
|
11
|
+
| [Development](development.md) | Local checks, test coverage, supported features, and roadmap. |
|
|
12
|
+
| [Architecture](architecture.md) | Server integration, shared IR, generation, and transport boundaries. |
|
|
13
|
+
|
|
14
|
+
Start with the [quickstart](quickstart.md), then use the
|
|
15
|
+
[codegen guide](codegen.md) to integrate the clients into your project.
|
|
16
|
+
|
|
17
|
+
Python 3.12+, FastAPI 0.143.3+, and Pydantic v2 are required. Code generation is
|
|
18
|
+
an optional extra: `fastapi-streams[codegen]`. No running server is needed to
|
|
19
|
+
export contracts or generate clients.
|
|
20
|
+
|
|
21
|
+
The [example application](../examples/agent_runs/__init__.py) includes GET and
|
|
22
|
+
authenticated POST streams. Its [TypeScript](../examples/agent_runs/consume.ts)
|
|
23
|
+
and [Python](../examples/agent_runs/consume.py) consumers demonstrate the complete
|
|
24
|
+
journey. Generated clients and the [contract fixture](../tests/fixtures/agent_runs.openapi.json)
|
|
25
|
+
are committed so you can inspect their output.
|
|
26
|
+
|
|
27
|
+
[Back to the project](../README.md)
|