Operations#

Build an Engine once with Setup, then render with Page, Fragment and Stream.

class Setup(root: str | Path, signing: SignOptions = ..., watch: bool = False, fragments: list[str] = ..., escape: bool = False, max_template_bytes: int = 1048576)[source]#

Where the templates are and how to render and sign them.

Build one at startup and call run() to get the Engine.

Example

>>> from anza import Setup, Page
>>> engine = Setup(root="templates").run()
>>> Page("/", {"title": "Hello"}).run(engine).html
'<html><body><h1>Hello</h1></body></html>'

Warning

With the default escape=False slot values are inserted without HTML escaping. Turn escape on, or pass values through escape_html(), when they can contain user input.

root: str | Path#

Directory holding the templates (every .html file under it is loaded).

signing: SignOptions#

How fragments are signed (off by default); keys are validated by run().

watch: bool = False#

Reload templates when files under root change (a polling thread; stop it with Engine.close()).

fragments: list[str]#

Fragment allowlist. Empty (the default) lets fragments use any template; otherwise an entry ending in / allows that prefix (e.g. "fragments/") and any other entry allows that exact name.

escape: bool = False#

HTML-escape {{slot}} values; {{{slot}}} stays raw. Off by default: values are inserted as is.

max_template_bytes: int = 1048576#

Refuse template files larger than this (default 1 MiB).

validate() → None[source]#

Check that root exists and is a directory.

Raises:

AnzaError – VALIDATION when it is missing or not a directory.

run() → Engine[source]#

Validate the root and the signing keys, load every template and start the watcher when watch is set.

Returns:

Engine – The engine to render with. Call Engine.close() to stop the watcher.

Raises:

AnzaError – VALIDATION for a bad root; CRYPTO for a missing, short or mismatched signing key (see SignOptions); TEMPLATE for a template that is not UTF-8, is too large or has a malformed placeholder; NOT_FOUND for a template that resolves (through a symlink) outside root.

class Page(route: str, params: Any = None)[source]#

Render the page for a request route into a full HTML document.

/ renders pages/home.html and /a/b (or /a/b/) renders pages/a/b.html. An unknown route falls back to pages/index.html when it exists. The page goes into layout/shell.html as {{content}} (the shell is bound with params plus content), or into a minimal default document with a <dock-main> open shadow root when there is no shell. See Rendering.

Example

>>> from anza import Page, Setup
>>> engine = Setup(root="templates").run()
>>> doc = Page("/", {"title": "Home"}).run(engine)
>>> doc.html
'<html><body><h1>Home</h1></body></html>'
>>> Page("/missing/../route").run(engine)
Traceback (most recent call last):
    ...
anza.errors.AnzaError: [Anza:NOT_FOUND] Page not found
route: str#

The request path, e.g. "/" or "/docs/intro".

params: Any = None#

Values to bind (a dict, (key, value) pairs, or any object whose public attributes are read, such as a dataclass or Pydantic model).

validate() → None[source]#

Check the operation before rendering.

Raises:

AnzaError – VALIDATION when route is empty.

run(engine: Engine) → Document[source]#

Render the page with engine.

Parameters:

engine – The engine from Setup.run().

Returns:

Document – The rendered HTML document.

Raises:

AnzaError – VALIDATION for an empty route; NOT_FOUND (message Page not found) for an unknown or malformed route (.., \, NUL) when there is no pages/index.html.

class Fragment(template: str, slot: str, params: Any = None)[source]#

Render one template into an Envelope for a slot.

The envelope (wire format v2) carries the slot, a Unix timestamp, the rendered HTML and, when the engine signs, a signature over all of them. Serialize it with Envelope.to_json() and send it as a fetch response, an SSE event or a WebSocket message.

Example

>>> from anza import Fragment, Setup, SignOptions
>>> signed = Setup(
...     root="templates",
...     signing=SignOptions(mode="hmac", secret="a-secret-of-at-least-32-bytes!!!"),
... ).run()
>>> env = Fragment("feed/card.html", "feed", {"title": "New"}).run(signed)
>>> env.v, env.slot, env.html
(2, 'feed', '<article>New</article>')
>>> len(env.sig)  # HMAC-SHA256, lowercase hex
64

Warning

Values are inserted without HTML escaping unless the engine was set up with escape=True.

template: str#

Template name relative to the root, e.g. "feed/card.html".

slot: str#

The client slot the HTML is for; it is covered by the signature.

params: Any = None#

The values to bind (see Page.params).

validate() → None[source]#

Check the operation before rendering.

Raises:

AnzaError – VALIDATION when template or slot is empty.

run(engine: Engine) → Envelope[source]#

Render the fragment with engine.

Parameters:

engine – The engine from Setup.run().

Returns:

Envelope – The envelope, signed when the engine has a signing key.

Raises:

AnzaError – VALIDATION for an empty or malformed template name or an empty slot; NOT_FOUND for an unknown template or one outside the fragments allowlist.

class Stream(template: str, slot: str, params: Any = None)[source]#

Render one template as a Server-Sent Event in one call.

Equivalent to format_event(Fragment(template, slot, params).run(engine)), see format_event() and Streaming.

Example

>>> from anza import Setup, Stream
>>> engine = Setup(root="templates").run()
>>> text = Stream("feed/card.html", "feed", {"title": "Push"}).run(engine)
>>> text.startswith('event: template\ndata: {"v":2,"slot":"feed",')
True
>>> text.endswith('\n\n')
True
template: str#

Template name relative to the root, e.g. "feed/card.html".

slot: str#

The client slot the HTML is for.

params: Any = None#

The values to bind (see Page.params).

validate() → None[source]#

Check the operation before rendering.

Raises:

AnzaError – VALIDATION when template or slot is empty.

run(engine: Engine) → str[source]#

Render the fragment and frame it as an SSE event.

Parameters:

engine – The engine from Setup.run().

Returns:

str – "event: template\ndata: <envelope JSON>\n\n".

Raises:

AnzaError – The same errors as Fragment.run().