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:
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
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.
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:
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],
) -> ExportSpecto_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
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_statenames one entry instates.statesmaps authored aliases to sparse input assignments.outputsmaps published names toOutputSpecvalues.
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:
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:
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| Factory | Stored 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.
from marimo_export.exporters import altair, anywidget, blob, parquet| Factory | Defaults | Producer extra |
|---|---|---|
altair.vegalite() | No options | charts |
altair.png(*, scale=1.0) | Positive finite scale | charts |
anywidget.bundle() | No options | anywidget |
parquet.table(*, compression="snappy", filename=None) | snappy, none, gzip, brotli, lz4, or zstd | parquet |
blob.json(*, media_type="application/json", filename=None, metadata=None) | Canonical JSON bytes | Base package |
blob.text(*, media_type="text/plain; charset=utf-8", filename=None, metadata=None) | UTF-8 text | Base package |
blob.html(*, filename=None, metadata=None) | UTF-8 HTML | Base 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()
from marimo_export.exporters import ExporterSpec, importable
exporter = importable(
"market_summary:encode",
options={"currency": "USD"},
dependencies=("market_summary.formatting",),
)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, ...] = (),
) -> ExporterSpecA 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:
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},
)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()
plan(
source: str | os.PathLike[str],
*,
spec: ExportSpec,
repository: ExportRepository | None = None,
timeout: float = 30.0,
progress: Callable[[ProgressEvent], None] | None = None,
) -> ExportPlanplan() 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:
| Field | Meaning |
|---|---|
identity | SHA-256 over producer, output-plan, and spec identities |
document_sha256 | Authored notebook document identity |
producer_sha256 | Notebook plus producer environment identity |
output_plan_sha256 | Identity of the authored output declarations |
spec_sha256 | Identity of the complete authored ExportSpec |
default_alias | Authored default state name |
default_fingerprint | SHA-256 of the complete input vector selected by the default alias |
inputs | Sorted inferred input names |
states | Normalized PlannedState records |
outputs | Ordered published output names |
reusable_states | Reusable state fingerprints |
missing_states | State fingerprints that need preparation |
observation_revision | Repository observation revision used by the plan |
observations | ObservedState records projected to the plan inputs |
exact_reuse | Whether 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.
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) -> strmatches() compares the spec, default, notebook, input and output names, aliases, fingerprints, and complete input vectors. The identity helpers live in marimo_export.planning.
prepare()
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,
) -> PreparedExportprepare() 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:
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()
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,
) -> ExportResultbuild() 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:
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: CacheActivityidentity 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:
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() -> Noneopen(), 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:
| Record | Fields | Reachability from high-level calls |
|---|---|---|
CaptureLimits from marimo_export.limits | max_asset_bytes, max_closure_bytes | High-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.result | hits, misses | Validates cache counts carried by the capture bridge. Public producer results expose the classified CacheActivity record |
StateRunTimings from marimo_export.result | states and setup, dependency execution, UI update, output materialization, and cleanup seconds | Validates state-run timing data carried by the capture bridge. It is not returned by the high-level producer result |
PhaseTimings from marimo_export.result | total, capture, export write, state run, and optional server lifecycle seconds | Public 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:
inspection_started
plan_ready
prepared_reused
state_started
state_finished
prepared_committed
write_finished
delivery_verification_started
delivery_commit_startedEach 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.
| Event | Fields and timing |
|---|---|
inspection_started | Emitted before file inspection and for every live plan or capture. Exact prepared-export reuse omits it. |
plan_ready | completed is the reusable-state count and total is the normalized-state count. |
prepared_reused | Emitted by prepare() and capture() for exact export reuse, with both counts equal to the normalized-state count. |
state_started | state is the primary alias. completed includes reused and captured states. total counts all unique planned states. |
state_finished | Advances the same complete-plan count and adds this state's cache activity and execution time. |
prepared_committed | Emitted after the complete export generation commits. |
write_finished | Emitted after destination writing and verification, with write duration. |
delivery_verification_started | Emitted before a staged application verifies its nested exports and outer tree. |
delivery_commit_started | Emitted 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.
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:
| Field | Count |
|---|---|
authored_hits | Non-projection cell attempts restored from cache in executed state children |
authored_misses | Non-projection cell attempts executed in state children |
projection_hits | Requested output receipts restored from cache |
projection_misses | Requested 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:
| Field | Type or meaning |
|---|---|
path | Absolute committed destination |
identity | SHA-256 of exact canonical index.json bytes |
plan | Resolved ExportPlan |
reused | Exact prepared-export reuse |
prepared_states | Fingerprints prepared by this operation |
reused_states | Fingerprints reused by this operation |
cache_activity | CacheActivity observed for work that ran |
assets | Unique asset count |
asset_bytes | Bytes across unique assets |
index_bytes | Canonical index byte count |
verification | VerificationResult for the committed export |
warnings | Recoverable post-commit ExportWarning values |
elapsed_seconds | Write 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.