Skip to content

Commit 2e0dada

Browse files
authored
Python SDK: Make response helpers opinionated about taking 0..N datastar events (#918)
* Make datastar opinionated about taking 0..N datastar events * Remove old response class aliases * Improve responses for sanic as well * Update sanic example * Switch quart to using DatastarResponse like the other frameworks * Various cleanups * Typing/linting from attribute generator merge * Update response class documentation in the readme * Declare the python sdk as typed
1 parent 5057c10 commit 2e0dada

File tree

20 files changed

+300
-193
lines changed

20 files changed

+300
-193
lines changed

examples/python/django/ds/views.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import datetime
44

55
from datastar_py.django import (
6-
DatastarStreamingHttpResponse,
6+
DatastarResponse,
77
ServerSentEventGenerator,
88
read_signals,
99
)
@@ -65,7 +65,7 @@ async def time_updates():
6565
)
6666
await asyncio.sleep(1)
6767

68-
return DatastarStreamingHttpResponse(time_updates())
68+
return DatastarResponse(time_updates())
6969

7070

7171
# WSGI Example
@@ -121,4 +121,4 @@ def time_updates():
121121
)
122122
time.sleep(0.5)
123123

124-
return DatastarStreamingHttpResponse(time_updates())
124+
return DatastarResponse(time_updates())

examples/python/fastapi/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import uvicorn
1515
from datastar_py.fastapi import (
16-
DatastarStreamingResponse,
16+
DatastarResponse,
1717
ReadSignals,
1818
ServerSentEventGenerator,
1919
)
@@ -80,7 +80,7 @@ async def time_updates():
8080
async def updates(signals: ReadSignals):
8181
# ReadSignals is a dependency that automatically loads the signals from the request
8282
print(signals)
83-
return DatastarStreamingResponse(time_updates())
83+
return DatastarResponse(time_updates())
8484

8585

8686
if __name__ == "__main__":

examples/python/fasthtml/advanced.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from pathlib import Path
1818

1919
import polars as pl
20-
from datastar_py.fasthtml import DatastarStreamingResponse, ServerSentEventGenerator
20+
from datastar_py.fasthtml import DatastarResponse, ServerSentEventGenerator
2121
from great_tables import GT
2222
from great_tables.data import reactions
2323

@@ -94,10 +94,7 @@ def GreatTable(pattern=default_pattern):
9494
# rendered table into the DOM with the request's 'filter' value
9595
@app.post
9696
async def table(filter: str):
97-
async def _():
98-
yield ServerSentEventGenerator.merge_fragments(GreatTable(filter))
99-
100-
return DatastarStreamingResponse(_())
97+
return DatastarResponse(ServerSentEventGenerator.merge_fragments(GreatTable(filter)))
10198

10299

103100
# Define default route which returns a FastTag from a GET request.
@@ -159,7 +156,7 @@ async def clock():
159156

160157
@rt
161158
async def time():
162-
return DatastarStreamingResponse(clock())
159+
return DatastarResponse(clock())
163160

164161

165162
@rt
@@ -169,7 +166,7 @@ async def _():
169166
await asyncio.sleep(1)
170167
yield ServerSentEventGenerator.merge_fragments(HELLO_BUTTON)
171168

172-
return DatastarStreamingResponse(_())
169+
return DatastarResponse(_())
173170

174171

175172
@rt
@@ -189,7 +186,7 @@ async def _():
189186
await asyncio.sleep(1)
190187
yield ServerSentEventGenerator.merge_fragments(reset_and_hello)
191188

192-
return DatastarStreamingResponse(_())
189+
return DatastarResponse(_())
193190

194191

195192
# Define the button once so that it can be used in the index response

examples/python/fasthtml/simple.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from datetime import datetime
1313
from pathlib import Path
1414

15-
from datastar_py.fasthtml import DatastarStreamingResponse, ServerSentEventGenerator, read_signals
15+
from datastar_py.fasthtml import DatastarResponse, ServerSentEventGenerator, read_signals
1616

1717
# ruff: noqa: F403, F405
1818
from fasthtml.common import *
@@ -66,7 +66,7 @@ async def clock():
6666
async def updates(request):
6767
signals = await read_signals(request)
6868
print(signals)
69-
return DatastarStreamingResponse(clock())
69+
return DatastarResponse(clock())
7070

7171

7272
if __name__ == "__main__":

examples/python/litestar/app.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
from datetime import datetime
1313

1414
import uvicorn
15-
from datastar_py.litestar import DatastarSSE, ServerSentEventGenerator, read_signals
15+
from datastar_py.litestar import (
16+
DatastarResponse,
17+
ServerSentEventGenerator,
18+
read_signals,
19+
)
20+
from datastar_py.sse import DatastarEvent
1621

1722
from litestar import Litestar, MediaType, get
1823
from litestar.di import Provide
@@ -57,7 +62,7 @@ async def read_root() -> str:
5762
return HTML.replace("CURRENT_TIME", f"{datetime.isoformat(datetime.now())}")
5863

5964

60-
async def time_updates() -> AsyncGenerator[str, None]:
65+
async def time_updates() -> AsyncGenerator[DatastarEvent, None]:
6166
while True:
6267
yield ServerSentEventGenerator.merge_fragments(
6368
f"""<span id="currentTime">{datetime.now().isoformat()}"""
@@ -72,9 +77,9 @@ async def time_updates() -> AsyncGenerator[str, None]:
7277
# We aren't using the signals for anything meaningful here, but `read_signals` can be
7378
# used as a dependency to automatically parse the signals from the request.
7479
@get("/updates", dependencies={"signals": Provide(read_signals)})
75-
async def updates(signals: dict | None) -> DatastarSSE:
80+
async def updates(signals: dict | None) -> DatastarResponse:
7681
print(signals)
77-
return DatastarSSE(time_updates())
82+
return DatastarResponse(time_updates())
7883

7984

8085
app = Litestar(route_handlers=[read_root, updates])

examples/python/quart/app.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from datetime import datetime
1212

1313
from datastar_py.quart import (
14+
DatastarResponse,
1415
ServerSentEventGenerator,
15-
make_datastar_response,
1616
read_signals,
1717
)
1818

@@ -72,8 +72,7 @@ async def time_updates():
7272
)
7373
await asyncio.sleep(1)
7474

75-
response = await make_datastar_response(time_updates())
76-
return response
75+
return DatastarResponse(time_updates())
7776

7877

7978
@app.route("/")

examples/python/sanic/app.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111
from datetime import datetime
1212

1313
from datastar_py.consts import FragmentMergeMode
14-
from datastar_py.sanic import ServerSentEventGenerator, datastar_respond, read_signals
14+
from datastar_py.sanic import (
15+
DatastarResponse,
16+
ServerSentEventGenerator,
17+
datastar_respond,
18+
read_signals,
19+
)
1520

1621
from sanic import Sanic
1722
from sanic.response import html
@@ -62,9 +67,7 @@ async def hello_world(request):
6267

6368
@app.get("/add_signal")
6469
async def add_signal(request):
65-
response = await datastar_respond(request)
66-
67-
await response.send(
70+
return DatastarResponse(
6871
ServerSentEventGenerator.merge_fragments(
6972
"""
7073
<div class="time signal">
@@ -76,14 +79,10 @@ async def add_signal(request):
7679
)
7780
)
7881

79-
await response.eof()
80-
8182

8283
@app.get("/add_fragment")
8384
async def add_fragment(request):
84-
response = await datastar_respond(request)
85-
86-
await response.send(
85+
return DatastarResponse(
8786
ServerSentEventGenerator.merge_fragments(
8887
f"""\
8988
<div class="time fragment">
@@ -95,8 +94,6 @@ async def add_fragment(request):
9594
)
9695
)
9796

98-
await response.eof()
99-
10097

10198
@app.get("/updates")
10299
async def updates(request):

sdk/python/README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,38 @@ async def updates():
4848

4949
## Response Helpers
5050

51-
The response for the quart example above could be rewritten using the helpers:
51+
A datastar response consists of 0..N datastar events. There are response
52+
classes included to make this easy in all of the supported frameworks.
53+
54+
The following examples will work across all supported frameworks when the
55+
response class is imported from the appropriate framework package.
56+
e.g. `from datastar_py.quart import DatastarResponse` The containing functions
57+
are not shown here, as they will differ per framework.
58+
59+
5260
```python
53-
return await make_datastar_response(time_updates())
61+
# 0 events, a 204
62+
return DatastarResponse()
63+
# 1 event
64+
return DatastarResponse(ServerSentEventGenerator.merge_fragments("<div id='mydiv'></div>"))
65+
# 2 events
66+
return DatastarResponse([
67+
ServerSentEventGenerator.merge_fragments("<div id='mydiv'></div>"),
68+
ServerSentEventGenerator.merge_signals({"mysignal": "myval"}),
69+
])
70+
# N events, a long lived stream (for all frameworks but sanic)
71+
async def updates():
72+
while True:
73+
yield ServerSentEventGenerator.merge_fragments("<div id='mydiv'></div>")
74+
await asyncio.sleep(1)
75+
return DatastarResponse(updates())
76+
# A long lived stream for sanic
77+
response = await datastar_respond(request)
78+
# which is just a helper for the following
79+
# response = await request.respond(DatastarResponse())
80+
while True:
81+
await response.send(ServerSentEventGenerator.merge_fragments("<div id='mydiv'></div>"))
82+
await asyncio.sleep(1)
5483
```
5584

5685
## Signal Helpers

sdk/python/pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,5 +104,9 @@ select = [
104104
"SIM",
105105
# isort
106106
"I",
107+
# Annotations
108+
"ANN",
109+
# Ruff specific
110+
"RUF",
107111
]
108-
fixable = ["ALL"]
112+
fixable = ["ALL"]

sdk/python/src/datastar_py/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
from collections.abc import Mapping
55
from typing import Any
66

7-
from .sse import SSE_HEADERS, ServerSentEventGenerator
87
from .attributes import attribute_generator
8+
from .sse import SSE_HEADERS, ServerSentEventGenerator
99

10-
__all__ = ["attribute_generator", "ServerSentEventGenerator", "SSE_HEADERS"]
10+
__all__ = ["SSE_HEADERS", "ServerSentEventGenerator", "attribute_generator"]
1111

1212

1313
def _read_signals(
14-
method: str, headers: Mapping, params: Mapping, body: str | bytes
14+
method: str, headers: Mapping[str, str], params: Mapping, body: str | bytes
1515
) -> dict[str, Any] | None:
1616
if "Datastar-Request" not in headers:
1717
return None

0 commit comments

Comments
 (0)