an.build#

Incremental re-processing: content-addressed build stages (ADR 0004).

A draft must stay adjustable without re-processing everything: an edit re-renders only what depends on it. The first slice is the shot cache — an render skips any shot whose key (everything its render reads, digested; never shot.id) already has an entry, and reuses that entry’s mp4.

  • an.build.keys — canonical digests, the project-wide fallback dependency, and the registry through which a renderer says what its shot render reads (register_shot_keyer()).

  • an.build.reads — what a shot read: the recording view of the mall a keyer compiles against, and the digests of the entries it saw (an#316).

  • an.build.shot_cache — the incremental= seam (IncrementalEngine), its built-in engine ShotCache, and the entries, shaped as lacing artifacts in a lacing.ArtifactStore.

  • an.build.gc — garbage collection: what the current scene and the recorded renders reach, and deleting the rest (an cache gc, an cache info; an.build.cli).

The core names no renderer; the cut-out keyer lives with the cut-out backend (an.stage.cache_key) and registers on its import.

>>> from an.build import ShotCache, resolve_incremental
>>> isinstance(resolve_incremental(True), ShotCache)
True

Functions

cache_info(project_dir, *[, reachability, ...])

What project_dir's shot cache holds; never fails on reachability (it says why it is unknown instead).

canonical_digest(obj)

The hex sha256 of canonical_json() of obj.

collect_garbage(project_dir, *[, dry_run, ...])

Delete the shot-cache entries of project_dir that nothing reaches.

compose_shot_key(parts)

The shot key: one digest over the named parts and the key's own version.

every_asset_digest(mall, *[, project_root])

Every asset in the project (decision 3's fallback), without the root files — those are project_dependencies()'.

default_environment_digest(renderer_name)

The digest of renderer_name's registered environment probe, once per process.

in_memory_shot_cache_store()

A shot cache held in dicts — for tests, and for a mall with no disk.

project_assets_digest(mall, *[, stores, ...])

One digest over every asset store of the project (ADR 0004 decision 3).

project_dependencies(mall, *[, project_root])

What every shot depends on project-wide: the project-root files (PROJECT_ROOT_FILES, the library lockfile), read by path.

read_digests(mall, reads)

{"store/key": digest} for every recorded read — the shot's dependency edges.

register_shot_keyer(renderer_name, keyer, *)

Declare how shots of renderer_name are keyed, and how its machine is probed.

registered_shot_keyers()

The renderer names that have a keyer.

resolve_incremental(incremental)

incremental= → an engine, or None for "render every shot cold".

shot_artifact_type()

The record type (lacing.Artifact subclass), built on first use.

shot_cache_store(root)

A filesystem shot cache under root: catalog/ + blobs/ (lacing's layout).

shot_keyer_for(renderer)

The keyer that describes renderer (an instance, or a name), or None.

Classes

RecordingMall(mall, *[, recorded])

A view of mall that records which asset entries are read through it.

BuildReport([outcomes, store_bytes, ...])

Every shot's outcome, in timeline order.

IncrementalEngine(*args, **kwargs)

The incremental= seam of an.render.render (ADR 0004 decision 5).

ShotCache([store, environment, ...])

The built-in engine: look a shot's key up in an ArtifactStore; record misses.

ShotKeyInputs(parts[, compile_s, details])

What a renderer's keyer returns for one shot.

ShotOutcome(shot_id, renderer, status[, ...])

What happened to one shot in one render, with its wall times (seconds).

ShotPlan(shot_id, renderer, key[, inputs, ...])

The engine's answer for one shot: its key, and what to reuse if anything.

Exceptions

CacheGcError

The collection cannot be done safely; nothing was deleted.

ShotCacheWarning

The shot cache could not do something it should have; the render still stands.

class an.build.BuildReport(outcomes=<factory>, store_bytes=None, store_entries=None)[source]#

Bases: object

Every shot’s outcome, in timeline order.

store_bytes: int | None = None#

The shot cache’s size after the render, when the engine measured it.

store_line()[source]#

How big the shot cache is after the render, or "" if unmeasured.

Return type:

str

>>> BuildReport(store_bytes=3 * 1024**2, store_entries=4).store_line()
'3.0 MB in 4 entries'
summary()[source]#

One line: what was rendered, what reused, and WHY each rendered shot was not reused — a cache that silently re-renders everything reads as a broken cache (an#243 review, R2-1).

Return type:

str

>>> BuildReport([ShotOutcome("a", "cutout", "uncached", reason=FRAMES_NOT_CACHED)]).summary()
'1 shot(s): 1 rendered (a), 0 reused (-); not reused: frames not cached: film has transitions/sound; pass --cache-frames (a)'
timing_table()[source]#

A Markdown table of the per-shot wall times.

Return type:

str

exception an.build.CacheGcError[source]#

Bases: RuntimeError

The collection cannot be done safely; nothing was deleted.

class an.build.IncrementalEngine(*args, **kwargs)[source]#

Bases: Protocol

The incremental= seam of an.render.render (ADR 0004 decision 5).

begin once per render with the project mall (and its root, for the root files every shot depends on); plan once per shot, BEFORE any shot renders (in the calling thread); record once per shot that was rendered (possibly from a worker thread); finish returns the report.

Two hooks are OPTIONAL (the render loop calls them when an engine has them): record_parts(plan, parts) once per rendered shot whose film window is not whole (an#260), and record_root(output_name, profile=..., output=...) once the film is delivered (what the garbage collector keeps).

class an.build.RecordingMall(mall, *, recorded=('characters', 'environments', 'props', 'styles', 'voices', 'sounds'))[source]#

Bases: Mapping

A view of mall that records which asset entries are read through it.

Stores named in recorded come back wrapped in a RecordingStore (one per store, so identity is stable across lookups); every other store comes back as it is. reads is the set of (store, key) read so far, key being WHOLE_STORE for a store that was listed.

class an.build.ShotCache(store=None, *, environment=<function default_environment_digest>, dependencies=<function project_dependencies>, fallback=<function every_asset_digest>, record_reads=True, cache_frames=False)[source]#

Bases: object

The built-in engine: look a shot’s key up in an ArtifactStore; record misses.

store is the injected lacing.ArtifactStore; None means the mall’s shot_cache (resolved in begin()), and a mall without one renders every shot. environment(renderer_name) -> digest is the environment seam — injectable so a test (or a remote-render backend) can state its machine rather than probe this one. Three dependency seams (see Dependencies):

  • record_reads (on by default) keys a shot whose keyer is registered with records_reads=True on the asset entries its compile read (an.build.reads, an#316: the assets part), so an edit to an asset re-renders only the shots that read it;

  • fallback is the assets part of every OTHER shot — a keyer that cannot vouch for its reads, or every shot under record_reads=False: by default every asset in the project (decision 3’s first slice);

  • dependencies is what EVERY shot depends on (the project part): by default the library lockfile. None drops it, and fallback=None keys an unrecorded shot on its own parts alone — use either knowingly.

An engine built with other seams writes keys an cache gc (which recomputes the current scene’s keys with the defaults) does not reach: collect such a cache with the same engine. cache_frames also stores each shot’s whole PNG sequence when a caller plans with needs_frames=True. The render loop no longer does (an#260): an assembled film takes a shot’s mp4 and, at a transition, its parts (window=), which are cached by default at a few MB. Kept for callers of the engine that need every frame; an cache gc collects old frames.

After a render, report holds what happened to each shot.

entry_ids(shot, renderer, ctx, *, window=None)[source]#

The catalog ids a plan() of this shot would read — computed by the same code, looking nothing up and rendering nothing. What an.build.gc keeps for the current scene. Call begin() first.

Return type:

list[str]

plan(shot, renderer, ctx, *, needs_frames=False, window=None, force=False)[source]#

window is what an assembled film needs from this shot (an.assemble.ShotWindow; None for a plain concat): a shot whose window is not whole is reused only when its parts entry is there too.

Return type:

ShotPlan

record_parts(plan, parts)[source]#

Store a freshly rendered shot’s an.assemble.ShotParts under plan.parts_id: its body mp4 and its window’s PNGs, one stored zip. A failed write never fails the render (the next render renders it).

Return type:

None

record_root(output_name, *, profile, output)[source]#

Record what this render used, as a ROOT for an.build.gc; return its id.

A root is keyed by the project (project_id()), the output name, the render profile (the knobs as passed — None meaning “the scene’s own”) and this machine (machine_id()), so the next render of the same output with the same knobs on the same machine replaces it — a browser upgrade included — while a render on another machine of a synced project, or of another project sharing the store, keeps its own. Its record is a lacing.Artifact of the delivered film, derived from the shot keys. Also measures the store, for the summary.

Return type:

str | None

exception an.build.ShotCacheWarning[source]#

Bases: UserWarning

The shot cache could not do something it should have; the render still stands.

class an.build.ShotKeyInputs(parts, compile_s=None, details=<factory>)[source]#

Bases: object

What a renderer’s keyer returns for one shot.

parts are named sha256 hex digests (the key’s content half); the engine adds renderer, project and environment. compile_s is the wall time of the compile the keyer ran to get its digest, or None for a renderer that has no compile stage.

details: Mapping[str, Any]#

Diagnostics a keyer wants on the record beside the digests — never part of the key (the digests are).

class an.build.ShotOutcome(shot_id, renderer, status, key=None, reason='', key_s=None, compile_s=None, render_s=None, cached_render_s=None)[source]#

Bases: object

What happened to one shot in one render, with its wall times (seconds).

key_s is the whole key computation, of which compile_s is the compile; render_s is this render’s wall time (None when reused) and cached_render_s the wall time of the render being reused.

class an.build.ShotPlan(shot_id, renderer, key, inputs=<factory>, reads=<factory>, code=<factory>, cached=None, reason='', key_s=None, compile_s=None, cached_render_s=None, needs_frames=False, window=None, parts_id=None, parts=None)[source]#

Bases: object

The engine’s answer for one shot: its key, and what to reuse if anything.

code: dict[str, str]#

The render path’s modules, digested ({module: digest}, an#395).

parts: Any = None#

The reused parts (an.assemble.ShotParts), materialised for this plan.

parts_id: str | None = None#

The catalog id of this shot’s parts entry, when its window is not whole.

reads: dict[str, str]#

The asset entries the shot read, digested ({"store/key": digest}).

window: Any = None#

What an assembled film needs from this shot (an.assemble.ShotWindow); None for a film that is a plain concat of shot mp4s.

an.build.cache_info(project_dir, *, reachability=True, engine=None, now=None)[source]#

What project_dir’s shot cache holds; never fails on reachability (it says why it is unknown instead).

Return type:

CacheInfo

an.build.canonical_digest(obj)[source]#

The hex sha256 of canonical_json() of obj.

Return type:

str

an.build.collect_garbage(project_dir, *, dry_run=False, max_size=None, max_age=None, engine=None, now=None, force=False)[source]#

Delete the shot-cache entries of project_dir that nothing reaches.

With no cap, every unreachable entry goes. With caps, unreachable history is kept within them, newest first, and both bind: max_age (seconds) keeps only unreachable entries written more recently than that (and lets a recorded render older than that stop protecting its entries); max_size (bytes) keeps only those that fit in a cache of that size. Neither ever removes a reachable entry, so the cache can stay above max_size. dry_run reports and deletes nothing. force collects a cache no render of this project has recorded a root in (see reachable_entries()).

See the module docstring for the reachability and concurrency argument.

Return type:

GcReport

an.build.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 (”textures moved”) rather than merely observed.

Return type:

str

an.build.default_environment_digest(renderer_name)[source]#

The digest of renderer_name’s registered environment probe, once per process.

Memoised per renderer NAME (each backend has its own machine: Chromium and ffmpeg for cut-out, a Manim install for Manim), and once per process, because the cut-out probe launches a browser and encodes a frame. So a long-lived host does not see a playwright install or a brew upgrade made after its first render: restart it, or pass force_render. A renderer with no probe has the empty environment.

Return type:

str

an.build.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()’. The assets part of a shot whose keyer cannot vouch for its reads.

Return type:

str

>>> every_asset_digest({"props": {"a": 1}}) != every_asset_digest({"props": {"a": 2}})
True
an.build.in_memory_shot_cache_store()[source]#

A shot cache held in dicts — for tests, and for a mall with no disk.

Return type:

Any

an.build.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 in root_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:

str

>>> 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.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:

str

>>> project_dependencies({}) == project_dependencies({"props": {"a": 1}})
True
an.build.read_digests(mall, reads)[source]#

{"store/key": digest} for every recorded read — the shot’s dependency edges.

A read of a store the mall does not have is recorded as ABSENT, so the day the store appears the key moves.

Return type:

dict[str, str]

>>> read_digests({}, [("props", "logo")])
{'props/logo': 'absent'}
an.build.register_shot_keyer(renderer_name, keyer, *, environment=None, renderer_type=None, records_reads=False, replace=False)[source]#

Declare how shots of renderer_name are 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_type binds the keyer to one renderer class: a renderer whose type is not exactly it is never cached. records_reads=True claims that every asset the shot depends on is read through ctx.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 unless replace=True — a silent replacement would drop the first keyer’s parts from every key without anyone saying so.

Return type:

None

an.build.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.

Return type:

tuple[str, ...]

>>> "cutout" in registered_shot_keyers()
True
an.build.resolve_incremental(incremental)[source]#

incremental= → an engine, or None for “render every shot cold”.

True is a fresh ShotCache over the mall’s store; False or None is off; anything else must be an IncrementalEngine.

Return type:

IncrementalEngine | None

>>> resolve_incremental(False) is None
True
>>> isinstance(resolve_incremental(True), ShotCache)
True
an.build.shot_artifact_type()[source]#

The record type (lacing.Artifact subclass), built on first use.

an.build.shot_cache_store(root)[source]#

A filesystem shot cache under root: catalog/ + blobs/ (lacing’s layout).

Return type:

Any

an.build.shot_keyer_for(renderer)[source]#

The keyer that describes renderer (an instance, or a name), or None.

None means 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

Modules

cli

an cache … — the shot cache from the shell (an#274).

derived

A renderer's derived stores, as an cache gc collects them (an#299).

gc

Garbage collection of the shot cache: an cache gc and an cache info (an#274).

keys

Cache keys for build stages: canonical digests, the project fallback, keyers.

reads

What a shot read: a read-recording view of the mall, and the digests of what it saw.

shot_cache

The content-keyed shot cache: ADR 0004's first slice, behind the incremental= seam.