API reference¶
The Python surface of spaday. Peer-package component classes are not listed here; see Author a component tree and Generate typed classes.
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:
ActionSet
propontargettovalue(anExpror a plain literal).
- class spaday.Toggle(target: Ref, prop: str)[source]¶
Bases:
ActionFlip a boolean
propontarget(e.g.hidden,checked,open).
- class spaday.Emit(event: str, detail: Any = None)[source]¶
Bases:
ActionDispatch a (bubbling) custom DOM event named
eventwith an optionaldetailexpression.
- class spaday.SendPatch(model: str, field: str, value: Any)[source]¶
Bases:
ActionSet
fieldtovalueon a host-routedmodel(e.g. a transports model).The runtime surfaces this as a patch intent (a bubbling
spaday:patchDOM 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:
ActionRun
thenifcondis truthy, elseels(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:
ActionA REST round-trip:
methodurlwith an optional JSONbody.urlmay be a static string or anExpr;bodymay be an expression or a plain value. The runtime performs the call withfetch.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 orShowon it):CallEndpoint("POST", "/api/order", obj({"symbol": field("symbol")}), result="order_result")
Without
resultthe call is fire-and-forget.
Expressions and references¶
- spaday.event_value() Expr[source]¶
The triggering event’s value — a control’s
checked(booleans) elsevalueelsedetail.
- spaday.prop(target: Ref, name: str) Expr[source]¶
The current value of a
nameprop ontarget— 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
pathfrom the innermostEachitem.An empty path returns the complete item. Missing paths evaluate to
undefinedin the browser.
- spaday.scope(reference: str) Expr[source]¶
Read a named current or ancestor item scope.
scope("staging.channel")readschannelfrom the nearest scope namedstaging;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.cond(test: Any, then: Any, otherwise: Any) Expr[source]¶
A ternary for a computed binding (
compute()):thenwhentestis truthy, elseotherwise(each a plain value or anExpr). Evaluated against the signal store in the browser — e.g. a booleandarkfield 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 aCallEndpointbody — 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"), }))
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, settarget_propontarget(aRef, e.g.by_id("panel")) to the source’s value — optionally passed throughtransform(e.g.not_()). Returnssourceso it composes in a tree:bind(WaSwitch().text("Show"), by_id("panel"), "hidden", transform=not_)
Event-driven (sugar over
SetPropon the source’schange); the signal-graph reactive engine and two-way binding are future work.
Validation¶
- spaday.validate(tree: Component | dict) None[source]¶
Raise
ValidationErrorif anyby_id(...)reference in the tree’s actions is unresolved.Pass a
Component(or its serialized node dict). ReturnsNoneon success.
- exception spaday.ValidationError[source]¶
Bases:
ValueErrorRaised 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_pathif given.
- spaday.classes(manifest_path: str) dict[str, type[Component]][source]¶
- spaday.classes(manifest_path: str, name: str) type[Component]
Build
Componentsubclasses from a manifest at runtime.The dynamic counterpart to
generate(): build classes without emitting a file. Withname, 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 forgenerate()(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()pageonto it.backgroundcoroutines run for the app’s lifetime (or pass a customlifespanfor ordered startup, e.g. a clustering relay); all other keyword options aremount()’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, plusroutes) to an existing Starletteappunderprefix. The suppliedroutesare prefixed too (aRoute/WebSocketRouteat/wsbecomes{prefix}/ws), so a wired panel’s generated ws URL and its endpoint line up — pass the unprefixed path (WebSocketRoute("/ws", …)) and letmountadd the prefix. Generation options pass tospaday.bootstrap.bootstrap()(incl.storeandnonce, a CSP nonce for the generated scripts);htmlserves a hand-authored bootstrap instead;jsoverrides the bundle dir.mountonly adds routes — the host owns the app’s lifespan, so run anytransports.autosyncin your own lifespan (seeexamples/embed.py). Returnsappfor 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).
baseprefixes the tree //js/ ws URLs so the page can be mounted under a sub-path.storeseeds a local signalStore(reactive UI state for two-way bindings +fieldactions) even without awire.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 onglobal.*next to one onsession.*) — the multi-model page.By default returns a whole HTML document. With
fragment=Trueit 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. Passtarget(a CSS selector) to mount into a specific element (e.g."#widget") instead ofdocument.body; the host provides that element.noncestamps the generated<script>/<link>tags with a CSP nonce, so a host with a strictscript-src/style-srcpolicy can allow the snippet. See the module docstring for the rest of the options and the route contract.packagesselects externalComponentPackagedescriptors directly, bymodule:attributepath, or by installed entry-point name.layoutselects source-checkout or installed-wheel asset URLs; by default it followsbundles_dir().
- class spaday.Wire(url: str, namespace: str | None = None, session: bool = False, flatten: bool = True)[source]¶
Bases:
objectOne transports model wire for a multi-model page — a typed, discoverable alternative to a raw dict in
serve/bootstrapwire=[…](both forms are accepted, mix freely):url— the websocket endpoint the model is mirrored over (matches a backendroutes=entry).namespace— mirror the model’s fields under<namespace>.so several models share one signal store without colliding (twoChartmodels onglobal.*/session.*); omit for bare fields.session— append?session=<uuid>so the model is a fresh per-page-load tenant (aHub).flatten— recurse nested sub-models to dottedparent.childfields (the default, what a form binds); setFalsefor an opaque map/dict field (a chart’s time-keyeddata, a Perspectivelayout) 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}/treefortree="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 packagedspaday/extensionassets.layoutcan force either form, mainly when serving a custom asset directory with a backend’sjs=option.
External component packages¶
- class spaday.ComponentPackage(name: str, assets_dir: Path, assets: Sequence[tuple[str, str]])[source]¶
Bases:
objectAssets that register one external component library in the browser.
assetscontains("css" | "js", relative_path)pairs underassets_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:attributepaths, 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¶
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.