Stario in ten minutes

Five ideas. Learn them and you can read any Stario app.

This is a bold claim, but I stand behind it: if you understand these five ideas, you can read any Stario app, and you can write most of one. Five minutes to read them once. Five more to read them again.

1. A route is a value#

A Route is one HTTP method on one path. You create it once, at import time, and you use the same object everywhere: to register a handler, to build a link, to fire a request from the browser.

from stario import Route
 
HOME = Route("GET /")
USER = Route("GET /users/{user_id}")
 
USER.href(user_id=42)  # "/users/42"

There is no @app.get decorator and no url_for("name"). The route is the name. A path with two methods is two routes: Route("GET /profile") and Route("POST /profile").

2. A handler is async def handler(c, w)#

Every handler has the same shape. c is the context: the request, the matched route, telemetry, and a dict for this request. w is the writer: the response on this connection.

async def user(c: Context, w: Writer) -> None:
    user_id = c.match.params["user_id"]
    w.html(user_view(user_id))

The handler does not return a response. It writes one. That one difference is what makes the next idea possible.

3. You decide when bytes go out#

w.html(...) sends a whole page and finishes the response. But you can also reply first and keep working, or write many times over a long time. The signature is the same in all three cases.

async def save(c: Context, w: Writer) -> None:
    w.empty()  # the browser has its answer
    await notify_followers()  # the handler keeps running
 
 
async def clock(c: Context, w: Writer) -> None:
    sse = SSE(w)
    async with c.alive():
        while True:
            sse.patch_elements(h.P({"id": "clock"}, now()))
            await asyncio.sleep(1)

c.alive() ends the handler when the browser leaves or the server shuts down.

4. HTML is Python#

Tags are functions. A dict becomes attributes. Other arguments become children. A view is a plain function that returns a tree.

from stario import h
 
 
def user_view(name: str):
    return h.Div(
        {"id": "user", "class": "card"},
        h.H2(name),
        h.P("Signed in"),
    )

There is no template language, and no second syntax for loops or conditions. You already know the language.

5. A live page is one open response#

When a page should change while it is open, the page holds one response open and the server pushes new HTML into it. Short commands go the other way as normal POSTs. Datastar, a small library in the page, puts each patch into the document.

async def subscribe(c: Context, w: Writer) -> None:
    sse = SSE(w)
    async with relay.subscribe("board") as live:
        sse.patch_elements(board_view(game))
        async for _ in c.alive(live):
            sse.patch_elements(board_view(game))
 
 
async def click(c: Context, w: Writer) -> None:
    w.empty()
    game.paint(cell_from(c))
    relay.publish("board", "painted")

The command does not render anything. It changes state and says so. Every open page re-renders from the same state. This is the whole of Realtime tiles, and Live pages explains why it is shaped like this.

And bootstrap#

One more thing, and it is small. bootstrap is where the app is assembled. It is an async generator with exactly one yield. Before the yield is startup. After it is shutdown.

async def bootstrap(app: App, span: Span):
    app.add(HOME, home)
    app.add(USER, user)
    yield

Shared objects such as a database or a Relay are created here and handed to handlers. Realtime tiles shows the pattern.

That is it. To see the ideas run, go to Hello world. To know why the pieces are shaped this way, read The Stario way.