API reference

The Python surface of spaday. Peer-package component classes are not listed here; see Author a component tree and Generate typed classes.

Authoring

class spaday.Component(*children: Component | dict | str, key: str | None = None, props: dict[str, Any] | None = None, **attrs: Any)[source]

Bases: object

Base for a node in the spaday component tree.

Author it two equivalent ways: nest children positionally in the constructor and set generic props as keywords — App(Nav("title"), Body(...), id="root") — or build it up fluently with .child() / .prop(). A string child becomes a text node. Subclasses set the class attribute tag and forward their typed props via props= (only the ones the author set — None means “leave the element’s own default”).

key(key: str) Component[source]

Set the reconciliation key (for keyed child diffing).

child(*nodes: Component | dict | str) Component[source]

Append one or more children to the default slot (a string child becomes a text node).

child_in(slot: str, node: Component | dict | str) Component[source]

Append a child to a named slot (a string becomes a <span> text node).

text(value: str | Expr) Component[source]

Set the element’s literal or reactive text content (e.g. a button or option label).

Text is set as the textContent DOM property by the runtime, so this is for leaf elements whose label is their text (don’t combine it with child nodes). An expression such as item("name") becomes a computed binding.

prop(name: str, value: Any) Component[source]

Set an arbitrary prop (escape hatch for attributes a typed class doesn’t expose).

style(**decls: Any) Component[source]

Set inline CSS declarations, e.g. .style(padding="1rem", font_size="2rem").

Keys are kebab-cased (font_sizefont-size; a trailing _ is dropped so reserved words work, float_float). Composes with css() and any literal style prop.

css(**variables: Any) Component[source]

Set CSS custom properties — the theming knob, e.g. .css(background_color="navy")--background-color: navy. This is how a web component’s documented –* theme tokens are set from Python (per component), and how the spa-* shell is re-themed at the app level (App().css(spa_surface="#111", spa_border="#333") cascades to the whole shell). WebAwesome’s own custom-property tokens are set the same way. See spaday.theme.

classes(*names: str) Component[source]

Add CSS classes (component variants / theme states), e.g. .classes("wa-dark").

on(event: str, action: Any) Component[source]

Bind a declarative Action to a DOM event (e.g. "click").

The action is serialized as data and interpreted in the browser when the event fires — no round-trip to Python.

bind(prop: str, field: str, *, mode: str = 'one-way') Component[source]

Reactively bind a prop to a state field in the runtime’s signal store.

mode="one-way" keeps the prop in sync with the field; "two-way" also writes the field back when the control changes (for value-like controls). The binding is data interpreted in the browser — the field’s value flows to the prop with no round-trip to Python.

compute(prop: str, expr: Any) Component[source]

Reactively set prop to a value computed from state fields (one-way).

expr is a field expression (field() / eq / not_ / all_ / any_ / lit / item / scope) evaluated in the browser and recomputed whenever any global field or repeater scope it reads changes, e.g. compute("disabled", not_(field("enabled"))).

bind_root_class(name: str, field: str) Component[source]

Toggle a CSS class on the document root (<html>) from a boolean reactive state field.

The escape hatch for page-level theming that lives outside the component tree — most notably WebAwesome’s wa-dark: App(...).bind_root_class("wa-dark", "dark") makes a switch bound to a dark field re-theme the whole page (the rest follows via CSS tokens; canvas widgets that can’t read a class take a .compute("theme", cond(field("dark"), "dark", "light")) instead). One-way (the field drives the class); active only when mounted with a signal Store.

to_node() dict[source]

The node as the core’s JSON-ready dict (empty fields omitted, like the Rust core).

to_json() str[source]

The node serialized for the core’s diff/apply.

spaday.element(tag: str, *children: Component | dict | str, key: str | None = None, **props: Any) Component[source]

Build a plain element (e.g. a div container) for structure a typed component doesn’t cover.

Children nest positionally; a prop name with a trailing underscore is de-escaped so reserved words work (class_class). e.g. element("div", Strong("hi"), id="root", class_="card").

class spaday.components.shell.Each(template: Component, *, field: str | None = None, items: Any | None = None, key: str, scope: str | None = None, **props: Any)[source]

Bases: Component

Render one live component subtree per item in a reactive collection, reusing instances by key.

field reads a global store collection. items accepts an expression, including item() for a nested collection. Inside template, item() reads the current item and scope("name.path") reads a named current or ancestor repeater scope:

Each(Row(Strong().compute("textContent", item("name"))), field="rows", key="id", scope="row")

The first release supports one component template root and read-only item scopes. Item keys must be unique strings or finite numbers. Reordering preserves each live root element and its local state.

Action DSL

Behavior attached to a component with Component.on; see Add behavior and reactivity.

Actions

class spaday.SetProp(target: Ref, prop: str, value: Any)[source]

Bases: Action

Set prop on target to value (an Expr or a plain literal).

class spaday.Toggle(target: Ref, prop: str)[source]

Bases: Action

Flip a boolean prop on target (e.g. hidden, checked, open).

class spaday.Sequence(*actions: Action)[source]

Bases: Action

Run several actions in order.

class spaday.Emit(event: str, detail: Any = None)[source]

Bases: Action

Dispatch a (bubbling) custom DOM event named event with an optional detail expression.

class spaday.SendPatch(model: str, field: str, value: Any)[source]

Bases: Action

Set field to value on a host-routed model (e.g. a transports model).

The runtime surfaces this as a patch intent (a bubbling spaday:patch DOM event carrying {model, field, value}); the app routes it to the actual wire. This is how a control edit is authored declaratively instead of with a hand-written transports listener.

class spaday.If(cond: Any, then: Action, els: Action | None = None)[source]

Bases: Action

Run then if cond is truthy, else els (if given) — branch on live state, e.g. If(prop(by_id("sw"), "checked"), SetProp(...), SetProp(...)).

class spaday.CallEndpoint(method: str, url: str | Expr, body: Any = None, result: str | None = None)[source]

Bases: Action

A REST round-trip: method url with an optional JSON body. url may be a static string or an Expr; body may be an expression or a plain value. The runtime performs the call with fetch.

Pass result (a signal-store field name) to capture the outcome: on completion the runtime writes {"status": <int>, "ok": <bool>, "body": <parsed JSON or text>} to that field, so success/error feedback stays declarative (bind or Show on it):

CallEndpoint("POST", "/api/order", obj({"symbol": field("symbol")}), result="order_result")

Without result the call is fire-and-forget.

class spaday.NamedJs(handler: str)[source]

Bases: Action

The escape hatch: invoke a pre-registered named JS handler (no arbitrary eval). Register it on the JS side with registerHandler(name, fn); use only for the rare irreducible case.

Expressions and references

spaday.lit(value: Any) Expr[source]

A literal value.

spaday.event_value() Expr[source]

The triggering event’s value — a control’s checked (booleans) else value else detail.

spaday.not_(of: Any) Expr[source]

Boolean negation of an expression (or a literal).

spaday.prop(target: Ref, name: str) Expr[source]

The current value of a name prop on target — reads live element state, e.g. prop(by_id("sw"), "checked") for use as a condition.

spaday.field(name: str) Expr[source]

The current value of a reactive state field — for a computed binding (Component.compute), evaluated against the signal store in the browser, e.g. not_(field("enabled")).

spaday.item(path: str = '') Expr[source]

Read path from the innermost Each item.

An empty path returns the complete item. Missing paths evaluate to undefined in the browser.

spaday.scope(reference: str) Expr[source]

Read a named current or ancestor item scope.

scope("staging.channel") reads channel from the nearest scope named staging; scope("staging") returns that scope’s complete item.

spaday.eq(a: Any, b: Any) Expr[source]

True when two expressions are equal, e.g. eq(field("mode"), "advanced").

spaday.all_(*exprs: Any) Expr[source]

True when every expression is truthy (logical AND).

spaday.any_(*exprs: Any) Expr[source]

True when any expression is truthy (logical OR).

spaday.cond(test: Any, then: Any, otherwise: Any) Expr[source]

A ternary for a computed binding (compute()): then when test is truthy, else otherwise (each a plain value or an Expr). Evaluated against the signal store in the browser — e.g. a boolean dark field driving a string theme prop:

chart.compute("theme", cond(field("dark"), "dark", "light"))
spaday.obj(fields: dict[str, Any]) Expr[source]

Compose a JSON object from named sub-expressions (each value a plain value or an Expr). Lets a whole model be POSTed declaratively as a CallEndpoint body — composing live control values without a hand-written handler:

CallEndpoint("POST", "/api/order", obj({
    "symbol": prop(by_id("symbol"), "value"),
    "qty": prop(by_id("qty"), "value"),
}))
spaday.this() Ref[source]

The element the event fired on (the listener’s element).

spaday.by_id(id: str) Ref[source]

The element with this id within the mounted tree.

Binding helper

spaday.bind is a one-way event-driven convenience (control change → set a target prop). For reactive state bindings prefer Component.bind / Component.compute (above).

spaday.bind(source: Any, target: Ref, target_prop: str, *, transform: Any = None) Any[source]

One-way reactive binding: when source (a control component) changes, set target_prop on target (a Ref, e.g. by_id("panel")) to the source’s value — optionally passed through transform (e.g. not_()). Returns source so it composes in a tree:

bind(WaSwitch().text("Show"), by_id("panel"), "hidden", transform=not_)

Event-driven (sugar over SetProp on the source’s change); the signal-graph reactive engine and two-way binding are future work.

Validation

spaday.validate(tree: Component | dict) None[source]

Raise ValidationError if any by_id(...) reference in the tree’s actions is unresolved.

Pass a Component (or its serialized node dict). Returns None on success.

exception spaday.ValidationError[source]

Bases: ValueError

Raised by validate() when a component tree has unresolved references.

CEM binding generator

spaday.parse_cem(manifest)

Parse a custom-elements.json manifest into the JSON-encoded list of component schemas.

spaday.generate(manifest_path: str, out_path: str | None = None, *, source: str | None = None) str[source]

Render a manifest’s components into a Python module; write it to out_path if given.

spaday.classes(manifest_path: str) dict[str, type[Component]][source]
spaday.classes(manifest_path: str, name: str) type[Component]

Build Component subclasses from a manifest at runtime.

The dynamic counterpart to generate(): build classes without emitting a file. With name, returns just that one class (MyClass = spaday.classes(manifest, "MyClass")); otherwise returns {class_name: class} for the whole manifest. Handy for binding an arbitrary or one-off manifest on the fly. Unlike a committed, generated peer-package catalog, these classes are not statically typed — the type checker can’t see their per-attribute signatures — though they still validate keyword names at call time. Reach for generate() (committed codegen) when you want typing.

Serving

Generate a page and deliver it on any backend; see Serve and embed and Sync over transports. The generator is framework-agnostic (spaday.bootstrap); a backend (spaday.backends.<name>starlette, aiohttp, flask, tornado) wires it into routes.

spaday.backends.starlette.serve(page: Page, *, background: Sequence[Awaitable] = (), lifespan: Callable | None = None, **opts) Starlette[source]

Create a Starlette app and mount() page onto it. background coroutines run for the app’s lifetime (or pass a custom lifespan for ordered startup, e.g. a clustering relay); all other keyword options are mount()’s (prefix/routes/html/js/title/packages/ wire/ws/tree/reconnect/scripts/head/store/nonce).

spaday.backends.starlette.mount(app: Starlette, page: Page, *, prefix: str = '', routes: Sequence = (), html: str | Path | None = None, js: str | Path | None = None, layout: AssetLayout | None = None, title: str = 'spaday', packages: PackageRef | Sequence[PackageRef] = (), wire: str | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: str = 'json', reconnect: bool = False, scripts: Sequence[str] = (), head: str = '', store: dict | None = None, nonce: str | None = None) Starlette[source]

Add spaday’s routes (page, tree, /js, plus routes) to an existing Starlette app under prefix. The supplied routes are prefixed too (a Route/WebSocketRoute at /ws becomes {prefix}/ws), so a wired panel’s generated ws URL and its endpoint line up — pass the unprefixed path (WebSocketRoute("/ws", …)) and let mount add the prefix. Generation options pass to spaday.bootstrap.bootstrap() (incl. store and nonce, a CSP nonce for the generated scripts); html serves a hand-authored bootstrap instead; js overrides the bundle dir. mount only adds routes — the host owns the app’s lifespan, so run any transports.autosync in your own lifespan (see examples/embed.py). Returns app for chaining.

spaday.bootstrap.bootstrap(*, base: str = '', packages: ComponentPackage | str | Sequence[ComponentPackage | str] = (), wire: str | Sequence[dict | Wire] | None = None, ws: str = '/ws', tree: str = 'json', reconnect: bool = False, scripts: Sequence[str] = (), head: str = '', title: str = 'spaday', store: dict | None = None, fragment: bool = False, target: str | None = None, nonce: str | None = None, layout: Literal['source', 'installed'] | None = None) str[source]

The bootstrap markup (init the wasm core, fetch the tree, mount it). base prefixes the tree / /js / ws URLs so the page can be mounted under a sub-path. store seeds a local signal Store (reactive UI state for two-way bindings + field actions) even without a wire.

wire="transports" mirrors one model into the store over a websocket; wire=[{"url": …, "namespace": …, "session": …}, …] mirrors several models into one store, each namespaced so their fields don’t collide (a chart on global.* next to one on session.*) — the multi-model page.

By default returns a whole HTML document. With fragment=True it returns just the package tags + the module <script> — a snippet to drop into a host page’s template (Jinja/Django/…), so spaday is one component among many rather than the whole page. Pass target (a CSS selector) to mount into a specific element (e.g. "#widget") instead of document.body; the host provides that element. nonce stamps the generated <script>/<link> tags with a CSP nonce, so a host with a strict script-src/style-src policy can allow the snippet. See the module docstring for the rest of the options and the route contract. packages selects external ComponentPackage descriptors directly, by module:attribute path, or by installed entry-point name. layout selects source-checkout or installed-wheel asset URLs; by default it follows bundles_dir().

class spaday.Wire(url: str, namespace: str | None = None, session: bool = False, flatten: bool = True)[source]

Bases: object

One transports model wire for a multi-model page — a typed, discoverable alternative to a raw dict in serve/bootstrap wire=[…] (both forms are accepted, mix freely):

  • url — the websocket endpoint the model is mirrored over (matches a backend routes= entry).

  • namespace — mirror the model’s fields under <namespace>. so several models share one signal store without colliding (two Chart models on global.* / session.*); omit for bare fields.

  • session — append ?session=<uuid> so the model is a fresh per-page-load tenant (a Hub).

  • flatten — recurse nested sub-models to dotted parent.child fields (the default, what a form binds); set False for an opaque map/dict field (a chart’s time-keyed data, a Perspective layout) so it’s mirrored whole.

Wire("/ws", namespace="global", flatten=False) reads better than {"url": "/ws", …} and gives editor help; it serializes to exactly that dict.

spaday.bootstrap.tree_json(page: Component | object) str[source]

The authored tree as a JSON string (serve at GET {base}/tree.json).

spaday.bootstrap.tree_frame(page: Component | object, *, id: str = 'spa-tree') bytes[source]

The authored tree as a transports Snapshot frame (serve at GET {base}/tree for tree="frame") — the same length-prefixed, codec-tagged envelope transports uses for model state, so the UI tree and the model data ride one wire.

spaday.bootstrap.bundles_dir(layout: Literal['source', 'installed'] | None = None) Path[source]

Directory a backend serves at {base}/js.

Uses the source checkout’s js/ directory when present and otherwise the wheel’s packaged spaday/extension assets. layout can force either form, mainly when serving a custom asset directory with a backend’s js= option.

External component packages

class spaday.ComponentPackage(name: str, assets_dir: Path, assets: Sequence[tuple[str, str]])[source]

Bases: object

Assets that register one external component library in the browser.

assets contains ("css" | "js", relative_path) pairs under assets_dir. Backends serve that directory at {prefix}/components/{name}; spaday.bootstrap.bootstrap() emits the matching tags.

spaday.resolve_component_packages(packages: ComponentPackage | str | Sequence[ComponentPackage | str] = ()) tuple[ComponentPackage, ...][source]

Resolve descriptors, module:attribute paths, or installed entry-point names.

Entry points are loaded only when explicitly named; installing an integration never injects assets into unrelated applications.

spaday.discover_component_packages() tuple[ComponentPackage, ...][source]

Load every installed component-package entry point, sorted by entry-point name.

Server-side rendering

spaday.render_html(tree: Component | dict) str[source]

Render a component (or an already-built node dict) to a light-DOM HTML string for hydration.

Notebook host

Core diff / apply

The low-level component-tree engine (JSON wire form), shared byte-for-byte with the browser runtime. encode_frame / decode_frame wrap a tree (or patch) in a transports Frame so the UI rides the same envelope as model state (used by tree="frame").

spaday.diff(old, new)

Diff two JSON-encoded component trees, returning the JSON-encoded patch.

Thin wrapper over the shared core (spaday::diff_json); the same code runs in the wasm binding.

spaday.apply(root, patch)

Apply a JSON-encoded patch to a JSON-encoded tree, returning the JSON-encoded result.

spaday.encode_frame(payload, model_type, kind, rev, codec)

Frame a JSON-encoded tree/patch into transports’ length-prefixed envelope bytes.

kind is “snapshot” or “patch”; codec is “application/json” or “application/msgpack”.

spaday.decode_frame(frame)

Decode one frame back to a {“model_type”,”kind”,”rev”,”payload”} JSON string.

Theming

The spa-* shell components are re-themed by setting their --spa-* CSS custom properties via Component.css (e.g. App().css(spa_surface="#111", spa_border="#333"), which cascades to the whole shell). spaday.SHELL_TOKENS maps each css() keyword to the CSS custom property it drives and what it controls — spa_surface, spa_surface_2, spa_border, spa_muted, spa_gap, spa_align, spa_justify, spa_gutter_width. Shell elements use neutral defaults unless an application or component package maps its theme tokens onto those variables.