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 builtSigner;Nonedisables signing.fragments – The fragment allowlist (see
Setup.fragments).
- Raises:
AnzaError –
CRYPTOwhensigningholds 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 (
repris redacted).
- 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 –
VALIDATIONfor a malformed name (empty, leading/,\, NUL, or an empty,.or..segment);NOT_FOUNDwhen 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
Envelopeor the parsed JSONdict.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 –
CRYPTOwhen signing is off, or when a check fails (the message names it, seeverify_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 asPage(route, params).run(self).See
Pagefor the route rules.- Parameters:
route – The request path, e.g.
"/".params – The values to bind.
- Returns:
Document – The rendered HTML document.
- Raises:
AnzaError –
NOT_FOUNDfor 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 –
VALIDATIONfor a malformed name;NOT_FOUNDfor an unknown template or one outside thefragmentsallowlist.
- 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 theSigner; a bad key fails setup, so a running engine never signs with a missing, short or mismatched key.mode="hmac"needssecret(str or bytes) of at least 32 bytes.mode="ed25519"needsprivate_keyas a 32-byte seed (bytes), 64-char lowercase hex, or acryptographyEd25519PrivateKey. An optionalpublic_key(same formats, or anEd25519PublicKey) 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
reprnever prints key material, so options can be logged.- 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 isEngine.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)
- 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 withEngine.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>'