Skip to content

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.

python
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()

python
ExportRepository.open(
    path: str | os.PathLike[str] | None = None,
    *,
    limits: RepositoryLimits | None = None,
) -> ExportRepository

When path is absent, MARIMO_EXPORT_REPOSITORY takes precedence over the platform cache directory:

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

python
ExportRepository.default_path() -> Path

Repository ownership

ExportRepository is a context manager. close() is idempotent. Operations on a closed repository raise RuntimeError.

High-level producer calls follow one ownership rule:

CallRepository 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 repositoryCaller 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

python
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() -> None

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

FieldDefaultContract
observation_bytes1 MiBMaximum canonical bytes in one observation
observations_per_producer256Retained observations per producer
observation_relation_bytes16 MiBRetained observation bytes across one producer relation
retained_producers32Producer histories retained by observation cleanup
retained_identities128Exact prepared-export identities retained
retained_generations_per_identity4Generations retained for one identity
retained_generations128Generations retained across the repository
retained_prepared_states4096Prepared states retained across producers
metadata_bytes16 MiBRepository metadata budget
prepared_state_bytes512 MiBPer-state maximum and aggregate prepared-state budget
generation_bytes1 GiBPer-generation maximum and aggregate generation budget
repository_bytes2 GiBTotal repository content budget
lease_ttl_seconds30.0Lease expiry after heartbeat loss
lease_heartbeat_seconds5.0Active 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:

python
producer_sha256: str
revision: int
fingerprint: str
values: Mapping[str, JsonValue]
input_names: tuple[str, ...]
canonical_values: bytes
byte_count: int

values 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

python
path: Path
producers: int
observations: int
prepared_states: int
identities: int
generations: int
content_bytes: int
active_leases: int

to_dict() returns the same fields and serializes path as a string.

PruneResult

python
prepared_states: int
generations: int
bytes_released: int
dry_run: bool

to_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

python
from marimo_export.observations import ObservedInputs

observed = ObservedInputs({"interval": "1wk", "region": "EU"})
python
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:

python
observed.fingerprint: str
observed.values: FrozenJsonObject
observed.canonical_values: bytes
observed.byte_count: int

ObservationLedger

python
from marimo_export.observations import ObservationLedger, ObservedInputs

with ObservationLedger("report.py") as ledger:
    ledger.record(ObservedInputs({"interval": "1wk"}))
    ledger.flush()
python
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() -> None

Construction 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

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

ErrorDefault codeMeaning
RepositoryErrorrepository_errorBase repository failure
RepositoryLimitErrorrepository_limit_exceededConfigured storage or record limit exceeded
RepositoryUnavailableErrorrepository_unavailableStorage unavailable
RepositoryBusyErrorrepository_busyLock 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.

Released under the Apache 2.0 License.