Skip to content

Commit f1f13d5

Browse files
authored
Add SDK tests for python sdk. (#975)
Make some tweaks to fully align with sdk tests.
1 parent 3724c3b commit f1f13d5

File tree

2 files changed

+74
-10
lines changed

2 files changed

+74
-10
lines changed

sdk-test.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.13"
3+
# dependencies = [
4+
# "datastar-py",
5+
# "sanic",
6+
# ]
7+
# [tool.uv.sources]
8+
# datastar-py = { path = "." }
9+
# ///
10+
11+
"""
12+
Runs a test server that the SDK tests can be run against.
13+
1. Start this server with `uv run sdk-test.py`
14+
2. Move to the sdk/tests folder.
15+
3. Run `test-all.sh http://127.0.0.1:8000` to run the tests.
16+
"""
17+
import re
18+
19+
from sanic import Request, Sanic
20+
21+
from datastar_py import ServerSentEventGenerator as SSE
22+
from datastar_py.sanic import DatastarResponse, read_signals
23+
from datastar_py.sse import DatastarEvent
24+
25+
app = Sanic("datastar-sdk-test")
26+
27+
28+
@app.route("/test", methods=["GET", "POST"])
29+
async def test_route(request: Request) -> None:
30+
signals = await read_signals(request)
31+
events: list[dict] = signals["events"]
32+
33+
response = await request.respond(response=DatastarResponse())
34+
35+
for event in events:
36+
await response.send(build_event(event))
37+
38+
39+
def build_event(input: dict) -> DatastarEvent:
40+
event_type = input.pop("type")
41+
signals_raw = input.pop("signals-raw", None)
42+
kwargs = {camel_to_snake(k): v for k, v in input.items()}
43+
if signals_raw:
44+
kwargs["signals"] = signals_raw
45+
return getattr(SSE, camel_to_snake(event_type))(**kwargs)
46+
47+
48+
def camel_to_snake(text: str) -> str:
49+
return re.sub(r"(.)([A-Z])", r"\1_\2", text).lower()
50+
51+
52+
if __name__ == "__main__":
53+
app.run(host="0.0.0.0", port=8000)

src/datastar_py/sse.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

33
import json
4-
from collections.abc import AsyncIterable, Iterable
4+
from collections.abc import AsyncIterable, Iterable, Mapping
55
from itertools import chain
66
from typing import Literal, Protocol, TypeAlias, Union, overload, runtime_checkable
77

88
import datastar_py.consts as consts
9+
from datastar_py.attributes import _escape
910

1011
SSE_HEADERS: dict[str, str] = {
1112
"Cache-Control": "no-cache",
@@ -46,12 +47,11 @@ def _send(
4647
event_id: str | None = None,
4748
retry_duration: int | None = None,
4849
) -> DatastarEvent:
49-
prefix = []
50+
prefix = [f"event: {event_type}"]
51+
5052
if event_id:
5153
prefix.append(f"id: {event_id}")
5254

53-
prefix.append(f"event: {event_type}")
54-
5555
if retry_duration and retry_duration != consts.DEFAULT_SSE_RETRY_DURATION:
5656
prefix.append(f"retry: {retry_duration}")
5757

@@ -94,7 +94,7 @@ def patch_elements(
9494
if isinstance(elements, _HtmlProvider):
9595
elements = elements.__html__()
9696
data_lines = []
97-
if mode:
97+
if mode and mode != "outer": # TODO: Should there be a constant for this?
9898
data_lines.append(f"{consts.MODE_DATALINE_LITERAL} {mode}")
9999
if selector:
100100
data_lines.append(f"{consts.SELECTOR_DATALINE_LITERAL} {selector}")
@@ -132,7 +132,7 @@ def remove_elements(
132132
@classmethod
133133
def patch_signals(
134134
cls,
135-
signals: dict,
135+
signals: dict | str,
136136
event_id: str | None = None,
137137
only_if_missing: bool | None = None,
138138
retry_duration: int | None = None,
@@ -146,7 +146,13 @@ def patch_signals(
146146
f"{consts.ONLY_IF_MISSING_DATALINE_LITERAL} {_js_bool(only_if_missing)}"
147147
)
148148

149-
data_lines.append(f"{consts.SIGNALS_DATALINE_LITERAL} {json.dumps(signals)}")
149+
signals_str = (
150+
signals if isinstance(signals, str) else json.dumps(signals, separators=(",", ":"))
151+
)
152+
data_lines.extend(
153+
f"{consts.SIGNALS_DATALINE_LITERAL} {line}"
154+
for line in signals_str.splitlines()
155+
)
150156

151157
return ServerSentEventGenerator._send(
152158
consts.EventType.PATCH_SIGNALS, data_lines, event_id, retry_duration
@@ -157,15 +163,20 @@ def execute_script(
157163
cls,
158164
script: str,
159165
auto_remove: bool = True,
160-
attributes: list[str] | None = None,
166+
attributes: Mapping[str, str] | list[str] | None = None,
161167
event_id: str | None = None,
162168
retry_duration: int | None = None,
163169
) -> DatastarEvent:
164170
attribute_string = ""
165171
if auto_remove:
166-
attribute_string += " data-effect='el.remove()'"
172+
attribute_string += ' data-effect="el.remove()"'
167173
if attributes:
168-
attribute_string += " " + " ".join(attributes)
174+
if isinstance(attributes, Mapping):
175+
attribute_string += " " + " ".join(
176+
f'{_escape(k)}="{_escape(v)}"' for k, v in attributes.items()
177+
)
178+
else:
179+
attribute_string += " " + " ".join(attributes)
169180
script_tag = f"<script{attribute_string}>{script}</script>"
170181

171182
return ServerSentEventGenerator.patch_elements(

0 commit comments

Comments
 (0)