an.build.keys#
Cache keys for build stages: canonical digests, the project fallback, keyers.
ADR 0004 decision 2: every key covers everything that changes the output, and
shot.id is never one of them (pillar 11). A shot key is composed from NAMED
parts, each a sha256 hex digest, so a re-render can say which input moved:
The core names no renderer: a backend joins by register_shot_keyer(). A
renderer with no keyer is never cached, which is the safe default — an opaque
shot is re-rendered, never reused on a guess.
>>> canonical_digest({"b": 1, "a": [1, 2]}) == canonical_digest({"a": [1, 2], "b": 1})
True
>>> len(compose_shot_key({"renderer": canonical_digest("cutout")}))
64
Module Attributes
The key's own version — the |
|
the art the compiler reads (characters, environments, props, styles) and the two the audio path reads (voices, sounds). |
|
Files at the project ROOT that every shot depends on, whether or not a mall store exposes them. |
|
an OS's folder metadata must not re-render a film. |
|
a missing texture, an audio ref the store does not hold. |
|
|
|
the renderer's environment record. |
|
one more named digest for a renderer's key — the additive seam for an input read OUTSIDE the compiled document (a genre's side file, a vocabulary entry's version). |
Functions
|
The hex sha256 of |
|
|
|
The hex sha256 of |
|
|
|
The shot key: one digest over the named parts and the key's own version. |
|
Every asset in the project (decision 3's fallback), without the root files — those are |
|
The hex sha256 of a file's bytes, read NOW. |
|
One digest over every asset store of the project (ADR 0004 decision 3). |
|
What every shot depends on project-wide: the project-root files ( |
|
|
|
Add one named input to every key of |
|
Declare how shots of |
The renderer names that have a keyer. |
|
|
The keyer that describes |
|
A digest of one store's whole content. |
Classes
|
What a renderer's keyer returns for one shot. |
Exceptions
A shot keyer or key part was registered twice, or collides with another. |
- an.build.keys.ABSENT: str = 'absent'#
a missing texture, an audio ref the store does not hold. Still deterministic, and different from any present value, so a part that later appears re-renders.
- Type:
A part’s value when the thing it digests is absent
- an.build.keys.EnvironmentProbe#
the renderer’s environment record. Called once per process per renderer (see
ShotCache), because a browser probe costs a launch.- Type:
probe() -> Mapping
- an.build.keys.IGNORED_ASSET_NAME_PREFIXES: tuple[str, ...] = ('.',)#
an OS’s folder metadata must not re-render a film.
- Type:
File names under an asset root that are never assets
- an.build.keys.PROJECT_ASSET_STORES: tuple[str, ...] = ('characters', 'environments', 'props', 'styles', 'voices', 'sounds')#
the art the compiler reads (characters, environments, props, styles) and the two the audio path reads (voices, sounds). What a recording view records (
an.build.reads), and what “every asset in the project” means for decision 3’s fallback. The scene document is deliberately NOT here — each shot’s own slice reaches its key through its compiled document, which is what lets an edit to one shot re-render only that shot.- Type:
The mall’s ASSET stores
- an.build.keys.PROJECT_ROOT_FILES: tuple[str, ...] = ('assets.lock.json',)#
Files at the project ROOT that every shot depends on, whether or not a mall store exposes them.
assets.lock.jsonis the asset library’s lockfile of pinned library versions (ADR 0005, P5): a re-pin changes what is checked out, so it must move every key — and it must do so before (and independently of) its registration in the mall (an#240). Read by path, throughproject_root_files_digest(); an absent file is recorded as absent.
- an.build.keys.SHOT_KEY_IMPL_VERSION: int = 2#
The key’s own version — the
impl_versionsalt ofnw.Transformand ofburns.RESOLVER_IMPL_VERSION(“a lock, not a receipt”). Bump it when the COMPOSITION of a key changes (a part added, renamed or re-spelled). It is NOT how a renderer’s code changes reach the key — a hand-bumped constant is one someone forgets — that is each keyer’scodepart, a digest of the render path’s source. Bumping it orphans every entry; nothing is deleted (decision 6).
- class an.build.keys.ShotKeyInputs(parts, compile_s=None, details=<factory>)[source]#
Bases:
objectWhat a renderer’s keyer returns for one shot.
partsare named sha256 hex digests (the key’s content half); the engine addsrenderer,projectandenvironment.compile_sis the wall time of the compile the keyer ran to get its digest, orNonefor a renderer that has no compile stage.
- an.build.keys.ShotKeyPart#
one more named digest for a renderer’s key — the additive seam for an input read OUTSIDE the compiled document (a genre’s side file, a vocabulary entry’s version). See
register_shot_key_part().- Type:
part(shot, ctx) -> str
- an.build.keys.ShotKeyer#
keyer(shot, ctx) -> ShotKeyInputs. Raises what the render itself would raise for the same inputs (a compile error, an invalid knob), so a bad shot fails before any browser launches.alias of
Callable[[Any,Any],ShotKeyInputs]
- exception an.build.keys.ShotKeyerRegistrationError[source]#
Bases:
ValueErrorA shot keyer or key part was registered twice, or collides with another.
- an.build.keys.bytes_digest(data)[source]#
The hex sha256 of
data.- Return type:
>>> bytes_digest(b"")[:8] 'e3b0c442'
- an.build.keys.callable_identity(obj)[source]#
module.qualnameof a function or class (of an instance: of its type).- Return type:
>>> callable_identity(callable_identity) 'an.build.keys.callable_identity'
- an.build.keys.canonical_digest(obj)[source]#
The hex sha256 of
canonical_json()ofobj.- Return type:
- an.build.keys.canonical_json(obj)[source]#
objas sorted, whitespace-free JSON — the one spelling every digest hashes.- Return type:
>>> canonical_json({"b": (1, 2), "a": None}) '{"a":null,"b":[1,2]}'
- an.build.keys.compose_shot_key(parts)[source]#
The shot key: one digest over the named parts and the key’s own version.
The parts are a MAPPING so the record can keep them by name, and a re-render can be explained (”
texturesmoved”) rather than merely observed.- Return type:
- an.build.keys.every_asset_digest(mall, *, project_root=None)[source]#
Every asset in the project (decision 3’s fallback), without the root files — those are
project_dependencies()’. Theassetspart of a shot whose keyer cannot vouch for its reads.- Return type:
>>> every_asset_digest({"props": {"a": 1}}) != every_asset_digest({"props": {"a": 2}}) True
- an.build.keys.file_digest(path)[source]#
The hex sha256 of a file’s bytes, read NOW.
Deliberately unmemoised. A (path, mtime, size) memo —
an.stage.raster’s, which is fine for a texture alias inside one compile — let a same-size edit whose mtime was restored (cp -p,rsync -t,tar x, a sync client) be served stale from cache in a long-running process (an#243 review, S2). A cache KEY is only as good as its weakest input, so every byte is read on every render; on the golden corpus the whole project digest is under 10 ms.- Return type:
- an.build.keys.project_assets_digest(mall, *, stores=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'), project_root=None, root_files=('assets.lock.json',))[source]#
One digest over every asset store of the project (ADR 0004 decision 3).
The first slice’s dependency edge for every shot: safe — no asset can change without every shot’s key moving — at the price of the per-character saving, which read recording buys back. A store the mall does not have is recorded as absent rather than skipped, so adding one later moves the digest.
With
project_root, the files inroot_files(the library lockfile) are hashed by PATH as well — so a re-pin moves every key whether or not the mall has a store for the file yet.- Return type:
>>> a = project_assets_digest({"characters": {"c": {"v": 1}}}, stores=["characters"]) >>> b = project_assets_digest({"characters": {"c": {"v": 2}}}, stores=["characters"]) >>> a == b False
- an.build.keys.project_dependencies(mall, *, project_root=None)[source]#
What every shot depends on project-wide: the project-root files (
PROJECT_ROOT_FILES, the library lockfile), read by path. A re-pin changes what is checked out, so it moves every key, conservatively.- Return type:
>>> project_dependencies({}) == project_dependencies({"props": {"a": 1}}) True
- an.build.keys.project_root_files_digest(project_root, *, files=('assets.lock.json',))[source]#
{name: sha256 or ABSENT}for the project-root files every shot depends on.>>> import tempfile >>> with tempfile.TemporaryDirectory() as d: ... project_root_files_digest(d, files=["assets.lock.json"]) {'assets.lock.json': 'absent'}
- an.build.keys.register_shot_key_part(renderer_name, part_name, part)[source]#
Add one named input to every key of
renderer_name’s shots — additively.For an input the render reads OUTSIDE its compiled document (whatever changes the document is already covered by the
compiledpart, with early cutoff for free): a genre package’s side file, a vocabulary entry’s version (P7). Refuses a duplicatepart_name; a name that collides with one of the keyer’s own parts is refused when the key is computed.A renderer registered LAZILY (the stage, an#247) brings its keyer when the renderer registry first loads, so this loads it first – a genre adding a key part at install (
cutan, P8) needs no prior lookup.- Return type:
- an.build.keys.register_shot_keyer(renderer_name, keyer, *, environment=None, renderer_type=None, records_reads=False, replace=False)[source]#
Declare how shots of
renderer_nameare keyed, and how its machine is probed.The registration seam for every backend (cut-out here; Manim’s opaque shots, keyed on source hash + Manim version + quality, are the next).
renderer_typebinds the keyer to one renderer class: a renderer whose type is not exactly it is never cached.records_reads=Trueclaims that every asset the shot depends on is read throughctx.mall(or is already digested by one of the keyer’s parts): the engine then keys the shot on the entries it read rather than on the whole project. A second registration for a name is refused unlessreplace=True— a silent replacement would drop the first keyer’s parts from every key without anyone saying so.- Return type:
- an.build.keys.registered_shot_keyers()[source]#
The renderer names that have a keyer.
A keyer registers beside its renderer, and a backend behind the import firewall registers when the renderer registry first loads it (an#247), so this loads the registry first.
>>> "cutout" in registered_shot_keyers() True
- an.build.keys.shot_keyer_for(renderer)[source]#
The keyer that describes
renderer(an instance, or a name), orNone.Nonemeans the shot is never cached: no keyer for the name, or a renderer whose class is not the one the keyer was registered for.- Return type:
_KeyerEntry|None
- an.build.keys.store_digest(store)[source]#
A digest of one store’s whole content.
A filesystem store (one exposing
_root) is digested from its FILES, sidecars included — a character’smeta.jsonis not its art; the SVG parts beside it are. That reads behind the mapping, which is ADR 0004’s gap 3 (“art bypasses the mall”), and it is exactly why this is the fallback: once art is read through the stores (the asset library, ADR 0005), a read-recording view replaces it with the keys a shot actually read. Any other mapping is digested from its items.- Return type:
>>> store_digest({"a": {"x": 1}}) == store_digest({"a": {"x": 1}}) True >>> store_digest({"a": {"x": 1}}) == store_digest({"a": {"x": 2}}) False