Skip to content

Commit c1e0d12

Browse files
committed
Clarify decorator streaming behavior in docs
1 parent 5478d7f commit c1e0d12

File tree

3 files changed

+124
-5
lines changed

3 files changed

+124
-5
lines changed

README.md

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,15 @@ async def updates(request):
135135
### Response Decorator
136136
To make returning a `DatastarResponse` simpler, there is a decorator
137137
`datastar_response` available that automatically wraps a function result in
138-
`DatastarResponse`. It works on async and regular functions and generator
139-
functions. The main use case is when using a generator function, as you can
140-
avoid a second generator function inside your response function. The decorator
141-
works the same for any of the supported frameworks, and should be used under
142-
any routing decorator from the framework.
138+
`DatastarResponse`. The decorator inspects what your function returns:
139+
140+
- async generators/iterables (`__aiter__`) and sync generators/iterables (`__iter__`) are streamed
141+
- awaitables (coroutines/tasks) are awaited and their single result is streamed once
142+
- everything else is wrapped as-is
143+
144+
That means you can write a generator route handler without adding an extra
145+
wrapper generator. Apply `@datastar_response` under your framework's route
146+
decorator.
143147

144148
```python
145149
# Import the decorator from the package specific to your framework
@@ -153,6 +157,20 @@ async def my_route(request):
153157
await asyncio.sleep(1)
154158
```
155159

160+
You can also return a single event from an async function and it will be awaited
161+
and streamed once:
162+
163+
```python
164+
@app.get("/single")
165+
@datastar_response
166+
async def single_event(request):
167+
return SSE.patch_signals({"done": True})
168+
```
169+
170+
Note: the Quart and Django adapters keep the wrapper async (as the frameworks expect),
171+
so when calling a decorated view directly you must still `await` it. The other adapters
172+
return a `DatastarResponse` immediately.
173+
156174
## Signal Helpers
157175
The current state of the datastar signals is included by default in every
158176
datastar request. A helper is included to load those signals for each
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# /// script
2+
# dependencies = ["datastar-py", "fastapi", "uvicorn"]
3+
# ///
4+
import asyncio
5+
from datetime import datetime
6+
7+
from datastar_py.fastapi import datastar_response
8+
from datastar_py.sse import ServerSentEventGenerator as SSE
9+
from fastapi import FastAPI
10+
from fastapi.responses import HTMLResponse
11+
12+
app = FastAPI()
13+
14+
15+
@app.get("/", response_class=HTMLResponse)
16+
async def index():
17+
return """
18+
<!DOCTYPE html>
19+
<html>
20+
<head>
21+
<title>Datastar.py FastAPI Decorator Example</title>
22+
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js"></script>
23+
</head>
24+
<body >
25+
<h1>Datastar.py FastAPI Decorator Example</h1>
26+
<div id="time-div" data-init="@get('/updates')"></div>
27+
</body>
28+
</html>
29+
"""
30+
31+
32+
@app.get("/updates")
33+
@datastar_response
34+
async def updates():
35+
"""
36+
This async generator yields a new time every second. The @datastar_response
37+
decorator handles wrapping the yielded events in a DatastarResponse.
38+
"""
39+
while True:
40+
now = datetime.now().isoformat()
41+
yield SSE.patch_elements(f'<div id="time-div">Current Time: {now}</div>')
42+
await asyncio.sleep(1)
43+
44+
45+
if __name__ == "__main__":
46+
import uvicorn
47+
48+
uvicorn.run(app, host="0.0.0.0", port=8000)

examples/fasthtml/decorator.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# /// script
2+
# requires-python = ">=3.12"
3+
# dependencies = ["datastar-py", "python-fasthtml"]
4+
# [tool.uv.sources]
5+
# datastar-py = { path = "../../" }
6+
# ///
7+
import asyncio
8+
from datetime import datetime
9+
10+
# ruff: noqa: F403, F405
11+
from fasthtml.common import *
12+
13+
from datastar_py.fasthtml import datastar_response
14+
from datastar_py.sse import ServerSentEventGenerator as SSE
15+
16+
app, rt = fast_app(
17+
htmx=False,
18+
hdrs=(
19+
Script(
20+
type="module",
21+
src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js",
22+
),
23+
),
24+
)
25+
26+
27+
@rt("/")
28+
def index():
29+
return Titled(
30+
"Datastar.py FastHTML Decorator Example",
31+
Body(
32+
Div(cls="wrapper")(
33+
Div(id="time-div", data_init="@get('/updates')"),
34+
)
35+
),
36+
)
37+
38+
39+
@rt("/updates")
40+
@datastar_response
41+
async def updates():
42+
"""
43+
Async generator that streams a new timestamp every second.
44+
The @datastar_response decorator wraps the generator in a DatastarResponse.
45+
"""
46+
while True:
47+
now = datetime.now().isoformat()
48+
yield SSE.patch_elements(Div(id="time-div")(f"Current Time: {now}"))
49+
await asyncio.sleep(1)
50+
51+
52+
if __name__ == "__main__":
53+
serve()

0 commit comments

Comments
 (0)