Datastar SDK
Datastar is the browser client. stario.datastar is the Python side. Attribute names and client behaviour live in the Datastar reference. Write the Python. The SDK emits the attributes. Why the split looks this way: Go-to architecture.
from stario.datastar import ModuleScript, SSE, at, data, read_signalsThe Datastar reference is the API map.
Load the client
from stario.datastar import ModuleScriptfrom stario.markup import html as h h.Head( ModuleScript(), # or a self-hosted file: # h.Script({"type": "module", "src": DATASTAR_JS}),)Self-host the file in production. ModuleScript() defaults to a CDN pin for experiments.
Views in Python
data.* returns attribute fragments. at.* returns the action string that goes inside them. Prefer at.fetch(route) when the endpoint is a Route — method and URL stay on one object.
from stario import Routefrom stario.datastar import at, datafrom stario.markup import html as h SUBSCRIBE = Route.get("/subscribe")PAINT = Route.post("/click") def board(): return h.Div( data.signals({"user_id": ""}, if_missing=True), data.init(at.fetch(SUBSCRIBE, retry="always")), h.Button(data.on("click", at.fetch(PAINT)), "Paint"), )The browser sees data-init, data-on:click, @get, @post. at.get / at.post remain for raw URL strings.
Bind a field with data.bind. Show a signal with data.text.
h.Input(data.bind("email"), {"type": "email"})h.Output(data.text("$count"))Read what the page sent
Datastar sends one JSON object on each action. read_signals(req) is json.loads on that blob. Treat it as untrusted. Signal names on the wire are Python snake_case.
from stario.datastar import SSE, read_signals async def increment(c, w): signals = await read_signals(c.req) count = int(signals.get("count") or 0) SSE(w).patch_signals({"count": count + 1})Typed reads and file signals: Reading and writing signals.
Patch HTML on the stream
One SSE(w) per response. patch_elements morphs HTML. patch_signals merges JSON. Do not mix SSE with a completed response on the same writer.
from stario.datastar import SSE async def subscribe(c, w): sse = SSE(w) sse.open() async with relay.subscribe("board") as live: async for _, _nudge in c.alive(live): sse.patch_elements(board_view(game), selector="#board")A command returns 204 and publishes. Go-to architecture. Realtime tiles.
Use data-star.dev for an attribute modifier, a Pro feature, or the client runtime.