Site your state, derive the wire — a Python→JS component compiler with an explicit client/server seam, for Litestar.
Project description
Site your state, derive the wire.
situ lets a Python developer write a reactive web UI as one component — real HTML on top, Python signals and handlers below — and declare, per piece of state, where it lives. A real Python→JS compiler reads that declaration and emits a small, app-specific client island; the client/server boundary stays explicit and is enforced at compile time. You author no client JavaScript, and you can read every line the compiler ships.
The compiler and the mount core are framework-neutral; situ ships a Litestar adapter (the reference) and a Flask one, and a component mounts on either unchanged. It is for Python full-stack and backend developers who reach for hypermedia (htmx, Datastar) and hit the wall where a piece of state wants to live in the browser — a live search, a selection, an open dialog.
situ is a young, alpha library extracted from a research programme (the webui repository); the compiler accepts a bounded dialect of Python and fails closed on anything outside it. See Status and the known limits.
The one idea: site each piece of state
Every signal declares its site, and the transport follows from that — you never write a fetch, a target, or an SSE wire.
| Site | Where it lives | What the compiler derives |
|---|---|---|
Local[T] |
the browser | compiled to JS — zero network |
Url[T] |
the query string | a shareable link the server re-renders |
Server[Facade] |
the database | a POST command that re-renders one region |
Synced[T] |
(reserved) | a local-first replica — design stage |
The boundary is a compile-time invariant: a client read of Server-sited state is a
CompileError, so database-backed state cannot reach the browser by accident.
Install
pip install situ # the compiler + the Litestar mount
pip install "situ[flask]" # + the Flask (WSGI) adapter
pip install "situ[sqlalchemy]" # + the SQLAlchemy/Dishka session helpers
pip install "situ[model-adapters]" # + attrs/msgspec/pydantic model sources for declui
Requires Python 3.12+.
Sixty seconds of situ
A component is two sibling files sharing a stem, so each gets native editor tooling.
counter.py — the signals and handlers:
from situ import Local
count: Local[int] = 0 # client state — compiled to JS, no network
def bump() -> None:
global count # names the local signal this handler writes
count = count + 1
counter.html — real HTML with shorthand reactive attributes:
<div data-region>
<button @click="bump">+1</button>
<strong :text="count"></strong>
</div>
app.py — the controller is one mount call plus the wiring:
from pathlib import Path
import situ
from litestar import Litestar
from litestar.plugins.jinja import JinjaTemplateEngine
from litestar.static_files import create_static_files_router
from litestar.template.config import TemplateConfig
from situ import mount_static_component
HERE = Path(__file__).parent
app = Litestar(
route_handlers=[
mount_static_component(
path="/counter",
stem=HERE / "counter", # counter.py + counter.html
template="page.html", # situ ships a minimal default
meta={"name": "Counter"},
),
# serve the runtime shim the generated island loads from /static/_rt.js
create_static_files_router(path="/static", directories=[situ.static_dir()]),
],
template_config=TemplateConfig(
directory=situ.templates_dir(), engine=JinjaTemplateEngine
),
)
litestar --app app:app run # then open http://localhost:8000/counter
Two rules the runtime enforces: every reactive element lives inside <header> or the
single <div data-region> (the two roots where binders are wired), and data-region is
the first attribute on that <div>.
When state needs the database
Declare the site, and the seam follows:
search: Local[str] = "" # live search — compiled to JS, zero network
filter: Url[str] = "all" # a shareable link the server re-renders
issues: Server[IssuesFacade] # the store: a facade in handlers, rows in the template
async def close(id): # `await` makes this a server command:
await issues.set_status(id, "closed") # → POST /cmd/close/{id} → region re-render
A handler containing await becomes a POST command; every other handler compiles to
client JS. The Server components guide
walks the wiring, and the tutorial builds a complete issue
tracker this way in six parts — live search, filters, selection, commands, a create
dialog — with zero hand-written JavaScript and a generated island of ~7 KB.
Or generate the UI from a model: declui
For CRUD-shaped screens, skip the component too. declui turns a typed model into a working form, list, master-detail, or server-backed tracker — at compile time, onto the same seam:
@dataclass
class Sample:
title: str # str → text input
shirt: Annotated[Shirt, Field()] = Shirt.medium # Enum → <select>
price: Annotated[Decimal, Field()] = Decimal("10.50") # Decimal → decimal input
need_by: Annotated[date | None, Field()] = None # date → date picker
app = Litestar(route_handlers=[mount_model(path="/sample", screen=Screen(model=Sample)), ...])
Conditional predicates (editable="rating > 50") compile to client binders; @action
methods become gated command buttons; models may be dataclasses, attrs, msgspec,
Pydantic, or SQLAlchemy. In the capstone example, an 8-line Screen stands in for the
311 lines of hand-written components in the equivalent demo.
declui documentation →
Litestar or Flask
The mount has a portable core (situ.mount.core — dispatch, render, the
command/region/feed/window protocol, no framework imported) with thin adapters over it:
- Litestar (the reference):
mount_component/mount_static_component/mount_tree, with per-request DI via Dishka. - Flask (WSGI):
situ.mount.flask.mount_flaskreturns an ordinaryBlueprint; a plainresolve=lambda: storecallable replaces the DI container, and the request path imports zero Litestar. Seeexamples/flask/.
Documentation
Full documentation lives in docs/ (a Zensical site —
make docs-serve to browse it locally at http://localhost:8000):
- Quickstart — the counter, explained.
- Tutorial — build an issue tracker in six parts; every listing compiles and is browser-verified.
- Concepts — sites & the seam, handlers & commands, the compiled dialect, composition, the wire protocol.
- Cheat sheet — the whole authoring surface on one page; plus the full binder, API, and error references.
- situ vs. … — React, Vue, Svelte, htmx, Datastar, LiveView, Eliom and the research lineage.
- For AI coding assistants:
llms.txt(index) andllms-full.txt— a single-file, self-contained reference with verified samples and the rules an agent must follow.
Demos & examples
Everything documented runs, and everything that runs is browser-verified (the e2e suite fails on any console error):
uv run litestar --app demos.app:app run # 17 demos behind one gallery
uv run uvicorn examples.declui.app:app # the 13-example declui tour
uv run --with flask flask --app examples/flask/app.py run # the Flask adapter
The flagship (/issues, above) is a five-component master-detail tracker whose entire
client is a 117-line generated island; across all thirty shipped apps the islands
measure 6–13 KB over a ~540-line shared shim. Each demo page shows its own source, its
signal→transport table, and the island it serves.
What's in the box
situ.compiler— the Python→JS compiler (parse_front_end,load_front_end,compile_app,splice_tree) and the site markers. Pure standard library.situ.mount— the framework-neutral mount core plus the Litestar route factories and the FlaskBlueprintadapter. Litestar bindings load lazily.situ.declui— the model→UI generator (Field,Screen,zones,action,mount_model).situ_ui— a component kit: 24 components (Dialog, Combobox, DataGrid, Menu, …), aui-*CSS class contract, interactions compiled from Python.situ.siting— theSignal/Signals/Sitecontract;situ.infra— Jinja string rendering and, behind[sqlalchemy], an async engine + Dishka session provider.python -m situ.check— static component-tree resolution for your lint loop.
Status
Alpha. The compiler accepts a bounded dialect of Python and rejects the rest with a clear
error; it does not relocate database I/O to the client. Synced is a reserved site — the
design names it, and the implementation is future work. Dishka is required only by the
Litestar mount_component; the Flask adapter takes a plain callable. The full list of
limits is in Status & roadmap.
The design is research-backed and the record is public: fifteen TodoMVC implementations
and a four-way master-detail comparison in the
webui repository, a per-round design log in
notes/, and a technical report in
docs/papers/TR-001.md.
License
MIT © Stéphane Fermigier
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file situ-0.2.0.tar.gz.
File metadata
- Download URL: situ-0.2.0.tar.gz
- Upload date:
- Size: 140.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4f15b181238cccd498fc87e43333409b6ff830b3bb85f63f97b35469eebf7e0
|
|
| MD5 |
389b896f790b1eaace2261ccfe6c6bbe
|
|
| BLAKE2b-256 |
3e3015ec740224051d2a0ce6331ed2b737ec2b4b404e4b870137e07d6bc64da3
|
File details
Details for the file situ-0.2.0-py3-none-any.whl.
File metadata
- Download URL: situ-0.2.0-py3-none-any.whl
- Upload date:
- Size: 172.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
48ad67c4e8265d5e334c9f569fb445d7aa1ee737502c1384a9d4fdccf23081be
|
|
| MD5 |
6737cebc6c040fdb7385a5f6b1a04140
|
|
| BLAKE2b-256 |
1f7d93402d80a61396847a6d6c714e1d143276a30dcea334b843e486fef9de47
|