# Stario - Extended Context For LLMs Stario 4 is a Python 3.14+ HTTP framework for server-rendered hypermedia apps. It uses explicit routes, async handlers, `Writer` responses, optional Datastar realtime over SSE, and built-in telemetry. It is not ASGI. Do not mount it in FastAPI, Starlette, Uvicorn, or Hypercorn. Run with `stario serve` or `stario watch` and a bootstrap callable such as `app.main:bootstrap`. For repo-local agent instructions, copy: https://stario.dev/AGENTS.md ## Core Invariants 1. `bootstrap` is `async def bootstrap(app: App, span: Span):` with exactly one `yield`. 2. Startup work happens before `yield`: create shared clients, register static assets, register routes. Teardown happens after `yield`. 3. Handlers are `async def handler(c: Context, w: Writer) -> None`; they write with `responses.*`, `SSE(w)`, or direct `Writer` methods. They do not return response objects. 4. Routes are `UrlPath` constants. Use `.href()` in markup and redirects. Do not use `url_for` or ASGI-style mounts. 5. Views are pure `data -> HTML` functions using `from stario.markup import html as h`. 6. Long-lived streams use `c.alive(iterable)`. Background work uses `c.app.create_task(...)`. 7. Datastar signals are request input, not source of truth. Authoritative state lives on the server. 8. Configure with `STARIO_*` environment variables. Stario does not load `.env` files. ## Common Imports ```python from stario import App, AssetManifest, Context, Relay, Span, StaticAssets, UrlPath, Writer from stario import HttpException, RedirectException import stario.responses as responses from stario.markup import html as h from stario.datastar import SSE, at, data, read_signals from stario.testing import TestClient ``` ## Minimal Handler Shape This is useful for tutorials, not the recommended product layout: ```python from stario import App, Context, Span, UrlPath, Writer import stario.responses as responses HOME = UrlPath("/") async def home(_c: Context, w: Writer) -> None: responses.text(w, "Hello") async def bootstrap(app: App, span: Span): span.attr("app.name", "example") app.get(HOME, home) yield ``` Run a tutorial file with `uv run stario watch main:bootstrap`. Run a product-shaped app with `uv run stario watch app.main:bootstrap`. ## Recommended App Layout Use this from day one for new apps, even with one feature. Reference implementation: https://github.com/bobowski/stario/tree/main/examples/chat-room ```text app/ main.py bootstrap; composition root assets.py AssetManifest + href constants, when needed config.py env-first Config, when needed db.py thin database core, when needed common/ shared shell, markup, identity, helpers features/ / urls.py UrlPath constants handlers.py handler factories + register_(app, ...) views.py pure markup builders models.py optional domain dataclasses data.py optional schema + queries signals.py optional Datastar signal reader subjects.py optional Relay subject helpers static/ tests/ test_.py AGENTS.md ``` Feature files are optional. Add the file when the feature has that job; do not create empty pattern files. Cross-feature imports should stay deliberate: importing another feature's `urls.py` for links is normal, but domain ownership should flow one way. When building a first feature, put it under `app/features//`. Use `UrlPath` constants in `urls.py`, route registration in `handlers.py`, pure markup views in `views.py`, and `TestClient` tests under `tests/`. Run `uv run ruff check .`, `uv run pyright`, and `uv run pytest` before finishing. ## Bootstrap And Route Registration `app/main.py` creates dependencies and calls `register_*`. Feature modules own their routes. ```python # app/features/room/urls.py from stario import UrlPath ROOMS = UrlPath("/rooms") ROOM = ROOMS / "{room_id}" SUBSCRIBE = ROOM / "subscribe" SEND = ROOM / "send" ``` ```python # app/features/room/handlers.py def register_room(app: App, db, relay: Relay[str]) -> None: app.get(ROOM, show_room(db)) app.get(SUBSCRIBE, subscribe(db, relay)) app.post(SEND, send_message(db, relay)) ``` Handlers commonly use factories so dependencies stay explicit: ```python def show_room(db): async def handler(c: Context, w: Writer) -> None: room = load_room(db, c.route.params["room_id"]) responses.html(w, room_view(room)) return handler ``` ## Datastar Realtime Datastar is optional. Use it when the UX needs push updates; otherwise plain request/response is easier to reason about. The common live shape is: - `GET /page`: first paint. - `GET /subscribe`: long-lived SSE stream. - `POST /action`: validate signals or URL params, mutate server state, `relay.publish(...)`, then usually `responses.empty(w, 204)` or redirect. - Subscribe loop: receive notification, re-read authoritative state, render HTML, send a patch with `SSE(w)`. Use `async with relay.subscribe(...) as live:` before publishing events this connection must observe. Iterate with `async for event in c.alive(live):`. Start with broad morphs: ```python sse = SSE(w) sse.patch_elements(home_view(state)) ``` This can be a whole page or a large fragment. It keeps the rule simple: `ui = f(state)`. Datastar morphs the DOM instead of blindly replacing everything, and compression makes repeated HTML practical for many pages. Optimize only after the broad morph is correct and you have evidence. Then choose one of these: - Stable shell plus live fragment: keep `data.signals(...)` and `data.init(...)` on an outer shell, patch `#feature-live` with `selector=...` and `mode="outer"`. - Targeted patch: patch one small selector, often with `mode="inner"` or `patch_signals`, when the update is truly local. - Navigation: use `sse.navigate(url)` when server state invalidates the current page. `Relay` is in-process and not durable. For multiple workers, keep the same command/subscribe shape and replace Relay with Redis, NATS, Postgres notifications, or another broker. ## Static Assets ```python from pathlib import Path from stario import AssetManifest, StaticAssets ASSETS = AssetManifest(Path(__file__).resolve().parent / "static") async def bootstrap(app: App, span: Span): static = StaticAssets(ASSETS) static.register(app) yield ``` Use `ASSETS.href("css/app.css")` in views or templates. ## Testing Prefer integration tests against the production bootstrap: ```python async with TestClient(bootstrap) as client: response = await client.get("/") assert response.status_code == 200 ``` Use `client.tracer` for span assertions. ## Telemetry Bootstrap receives `span`; handlers use `c.span`. For local SQLite traces: ```bash STARIO_TRACER=sqlite STARIO_TRACERS_SQLITE=traces.sqlite3 uv run stario serve app.main:bootstrap ``` HTTP spans store `request.method`, `request.path`, and `response.status_code` in `attrs_json`: ```sql SELECT attrs_json ->> 'request.path', duration_ns / 1e6 AS ms FROM spans WHERE attrs_json ->> 'request.path' IS NOT NULL ORDER BY end_ns DESC LIMIT 20; ``` ## Commands ```bash uv sync uv run ruff check . uv run ruff format . uv run pyright uv run pytest uv run stario watch app.main:bootstrap ``` ## Key URLs - https://stario.dev/AGENTS.md - https://stario.dev/docs/how-tos/ai-assisted-development - https://stario.dev/docs/how-tos/structuring-apps - https://stario.dev/docs/explanation/go-to-architecture - https://stario.dev/docs/reference/routing - https://stario.dev/docs/reference/runtime - https://stario.dev/docs/reference/datastar - https://stario.dev/docs/reference/testing - https://github.com/bobowski/stario/tree/main/examples/chat-room