Skip to content

API & CLI

The public surface, as importable today. Signatures are verbatim from the package.

Site & composition markers — situ

Transparent Annotated aliases: the checker sees the payload type; the compiler reads the site from the outer name.

Marker To a type checker To the compiler
Local[T] Url[T] Server[T] Synced[T] T the signal's site
Prop[T] T parent → child input
Emit[T] Callable[[T], None] child → parent event
Provide[T] / Inject[T] T the hierarchical value channel

Mount factories — Litestar (situ)

The reference mount adapter, over the framework-neutral core (situ.mount.core). These names load lazily, so import situ pulls in no Litestar until you call one.

mount_static_component(*, path: str, stem: Path, template: str,
                       meta: Mapping[str, str]) -> Router

Pure client: GET / + GET /island.js. No DI, no commands.

mount_component(*, path: str, stem: Path, template: str, meta: Mapping[str, str],
                service_type: type[ServiceT],
                facade: Callable[..., object] | None = None,
                context: Callable[..., Awaitable[Mapping[str, object]]],
                children: Mapping[str, Path] | None = None,      # deprecated — use components=
                components: Context | None = None,
                overrides: Mapping[str, Path] | None = None,
                unified: bool = False,
                live: bool = False,
                hub: Hub | None = None,                          # your own Hub → publish from outside a request
                fallback: str | None = None,                     # region markup when the page render raises
                window_template: str | None = None,
                guard: Guard | None = None,
                identity: Callable[[Request], str] | None = None,
                extra_routes: list | None = None,
                exception_handlers: ExceptionHandlersMap | None = None) -> Router

The server-backed routes: adds POST /cmd/{name} (+ /{arg:int}), and with live=True (or a hub=) an SSE /feed + /region, with window_template= a /window. service_type is resolved per request from request.state.dishka_container (the Litestar adapter's DI); facade optionally wraps it for the handlers; context builds the template data. guard / identity / extra_routes / exception_handlers are pass-through integration hooks (see mount/factories.py).

Entity mounts. A parametrized path (/things/{id:int}) is an entity mount: one compiled island serves one page per entity, and the path converters merge into the view context() reads (context(service, view) reads view["id"] from the path, stringified, exactly as it reads a query param). The command / island / feed / region URLs bake relative (cmd/…, island.js) so a single island serves every id, resolved against the entity page URL; a converter named name / arg (the command routes' own params) raises at mount, and a bare-URL request (/things/1) 307-redirects to the trailing-slash form so a bookmark still boots a working island. Query-string selection (a Url signal) stays the default; the parametrized path is the opt-in entity form (parity with the Flask adapter).

Fallback (fallback=): markup served in the region when building the page raises — a context() that hit a database timeout, a facade bug. The page, its shell, and its island still ship with that markup in place of the region, and the exception is logged, so one failing region degrades instead of 500ing the whole page. A deliberate HTTPException (a context() raising NotFoundException for a missing entity) passes through as itself, so a real 404 stays a 404. Note that _local seeding comes from context(), so on the fallback path client signals boot from their declared defaults. The markup is trusted and injected unescaped — pass a constant, never user input or an exception's text (the same rule as head).

Live from outside a request (hub=): pass your own Hub() (which implies live) and hold the handle to publish a "peers re-fetch" ping from a background worker / webhook / domain event — hub.publish_threadsafe() hops onto the captured event loop, so a non-loop thread is safe. It is a no-op until a peer subscribes.

mount_tree(*, path: str, root: Path,
           children: Mapping[str, Path] | None = None,
           components: Context | None = None,
           overrides: Mapping[str, Path] | None = None,
           template: str, meta: Mapping[str, str]) -> Router

A pure-client component tree: the root plus resolved children, spliced to one island.

Mount adapter — Flask (situ.mount.flask)

mount_flask(*, path: str, app_factory: Callable[[str], EmittedApp], template: str,
            meta: Mapping[str, str], resolve: Callable[[], object],
            context: Callable[[object, Mapping[str, str]], Awaitable[Mapping[str, object]]],
            facade: Callable[..., object] | None = None,
            guard: Callable[[], Any] | None = None, head: str = "",
            page_render: Callable[[str, Mapping[str, object]], str] | None = None) -> Blueprint

Serves a compiled component on Flask (WSGI): the same four routes as mount_component, over a Flask Blueprint. path may carry Werkzeug converters (/members/<int:oid>) — an entity mount: converter values are merged into the view mapping (stringified, like query params), and the island's command/island URLs are compiled relative (cmd/…, island.js) so one island serves every entity, resolved against the page URL; converter names must not shadow name/arg. app_factory(cmd_base) returns the compiled EmittedApp (the caller compiles, so the baked-in command URLs match the mount); resolve() supplies the domain service (no Dishka); context(service, view) builds the region data. guard is a Flask before_request callable run before every route on the mount (page + island + commands) — return None to allow, a response or abort(...) to deny; this is the seam for per-mount auth / an ACL. head is trusted HTML injected into the page <head> — pass '<link rel="stylesheet" href="/static/declui.css">' for declui's scaffold styling (and serve situ.static_dir() at /static). page_render defaults to situ's bundled Jinja; pass lambda name, ctx: flask.render_template(name, **ctx) to reuse the app's own env + context processors (not bare render_template — the context rides as kwargs). Commands run the async core via asyncio.run. CSRF: every command POST must carry the shim's X-Siting: 1 header (a cross-origin request can't set it without a CORS preflight) — exempt the command routes from form-token CSRF (Flask-WTF: csrf.exempt(bp)) and rely on this + a SameSite cookie. Ships in situ[flask]; importing it pulls in no Litestar. See examples/flask/.

Mount adapter — generic ASGI (situ.mount.asgi)

asgi_app(*, app_factory: Callable[[str], EmittedApp], template: str,
         meta: Mapping[str, str], resolve: Callable[[], object | Awaitable[object]],
         context: Callable[[object, Mapping[str, str]], Awaitable[Mapping[str, object]]],
         facade: Callable[..., object] | None = None, head: str = "",
         page_render: Callable[[str, Mapping[str, object]], str] | None = None,
         fallback: str | None = None) -> ASGIApp

One adapter for every ASGI host — FastAPI, Starlette, Quart, or a bare uvicorn run — built on the ASGI protocol itself, so it imports no web framework (not even Starlette) and adds no dependency. Mount it wherever the host mounts sub-applications: app.mount("/board", asgi_app(...)). resolve() supplies the domain service and may be sync or async (an ASGI host's DI usually is); the other arguments match mount_flask. Because the mount core is already async, an ASGI host awaits it natively — there is no asyncio.run bridge and none of its loop-affinity limits for async database resources. The four routes resolve against the path remaining after the host's mount prefix, and every baked URL is relative (cmd/…, island.js), so the adapter never needs to know its own prefix; a slash-less page request 307-redirects to the trailing-slash form first, since that is what relative URLs resolve against. Entity selection is by query string here — path converters belong to the host router, which an ASGI sub-app does not see. A :virtual component raises at mount (no /window route).

Neutral mount core — situ.mount.core

The portable mechanics both adapters wrap, importing no web framework: dispatch_command(app, *, name, arg, form, facade, make_context) -> CommandResult(html, patch, found), page_data(app, template, ctx, ...) -> (template_name, context_dict), view_from_query(app, query), the compile cache (compile_mount / compile_tree), the wire helpers (parse_id_set / coerce_int), fallback_page(app, template, markup, ...) (the degraded page a mount's fallback= serves — call it from an except block; it logs the active exception), and the live Hub. An adapter reads its framework's request, calls these, and wraps the result in that framework's response.

Component resolution — situ.Context

Context.of(mapping: Mapping[str, Path]) -> Context
Context.from_dir(directory: Path, *, aliases: Mapping[str, str] | None = None) -> Context
ctx.merge(other: Context) -> Context
ctx.with_override(tag: str, stem: Path) -> Context
ctx.get(tag) / ctx.tags()

Immutable and value-hashable (usable as a cache key). from_dir derives PascalTag from snake_case.html filenames; aliases covers the exceptions.

Compiler — situ

load_front_end(stem: Path) -> FrontEnd            # read a .py/.html pair
parse_front_end(template: str, python_src: str) -> FrontEnd
splice_tree(root: FrontEnd, children: dict[str, FrontEnd]) -> FrontEnd
compile_app(front: FrontEnd, cmd_base: str = ...) -> EmittedApp

EmittedApp carries page_template, region_template, island_js, local_init, url_names, set_names, routes, handler_kinds, signal_table, island_loc. CompileError is the one exception type every layer raises.

Paths for wiring: situ.static_dir() (the _rt.js shim), situ.templates_dir() (the default page.html).

Siting contract — situ.siting

Site (str-enum: local / url / server / synced), Signal, Signals (projections: local_init(), url_names(), client_init(), table()), and SIGNALS_HEADER = "X-Siting-Signals".

Kit — situ_ui

situ_ui.STATIC          # Path to ui.css + widgets.js — serve beside situ.static_dir()
situ_ui.component(name: str) -> Path     # one component's stem
situ_ui.kit() -> Context                 # the whole kit as a resolution Context
situ_ui.css_path() -> Path

declui — situ.declui

Field(label=None, label_field=False, hidden=False, secret=False, widget=None,
      control=None, component=None, binds=None,  # widget="custom": inline markup OR a component tag, + the attrs it binds
      search=False, labels=None, badge=False, format=None, source=None,
      required=False, filter=False, link=None, in_=None,
      visible=None, editable=None, valid=None)

mount_model(..., components=None)   # a Context resolving a widget="custom" component= tag

Screen(model: type, zones={}, rules=(), object_editable=None, row_links=())
zones(**named: Sequence[str]) -> Mapping[str, tuple[str, ...]]

Rule(selectors: Mapping[str, Any], properties: Mapping[str, Any])   # the rule cascade
Force(value: str)                                                   # stop a predicate AND-chain
RowLink(label: str, href: str, visible: str | None = None)         # a per-row navigation link
TrackerTheme(...)                                                   # per-slot CSS class overrides

action(func=None, *, label=None, visible=None, message=None, bulk=False)

mount_model(*, path: str, screen: Screen, screens: Sequence[str] = ("form",),
            rows: Sequence[object] = (), choices: Mapping[str, Any] | None = None,
            service_type: type | None = None, facade: Callable[..., Any] | None = None,
            context: ... | None = None, template: str = "page.html",
            meta: Mapping[str, str] | None = None, theme: TrackerTheme | None = None,
            page_size: int | None = None, after: str | None = None,
            components: Context | None = None,          # resolves a widget="custom" component= tag
            new_link: tuple[str, str] | None = None) -> Router  # (label, url): a "New" link in the tracker header

Redirect(url: str)                       # from `situ` — return it from a command to navigate on success
explain(screen: Screen, field: str, operation: str) -> str          # trace a field's rule resolution
clamp_offset(offset, total, page_size) -> int  ·  require_total(ctx) -> ctx
read_model(cls: type) -> list[FieldSpec]
generate_form / generate_list / generate_master_detail / generate_server_tracker / generate_write_form

Infrastructure — situ.infra

templating.render_string (the shared Jinja string renderer); behind the [sqlalchemy] extra, infra.db (an async engine helper) and infra.di (a Dishka session provider).

CLI — situ

Installing situ puts a situ command on your PATH. Each subcommand also runs as python -m situ.<name>.

situ new NAME [--to DIR] [--server] [--force]

Scaffold a component's two sibling files. situ new Counter --to components writes counter.html + counter.py already obeying the rules that fail silently when broken — every reactive element inside the single <div data-region>, and data-region first on that div. --server also declares a Server facade and an async command, so a starting file shows both sides of the seam. --force overwrites an existing pair.

CLI — situ check

situ check ROOT [--from-dir DIR]... [--kit]
python -m situ.check ROOT [--from-dir DIR]... [--kit]

Two static passes over a component tree — no server boot — exiting non-zero and listing every problem it finds:

  1. Tag resolution. Every PascalCase <Tag/> the tree reaches must resolve.
  2. The column contract. Every <th> must honour the DataGrid column data-* contract — the mistakes the kit's runtime cannot catch, because a <th> it never reads is a <th> it cannot reject: a knob on a <th> with no data-field (silently dropped), a typo'd attribute name (data-formatt), an unknown value, or a value on a presence-only knob (data-pin="false" pins the column — the engine tests only for the attribute's presence).

--from-dir adds a directory context (repeatable); --kit includes situ_ui.kit(). Run it in your lint loop.

CLI — situ.declui explain

python -m situ.declui explain MODULE:SCREEN FIELD [OPERATION]

Traces how one field resolves under the rule cascade — the field context, its rank-1 Field, the matching rank-2 rules in rank order, and the effective result. OPERATION defaults to form. Pure over the Screen; no server boot.