Repository and observations
The export repository keeps reusable prepared states, immutable prepared-export generations, and observed input vectors. A notebook export written to a deployment directory is a separate portable artifact.
from marimo_export import ExportRepository
with ExportRepository.open(".exports") as repository:
status = repository.status()
preview = repository.prune(dry_run=True)
print(status.content_bytes)
print(preview.bytes_released)Use one repository across related plans and preparations when those operations should share observations and prepared work.
ExportRepository.open()
ExportRepository.open(
path: str | os.PathLike[str] | None = None,
*,
limits: RepositoryLimits | None = None,
) -> ExportRepositoryWhen path is absent, MARIMO_EXPORT_REPOSITORY takes precedence over the platform cache directory:
| Platform | Default root |
|---|---|
| macOS | ~/Library/Caches/marimo-export/repository |
| Windows | %LOCALAPPDATA%/marimo-export/repository |
| Other platforms | $XDG_CACHE_HOME/marimo-export/repository or ~/.cache/marimo-export/repository |
Opening creates the directory when needed, rejects a symbolic-link root, and sets owner-only permissions on POSIX systems. It attempts maintenance recovery for the private SQLite catalog and invalid repository artifacts. When another process holds the maintenance transaction lock, opening continues without that pass. Recovery can quarantine a corrupt catalog and open a fresh one, which also resets catalog-backed observation history. Recovery never treats the repository as a notebook export directory.
limits defaults to RepositoryLimits(). The policy belongs to the opened handle and is not persisted with the repository path. A later handle can apply different limits. CLI commands open with the default policy.
default_path() returns the selected default path without creating it:
ExportRepository.default_path() -> PathRepository ownership
ExportRepository is a context manager. close() is idempotent. Operations on a closed repository raise RuntimeError.
High-level producer calls follow one ownership rule:
| Call | Repository ownership |
|---|---|
plan(..., repository=None) | Opens and closes a repository during the call |
prepare(..., repository=None) | Returned PreparedExport owns it until close |
capture(..., repository=None) | Returned PreparedExport owns it until close |
| Any call with a supplied repository | Caller keeps ownership |
A PreparedExport returned by repository.prepared(plan) has its own artifact lease. Close that handle before closing or pruning related resources.
Repository operations
repository.record_observation(
plan: ExportPlan,
inputs: Mapping[str, object],
) -> ObservedState
repository.observation_revision(plan: ExportPlan) -> int
repository.observations(plan: ExportPlan) -> tuple[ObservedState, ...]
repository.clear_observations(plan: ExportPlan) -> int
repository.prepared(plan: ExportPlan) -> PreparedExport | None
repository.status() -> RepositoryStatus
repository.prune(*, dry_run: bool = False) -> PruneResult
repository.close() -> Nonerecord_observation() requires exactly the plan's complete input-name set. It canonicalizes the values, advances the producer observation revision, and returns the stored ObservedState.
observations() returns observations stored for the plan's exact ordered input relation. Planning performs the separate projection that can select a subset of values from broader observations. clear_observations() removes the producer's observation history and returns the number removed.
prepared() returns an exact verified prepared export when the repository has one matching producer, output plan, and exact spec identity. It returns None when no exact export generation matches.
status() reports current counts and bytes. prune() applies the configured retention policy and removes candidates when dry_run=False. A dry run reports prepared states, generations, and bytes. A live prune can also remove producer records and their observation history, which PruneResult does not count. Active staging, state, generation, and detached asset leases protect their artifacts from pruning.
RepositoryLimits
RepositoryLimits is an immutable storage and lifecycle policy:
| Field | Default | Contract |
|---|---|---|
observation_bytes | 1 MiB | Maximum canonical bytes in one observation |
observations_per_producer | 256 | Retained observations per producer |
observation_relation_bytes | 16 MiB | Retained observation bytes across one producer relation |
retained_producers | 32 | Producer histories retained by observation cleanup |
retained_identities | 128 | Exact prepared-export identities retained |
retained_generations_per_identity | 4 | Generations retained for one identity |
retained_generations | 128 | Generations retained across the repository |
retained_prepared_states | 4096 | Prepared states retained across producers |
metadata_bytes | 16 MiB | Repository metadata budget |
prepared_state_bytes | 512 MiB | Per-state maximum and aggregate prepared-state budget |
generation_bytes | 1 GiB | Per-generation maximum and aggregate generation budget |
repository_bytes | 2 GiB | Total repository content budget |
lease_ttl_seconds | 30.0 | Lease expiry after heartbeat loss |
lease_heartbeat_seconds | 5.0 | Active lease renewal interval |
Integer limits must be positive and fit SQLite's signed integer range. Lease durations must be positive finite numbers. The heartbeat interval must be shorter than the time to live.
repository_bytes is a steady-state admission budget. Replacing a leased generation can temporarily retain old and new bytes above that value.
Repository result records
ObservedState
One ObservedState contains:
producer_sha256: str
revision: int
fingerprint: str
values: Mapping[str, JsonValue]
input_names: tuple[str, ...]
canonical_values: bytes
byte_count: intvalues is a read-only top-level mapping decoded from canonical bytes. Nested lists and dictionaries are detached mutable values. fingerprint is computed from the complete canonical values. to_dict() returns producer identity, revision, fingerprint, and another detached values object.
RepositoryStatus
path: Path
producers: int
observations: int
prepared_states: int
identities: int
generations: int
content_bytes: int
active_leases: intto_dict() returns the same fields and serializes path as a string.
PruneResult
prepared_states: int
generations: int
bytes_released: int
dry_run: boolto_dict() returns the same fields.
Observation model
An observation is one successful complete input vector retained as authoring evidence. Observations can inform a future ExportPlan, but they enter a notebook export only when an author places the desired values in an explicit ExportSpec state row.
Use record_observation() when an application already has a complete plan and input vector. Use ObservationLedger when a host records successful notebook runs asynchronously.
ObservedInputs
from marimo_export.observations import ObservedInputs
observed = ObservedInputs({"interval": "1wk", "region": "EU"})ObservedInputs(values: Mapping[str, object])Input names must be valid non-keyword Python identifiers. Values must be portable JSON. The record copies and canonicalizes the mapping, then exposes:
observed.fingerprint: str
observed.values: FrozenJsonObject
observed.canonical_values: bytes
observed.byte_count: intObservationLedger
from marimo_export.observations import ObservationLedger, ObservedInputs
with ObservationLedger("report.py") as ledger:
ledger.record(ObservedInputs({"interval": "1wk"}))
ledger.flush()ObservationLedger(
source: str | os.PathLike[str],
*,
repository: ExportRepository | None = None,
)
ledger.record(
observed: ObservedInputs,
*,
producer_sha256: str | None = None,
) -> None
ledger.flush() -> None
ledger.close() -> NoneConstruction validates source and starts one daemon persistence worker. The default repository opens lazily after the first record. A supplied repository stays caller-owned.
record() validates the current source identity when producer_sha256 is absent, queues the observation, and returns before persistence completes. Repeated pending vectors coalesce without losing their occurrence count. The direct queue retains at most 256 observations, 16 MiB, and 32 producers. Each observation is limited to 1 MiB. It can evict older pending vectors while still advancing their observation revisions. Deferred host observations reject new work when the corresponding bounds are full.
flush() waits for queued and deferred writes to settle. After a successful close it returns immediately. close() is idempotent and joins the worker without a separate timeout. Busy repository writes make up to three attempts with 10 and 20 millisecond waits between attempts. Both methods replay a terminal worker failure as ObservationPersistenceError. Recording after close replays a prior failure or raises RuntimeError.
A repository-limit rejection advances the producer revision without retaining the oversized vector. Queue ingestion that exceeds its own bound raises ObservationRejectedError and leaves the worker available for later records.
Attach a ledger to a running kernel
from marimo_export.observations import (
ObservationLedger,
install_observation_ledger,
)
ledger = ObservationLedger("report.py")
release = install_observation_ledger(context, ledger)
try:
...
finally:
release()
ledger.close()install_observation_ledger(context, ledger) attaches a final kernel hook and returns an idempotent release callback. It records runs that complete without an interrupt, exception, cancelled cell, or scratch cell. It also requires the live kernel to remain bound to the ledger's saved notebook source.
This is an advanced host integration. Applications that do not own a marimo kernel context should record through ExportRepository.record_observation() or use an integration that owns the hook lifecycle. See Host integration.
Repository errors
Import public repository failures from marimo_export.repository:
| Error | Default code | Meaning |
|---|---|---|
RepositoryError | repository_error | Base repository failure |
RepositoryLimitError | repository_limit_exceeded | Configured storage or record limit exceeded |
RepositoryUnavailableError | repository_unavailable | Storage unavailable |
RepositoryBusyError | repository_busy | Lock contention exceeded its bounded wait |
Repository errors inherit MarimoExportError, so each exposes code, details, and wire(). A confirmed integrity failure retires the affected artifact. A temporary availability failure preserves the current prepared export.
Use Produce an export to pass the repository into planning and preparation. Use Delivery and publications to retain prepared generations for an application.