Skip to content

Produce an export from Python ​

build() prepares every distinct normalized state in an ExportSpec and writes one verified notebook export. Equal authored rows share one state fingerprint and execute once. Start with portable JSON when the consumer needs structured records, arrays, or metrics:

python
from pathlib import Path

from marimo_export import ExportSpec, OutputSpec, build

spec = ExportSpec(
    default_state="baseline",
    states={
        "baseline": {},
        "weekly": {"interval": "1wk"},
    },
    outputs={"summary": OutputSpec.json("report.summary")},
)

Path("dist").mkdir(exist_ok=True)
result = build("report.py", spec=spec, output="dist/report")
print(result.path)

build() runs the notebook with its current environment, file, credential, and network access. It creates a temporary sibling copy beside the notebook, so the notebook directory must be writable. The output directory's parent must already exist. The authored notebook remains unchanged.

StateSpace ​

python
StateSpace(
    *,
    default_state: str,
    states: Mapping[str, Mapping[str, JsonValue]] | None = None,
    matrix: Mapping[str, list[JsonValue]] | None = None,
)

A StateSpace declares state rows independently of outputs. states maps stable names to sparse input assignments. matrix expands the Cartesian product of nonempty input domains into deterministic matrix-000000 names.

python
from marimo_export import ExportSpec, OutputSpec, StateSpace

state_space = StateSpace.from_file("states.yaml")
spec = ExportSpec.from_state_space(
    state_space,
    outputs={"summary": OutputSpec.json("report.summary")},
)

Methods and properties:

python
StateSpace.from_file(path: str | os.PathLike[str]) -> StateSpace
StateSpace.from_yaml(text: str | bytes, *, source: str = "<memory>") -> StateSpace
StateSpace.from_value(value: object) -> StateSpace
StateSpace.json_schema() -> dict[str, object]
state_space.to_value() -> dict[str, object]
state_space.digest: str
ExportSpec.from_state_space(
    state_space: StateSpace,
    *,
    outputs: Mapping[str, OutputSpec],
) -> ExportSpec

to_value() returns normalized explicit states after matrix expansion. The digest is the SHA-256 identity of that normalized form.

from_file() accepts .json, .yaml, and .yml files up to 16 MiB. JSON and YAML reject duplicate keys. YAML also rejects aliases and merge keys. Both formats enforce 256 container levels and 100,000 composed values. Invalid documents and state-space values fail before planning with SpecError or TypeError according to the rejected field.

ExportSpec ​

python
ExportSpec(
    *,
    default_state: str,
    states: Mapping[str, Mapping[str, JsonValue]],
    outputs: Mapping[str, OutputSpec],
)

An ExportSpec declares the finite state-output relation that consumers can select:

  • default_state names one entry in states.
  • states maps authored aliases to sparse input assignments.
  • outputs maps published names to OutputSpec values.

Planning infers the complete input-name set, fills omitted values from one captured baseline, and deduplicates rows that resolve to the same complete input vector. The immutable states and outputs mappings are sorted during normalization.

Methods:

python
ExportSpec.from_file(path: str | os.PathLike[str]) -> ExportSpec
ExportSpec.from_value(value: object) -> ExportSpec
ExportSpec.json_schema() -> dict[str, object]
spec.to_value() -> dict[str, object]

from_file() reads a UTF-8 .json, .yaml, or .yml file up to 16 MiB. It rejects duplicate keys. YAML aliases and merge keys are invalid. from_value() accepts an existing ExportSpec or validates the exact wire object. json_schema() returns the Draft 2020-12 authoring schema. to_value() returns detached mutable data.

Invalid wire values raise SpecError. Its code identifies the affected part as spec_invalid, spec_value_invalid, spec_output_invalid, or spec_exporter_invalid.

The StateSpace and ExportSpec reference defines state values, matrix expansion, selector syntax, and the YAML and JSON shapes.

OutputSpec ​

Use the factory that matches the stored representation:

python
OutputSpec.json(selector: str) -> OutputSpec
OutputSpec.native(selector: str) -> OutputSpec
OutputSpec.export(selector: str, exporter: ExporterSpec) -> OutputSpec
OutputSpec.output(selector: str) -> OutputSpec
OutputSpec.cell(name: str | None = None, *, id: str | None = None) -> OutputSpec
FactoryStored output
json()Canonical portable JSON selected from a notebook definition
native()marimo cache representation for a scalar, JSON value, NumPy array, Arrow table, or BlobAsset
export()BlobAsset returned by an explicit exporter
output()Formatted marimo.output.v1 snapshot and replay resources
cell()Complete marimo.cell.v1 snapshot selected by authored cell name or inspected runtime ID

cell() requires exactly one of name or id. Selected-value factories parse the selector during construction. Invalid selectors and cell references raise SpecError with a spec_output_invalid code.

OutputSpec.source exposes the normalized source record for inspection. Its concrete source-record classes are not public construction helpers. Construct an output through the five factories and use ExportSpec.to_value() when code needs the portable source shape.

Built-in exporters ​

Typed factories return immutable ExporterSpec values. Install the matching producer extra before preparing the export.

python
from marimo_export.exporters import altair, anywidget, blob, parquet
FactoryDefaultsProducer extra
altair.vegalite()No optionscharts
altair.png(*, scale=1.0)Positive finite scalecharts
anywidget.bundle()No optionsanywidget
parquet.table(*, compression="snappy", filename=None)snappy, none, gzip, brotli, lz4, or zstdparquet
blob.json(*, media_type="application/json", filename=None, metadata=None)Canonical JSON bytesBase package
blob.text(*, media_type="text/plain; charset=utf-8", filename=None, metadata=None)UTF-8 textBase package
blob.html(*, filename=None, metadata=None)UTF-8 HTMLBase package

marimo_export.exporters.parquet.Compression is the type alias for the six accepted Parquet compression strings.

filename must become a portable basename when the exporter returns its BlobAsset. metadata must be a portable JSON object. Missing optional Python distributions raise an output failure with code runtime_distribution_unavailable.

ExporterSpec and importable() ​

python
from marimo_export.exporters import ExporterSpec, importable

exporter = importable(
    "market_summary:encode",
    options={"currency": "USD"},
    dependencies=("market_summary.formatting",),
)
python
ExporterSpec(
    name: str,
    *,
    options: Mapping[str, JsonValue] | None = None,
    dependencies: tuple[str, ...] = (),
)
ExporterSpec.from_value(value: object) -> ExporterSpec
exporter.to_value() -> JsonValue
importable(
    name: str,
    *,
    options: Mapping[str, JsonValue] | None = None,
    dependencies: tuple[str, ...] = (),
) -> ExporterSpec

A custom name uses module:symbol. Option keys must be non-keyword Python identifiers. dependencies contains at most 256 sorted, unique importable module names whose source affects the returned bytes. Custom exporter calls run for every state that needs preparation. A live session keeps already imported modules, so restart it after changing custom exporter source.

BlobAsset ​

Custom exporters return marimo_export.outputs.BlobAsset:

python
from marimo_export.outputs import BlobAsset


def encode(value: str, *, currency: str = "USD") -> BlobAsset:
    return BlobAsset(
        data=value.encode("utf-8"),
        media_type="text/plain; charset=utf-8",
        filename="summary.txt",
        metadata={"schema": "example.summary.v1", "currency": currency},
    )
python
BlobAsset(
    *,
    data: bytes,
    media_type: str | None = None,
    filename: str | None = None,
    metadata: Mapping[str, object] | None = None,
)

data must be bytes. filename must be a portable basename. metadata is copied, normalized, and exposed as recursively immutable portable JSON. Its canonical encoding is limited to 256 KiB. Supply media_type for a value that will enter a notebook export. Export production rejects a BlobAsset whose media type is absent or invalid.

plan() ​

python
plan(
    source: str | os.PathLike[str],
    *,
    spec: ExportSpec,
    repository: ExportRepository | None = None,
    timeout: float = 30.0,
    progress: Callable[[ProgressEvent], None] | None = None,
) -> ExportPlan

plan() returns the exact state-output relation and reports which state fingerprints are reusable or missing. An exact prepared export avoids notebook startup. Otherwise, planning runs the notebook's initial autorun to inspect its baseline and dependencies.

timeout must be a positive finite number. It bounds managed server startup and transport inactivity. A caller-supplied repository stays caller-owned. A repository opened by plan() closes before the call returns.

ExportPlan ​

ExportPlan is an immutable record:

FieldMeaning
identitySHA-256 over producer, output-plan, and spec identities
document_sha256Authored notebook document identity
producer_sha256Notebook plus producer environment identity
output_plan_sha256Identity of the authored output declarations
spec_sha256Identity of the complete authored ExportSpec
default_aliasAuthored default state name
default_fingerprintSHA-256 of the complete input vector selected by the default alias
inputsSorted inferred input names
statesNormalized PlannedState records
outputsOrdered published output names
reusable_statesReusable state fingerprints
missing_statesState fingerprints that need preparation
observation_revisionRepository observation revision used by the plan
observationsObservedState records projected to the plan inputs
exact_reuseWhether one matching prepared export supplied the plan

Each PlannedState has sorted aliases, a complete immutable inputs mapping, and its fingerprint. plan.state_fingerprints returns every normalized fingerprint.

python
plan.matches(notebook_export: NotebookExport) -> bool
plan.to_dict() -> dict[str, object]
export_plan_identity(
    *, producer_sha256: str, output_plan_sha256: str, spec_sha256: str
) -> str
output_plan_sha256(spec: ExportSpec) -> str

matches() compares the spec, default, notebook, input and output names, aliases, fingerprints, and complete input vectors. The identity helpers live in marimo_export.planning.

prepare() ​

python
prepare(
    source: str | os.PathLike[str],
    *,
    spec: ExportSpec,
    repository: ExportRepository | None = None,
    timeout: float = 30.0,
    progress: Callable[[ProgressEvent], None] | None = None,
    cancelled: Callable[[], bool] | None = None,
) -> PreparedExport

prepare() returns a leased immutable export generation. Exact reuse returns before starting a notebook. Missing work starts one owned notebook session, prepares every missing state, commits the complete generation, and closes the owned process tree.

Use the returned handle as a context manager:

python
with prepare("report.py", spec=spec) as prepared:
    notebook_export = prepared.open()
    result = prepared.write("dist/report", replace=True)

When repository is absent, PreparedExport owns the repository until the handle closes. A supplied repository stays caller-owned. cancelled is checked between bounded preparation phases and while waiting for a preparation reservation. Cancellation raises an execution error with code preparation_cancelled and preserves the previous committed generation.

Preparation operations for the same repository identity are serialized across threads and processes. A waiter uses timeout as the reservation-acquisition deadline. When the active operation commits first, the waiter rechecks the repository and can return that exact generation without starting another notebook. Reservation timeout raises the public RepositoryBusyError base with code repository_reservation_timeout.

Each reservation carries a monotonically increasing fencing token. Loss of the reservation removes commit authority. A later state or generation commit then raises the public RepositoryError base with code repository_fence_stale, preserving the current committed generation. The acquisition timeout does not limit the duration of a reservation after it has been acquired.

build() ​

python
build(
    notebook: str | os.PathLike[str],
    *,
    spec: ExportSpec,
    output: str | os.PathLike[str],
    repository: ExportRepository | None = None,
    timeout: float = 30.0,
    replace: bool = False,
    progress: Callable[[ProgressEvent], None] | None = None,
    cancelled: Callable[[], bool] | None = None,
) -> ExportResult

build() preflights output, calls prepare(), writes and verifies the notebook export, then closes the prepared handle. Preflight happens before notebook execution. An existing destination raises NotebookExportError with code destination_exists unless replace=True.

cancelled applies through preparation. Once writing begins, the writer stages, verifies, and commits the destination without another cancellation callback.

Replacement uses a staged sibling directory and rollback. A successful commit replaces the complete destination tree, including permitted root sidecars that exist only in the old directory. Keep application-owned files outside that destination.

The operation can return a warning when parent-directory synchronization or cleanup of the retired destination fails.

PreparedExport ​

prepare(), capture(), and ExportRepository.prepared() return PreparedExport. Callers cannot construct it directly.

Properties:

python
prepared.identity: str
prepared.plan: ExportPlan
prepared.path: Path
prepared.reused: bool
prepared.prepared_states: tuple[str, ...]
prepared.reused_states: tuple[str, ...]
prepared.cache_activity: CacheActivity

identity is the SHA-256 of the export generation's exact canonical index.json. reused means the complete prepared export was reused. reused_states can also contain state-level reuse during a preparation that was not an exact export reuse.

Methods:

python
prepared.open() -> NotebookExport
prepared.asset(relative: str) -> PreparedAsset
prepared.manifest(
    export_url: str,
    *,
    state: str | Mapping[str, object] | None = None,
    refresh_interval_ms: int | None = None,
) -> dict[str, object]
prepared.to_dict() -> dict[str, object]
prepared.write(
    output: str | os.PathLike[str],
    *,
    replace: bool = False,
    progress: Callable[[ProgressEvent], None] | None = None,
) -> ExportResult
prepared.renew() -> None
prepared.close() -> None

open(), path, asset(), renew(), and write() verify the required repository files while the lease is alive. renew() is an immediate lease liveness and index.json integrity check. The repository heartbeat owns catalog expiry renewal. Keep the PreparedExport open while using a NotebookExport returned by open().

PreparedAsset gives file-scoped access through an independently owned lease on the complete export generation. It can outlive its parent PreparedExport, and one open asset keeps that whole generation protected from retention. Close it after the response or file consumer finishes.

The handle exposes path, size, read_bytes(), and idempotent close(). Accessing path verifies the file at that point and returns a filesystem path. The code that opens the path owns any later filesystem race. read_bytes() opens and verifies the returned bytes in one operation.

Calls that need files raise RepositoryError after close or lease loss. Integrity changes raise IntegrityError. close() is idempotent.

Delivery and publications defines prepared manifests and application-level retention.

Narrow protocol records ​

Several focused modules export records used between the high-level producer and its capture protocol:

RecordFieldsReachability from high-level calls
CaptureLimits from marimo_export.limitsmax_asset_bytes, max_closure_bytesHigh-level prepare(), build(), and capture() apply the fixed 64 MiB asset and 512 MiB closure defaults. They do not accept this record as an option
CacheSummary from marimo_export.resulthits, missesValidates cache counts carried by the capture bridge. Public producer results expose the classified CacheActivity record
StateRunTimings from marimo_export.resultstates and setup, dependency execution, UI update, output materialization, and cleanup secondsValidates state-run timing data carried by the capture bridge. It is not returned by the high-level producer result
PhaseTimings from marimo_export.resulttotal, capture, export write, state run, and optional server lifecycle secondsPublic construction record with no high-level producer return path

These records validate nonnegative counts and finite nonnegative durations. CaptureLimits accepts positive safe integers no larger than the fixed producer limits. Use PreparedExport.cache_activity, ExportResult.cache_activity, and ProgressEvent.cache for supported application cache reporting.

Progress callbacks ​

Producer and staged-delivery progress callbacks receive ordered immutable ProgressEvent values synchronously:

text
inspection_started
plan_ready
prepared_reused
state_started
state_finished
prepared_committed
write_finished
delivery_verification_started
delivery_commit_started

Each event contains kind and optional completed, total, state, cache, elapsed_seconds, and message fields. to_dict() includes every field and uses None when a field does not apply.

ProgressKind is the type alias for the nine supported kind strings.

EventFields and timing
inspection_startedEmitted before file inspection and for every live plan or capture. Exact prepared-export reuse omits it.
plan_readycompleted is the reusable-state count and total is the normalized-state count.
prepared_reusedEmitted by prepare() and capture() for exact export reuse, with both counts equal to the normalized-state count.
state_startedstate is the primary alias. completed includes reused and captured states. total counts all unique planned states.
state_finishedAdvances the same complete-plan count and adds this state's cache activity and execution time.
prepared_committedEmitted after the complete export generation commits.
write_finishedEmitted after destination writing and verification, with write duration.
delivery_verification_startedEmitted before a staged application verifies its nested exports and outer tree.
delivery_commit_startedEmitted after staged verification and the commit guard, before final revalidation and destination mutation.

An exact prepare() or capture() reuse path emits plan_ready and prepared_reused before a later write. An exact plan() reuse emits plan_ready. A path with missing work emits state events and prepared_committed.

python
ProgressEvent(
    kind: ProgressKind,
    completed: int | None = None,
    total: int | None = None,
    state: str | None = None,
    cache: CacheActivity | None = None,
    elapsed_seconds: float | None = None,
    message: str | None = None,
)
CacheActivity(
    authored_hits: int = 0,
    authored_misses: int = 0,
    projection_hits: int = 0,
    projection_misses: int = 0,
)

CacheActivity reports effective marimo computation-cache decisions for states that executed during this producer operation:

FieldCount
authored_hitsNon-projection cell attempts restored from cache in executed state children
authored_missesNon-projection cell attempts executed in state children
projection_hitsRequested output receipts restored from cache
projection_missesRequested output receipts produced live, including forced live output work

The counters exclude exact prepared-export reuse and prepared-state reuse. An exact prepared-export reuse returns four zero counters. Zero therefore does not mean that the notebook has no cacheable cells. to_dict() returns the four nonnegative fields. Exceptions raised by the callback propagate to the producer call.

Producer progress callbacks are notifications, not transaction guards. A state_finished event follows the prepared-state commit. A prepared_committed event follows the generation commit. A write_finished event follows destination commit and verification. An exception from one of those callbacks leaves the preceding durable state available even though the producer call raises.

StagedDelivery.commit() emits its two delivery events before destination mutation. A callback exception therefore preserves the previous destination. Use guard for application preconditions such as cancellation or source revision checks. The returned DeliveryResult is the terminal signal after the new application directory becomes visible.

ExportResult and warnings ​

build() and PreparedExport.write() return immutable ExportResult records:

FieldType or meaning
pathAbsolute committed destination
identitySHA-256 of exact canonical index.json bytes
planResolved ExportPlan
reusedExact prepared-export reuse
prepared_statesFingerprints prepared by this operation
reused_statesFingerprints reused by this operation
cache_activityCacheActivity observed for work that ran
assetsUnique asset count
asset_bytesBytes across unique assets
index_bytesCanonical index byte count
verificationVerificationResult for the committed export
warningsRecoverable post-commit ExportWarning values
elapsed_secondsWrite duration

result.to_dict() returns the same nested shape used by CLI JSON output.

An ExportWarning contains code, message, detached mutable portable details, and to_dict(). Current warning codes are export_parent_sync_failed and retired_destination_cleanup_failed. The destination is already visible when either warning is returned.

Use Read and verify exports after writing the notebook export, or Sessions and inspection to prepare from a running session.

Released under the Apache 2.0 License.