Engine#

class Engine(root: str | Path, cache: CacheStore, signing: SignOptions | Signer | None = None, fragments: Sequence[str] | None = None)[source]#

Loaded templates plus the validated signing key.

Build it with Setup.run() rather than directly. An engine is safe to share between threads and requests: renders only read the template cache, and a reload (watch=True) swaps the whole cache at once.

Parameters:
  • root – The template root directory.

  • cache – The loaded templates.

  • signing – SignOptions (validated here) or an already built Signer; None disables signing.

  • fragments – The fragment allowlist (see Setup.fragments).

Raises:

AnzaError – CRYPTO when signing holds a bad key.

Example

>>> from anza import Setup
>>> engine = Setup(root="templates").run()
>>> engine.render_fragment("feed/card.html", "feed", {"title": "Hi"}).html
'<article>Hi</article>'
>>> engine.close()
root#

The template root directory.

cache#

The loaded templates, by name.

signer#

The validated signing key (repr is redacted).

fragments: tuple[str, ...]#

Fragment allowlist (see Setup.fragments).

watcher: Any#

The template watcher when Setup.watch is on.

close() → None[source]#

Stop the template watcher, if any.

get_template(name: str) → Template[source]#

Look a template up by name.

Only the templates loaded at setup are searched; the filesystem is never touched.

Parameters:

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

Returns:

Template – The parsed template.

Raises:

AnzaError – VALIDATION for a malformed name (empty, leading /, \, NUL, or an empty, . or .. segment); NOT_FOUND when no such template was loaded.

verify_envelope(envelope: Any, *, max_age_secs: int, slot: str, skew_secs: int = 5, now: int | None = None) → None[source]#

Verify an envelope against this engine’s own key.

Uses the HMAC secret, or the public half of the Ed25519 key, and runs the same checks as verify_envelope().

Parameters:
  • envelope – An Envelope or the parsed JSON dict.

  • max_age_secs – Oldest accepted envelope, in seconds.

  • slot – The slot the envelope must be for.

  • skew_secs – Tolerance for a timestamp ahead of this clock.

  • now – The current Unix time (defaults to time.time()).

Returns:

None – When the envelope is valid.

Raises:

AnzaError – CRYPTO when signing is off, or when a check fails (the message names it, see verify_envelope()).

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": "Hi"}).run(signed)
>>> signed.verify_envelope(env.to_dict(), max_age_secs=60, slot="feed")
>>> signed.verify_envelope(env.to_dict(), max_age_secs=60, slot="menu")
Traceback (most recent call last):
    ...
anza.errors.AnzaError: [Anza:CRYPTO] slot mismatch
render_page(route: str, params: Any = None) → Document[source]#

Render the page for route; same as Page(route, params).run(self).

See Page for the route rules.

Parameters:
  • route – The request path, e.g. "/".

  • params – The values to bind.

Returns:

Document – The rendered HTML document.

Raises:

AnzaError – NOT_FOUND for an unknown or malformed route.

render_fragment(template_name: str, slot: str, params: Any = None) → Envelope[source]#

Render a fragment; same as Fragment(template_name, slot, params).run(self).

Parameters:
  • template_name – Template name relative to the root.

  • slot – The client slot the HTML is for.

  • params – The values to bind.

Returns:

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

Raises:

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

class SignOptions(mode: Literal['none', 'hmac', 'ed25519'] = 'none', secret: str | bytes | None = None, private_key: Any = None, public_key: Any = None)[source]#

How fragments are signed. Signing is off by default.

The keys are only checked when Setup.run() builds the Signer; a bad key fails setup, so a running engine never signs with a missing, short or mismatched key.

  • mode="hmac" needs secret (str or bytes) of at least 32 bytes.

  • mode="ed25519" needs private_key as a 32-byte seed (bytes), 64-char lowercase hex, or a cryptography Ed25519PrivateKey. An optional public_key (same formats, or an Ed25519PublicKey) must match it.

Every signature covers the v2 message from signature_message(); see Signing & verification.

Example

>>> from anza import SignOptions
>>> from anza.crypto import ed25519
>>> seed, public = ed25519.keypair()
>>> SignOptions(mode="ed25519", private_key=seed.hex())
SignOptions(mode='ed25519', private_key=<redacted>)
>>> SignOptions(mode="hmac", secret="a-secret-of-at-least-32-bytes!!!")
SignOptions(mode='hmac', secret=<redacted>)

Note

repr never prints key material, so options can be logged.

mode: Literal['none', 'hmac', 'ed25519'] = 'none'#

The signing algorithm.

secret: str | bytes | None = None#

The HMAC secret (mode="hmac"), at least 32 bytes.

private_key: Any = None#

The Ed25519 private key (mode="ed25519").

public_key: Any = None#

Optional Ed25519 public key; setup fails when it does not match private_key.

SignMode#

How envelopes are signed: "none" (no signature), "hmac" (HMAC-SHA256 with a shared secret) or "ed25519" (Ed25519 with a private key).

alias of Literal[‘none’, ‘hmac’, ‘ed25519’]

class CacheStore(templates: dict[str, Template] | None = None)[source]#

The loaded templates, by name.

Lookups are exact: names are never rewritten, so aliases of one file (a/./b.html, \a\b.html) cannot grow the cache. The engine’s store is Engine.cache.

Parameters:

templates – Initial templates by name (empty when omitted).

Example

>>> from anza import Setup
>>> store = Setup(root="templates").run().cache
>>> sorted(store.keys())
['feed/card.html', 'layout/shell.html', 'pages/home.html']
>>> store.has("pages/home.html"), store.get("pages/nope.html")
(True, None)
templates: dict[str, Template]#

Templates by name. A reload replaces the whole dict.

get(name: str) → Template | None[source]#

The template called name, or None.

insert(name: str, template: Template) → None[source]#

Add or replace the template called name.

remove(name: str) → None[source]#

Drop the template called name, if present.

has(name: str) → bool[source]#

Whether a template called name is loaded.

keys() → list[str][source]#

The names of every loaded template.

manifest() → Manifest[source]#

A Manifest of every template’s SHA-256 digest.

Example

>>> from anza import Setup
>>> m = Setup(root="templates").run().cache.manifest()
>>> m.get("pages/home.html")
'4e1df12fd5e679b7bdaafe259f349f94175b4154900ded81942a83707d2a3405'
class Template(name: str, path: Path, raw: str, digest: str, chunks: list[Chunk], escape: bool = False)[source]#

One parsed template, compiled once for fast binding.

Templates are built by Setup.run() and looked up with Engine.get_template().

Example

>>> from anza import Setup
>>> tpl = Setup(root="templates").run().get_template("feed/card.html")
>>> tpl.name, tpl.raw
('feed/card.html', '<article>{{title}}</article>')
>>> tpl.render({"title": "Hi"})
'<article>Hi</article>'
name: str#

Name relative to the root, /-separated ("feed/card.html").

path: Path#

Path of the file it was loaded from.

raw: str#

The template source.

digest: str#

Lowercase-hex SHA-256 of raw.

chunks: list[Chunk]#

The parsed static text and slots (see extract()).

escape: bool = False#

HTML-escape {{slot}} values (Setup.escape).

bind(params: Any = None) → str[source]#

Bind params into the template; same as render().

Parameters:

params – A dict, (key, value) pairs, or an object whose public, non-callable attributes are read. None renders every slot empty.

Returns:

str – The rendered HTML.

render(params: Any = None) → str[source]#

Bind params into the template (see bind()).