Skip to content

README

liblaf.cherries

Run Python experiments with typed config, path helpers, and plugins.

Cherries exposes a compact facade around a process-global Run. Use main to run an experiment function inside a profile, BaseConfig for typed settings, and helpers such as output to queue artifacts for logging at shutdown.

Modules:

Classes:

  • BaseConfig –

    Base class for experiment configuration models.

  • Run –

    Mutable state for one Cherries experiment run.

  • RunAccessor –

    Read verified assets while holding the source record locally.

Functions:

Attributes:

__commit_id__ module-attribute

__commit_id__: str | None = None

__version__ module-attribute

__version__: str = '4.0.8.dev2+g7918bbd93'

__version_tuple__ module-attribute

__version_tuple__: tuple[int | str, ...] = (
    4,
    0,
    8,
    "dev2",
    "g7918bbd93",
)

run module-attribute

run: Run = Run()

Process-global run used by Cherries convenience functions.

BaseConfig

Bases: BaseSettings


              flowchart TD
              liblaf.cherries.BaseConfig[BaseConfig]

              

              click liblaf.cherries.BaseConfig href "" "liblaf.cherries.BaseConfig"
            

Base class for experiment configuration models.

Subclass BaseConfig when an experiment callable should receive structured settings. main instantiates missing annotated arguments, logs Pydantic models as parameters, and then calls the experiment. The default settings config enables CLI parsing and converts field names to kebab-case flags.

Examples:

>>> class Config(BaseConfig):
...     name: str = "world"
...     epochs: int = 3
>>> Config.model_fields["name"].default
'world'

Attributes:

model_config class-attribute

model_config: SettingsConfigDict = SettingsConfigDict(
    cli_parse_args=True, cli_kebab_case=True
)

Run

Mutable state for one Cherries experiment run.

A Run owns plugin registration, path helpers, metrics, parameters, and miscellaneous metadata. Profiles configure the process-global run, while main starts and ends it around an experiment callable.

Parameters:

  • store_root (Path | None, default: None ) –
  • run_id (str, default: 'f30a73e7-ab30-4645-82d2-6b54ba3ae030' ) –
  • store (Store | None, default: None ) –
  • active (bool, default: False ) –
  • record_result (dict[str, Any] | None, default: None ) –
  • plugins (PluginManager, default: <dynamic> ) –

    Register plugins and delegate hook calls in dependency order.

    Only methods decorated with impl are invoked. Hook order is cached per method and recalculated whenever a plugin is registered.

Methods:

  • abort_start –

    Retain an incomplete stage when source or startup recording fails.

  • end –

    Persist required local evidence; recording failures propagate.

  • get_metric –

    Return one metric series.

  • get_metrics –

    Return selected metric series concatenated into one dataframe.

  • get_other –

    Return one flattened metadata value.

  • get_others –

    Return logged metadata as a nested dictionary.

  • get_param –

    Return one flattened parameter value.

  • get_params –

    Return logged parameters as a nested dictionary.

  • get_step –

    Return the default metric step.

  • input –

    Copy verified input bytes into the active run and record their origin.

  • log_asset –

    Retain an explicit artifact as an independent file inside the active run.

  • log_input –

    Copy and retain an existing input in the active run.

  • log_metric –

    Log one scalar metric.

  • log_metrics –

    Log multiple scalar metrics, flattening nested mappings with /.

  • log_other –

    Log one metadata value.

  • log_others –

    Log multiple metadata values.

  • log_output –

    Copy or register an existing output inside the active run.

  • log_param –

    Log one parameter value.

  • log_params –

    Log multiple parameter values.

  • log_temp –

    Promote a temporary file into retained run artifacts.

  • output –

    Declare a required output in the active run; missing outputs fail saving.

  • set_step –

    Set the default metric step.

  • start –

    Allocate local work and capture evidence before calling user code.

  • summary –

    Build a JSON/YAML-friendly run summary.

  • temp –

    Return a disposable scratch path in the active run.

Attributes:

active class-attribute instance-attribute

active: bool = False

entrypoint cached property

entrypoint: Path

Python entrypoint used to derive the experiment name and folders.

plugins class-attribute instance-attribute

plugins: PluginManager = attrs.field(factory=PluginManager)

project_dir cached property

project_dir: Path

Git repository root, or the current directory outside a Git repo.

project_name cached property

project_name: str

Project name reported to plugins.

record_result class-attribute instance-attribute

record_result: dict[str, Any] | None = None

repo cached property

repo: Repo | None

run_id class-attribute instance-attribute

run_id: str = attrs.field(factory=lambda: str(uuid.uuid4()))

run_key cached property

run_key: Path

run_name cached property

run_name: str

Run name from CHERRIES_NAME or the entrypoint path.

start_time cached property

start_time: datetime

Timezone-aware timestamp captured when the run object is first used.

step property writable

step: int

Default metric step.

store class-attribute instance-attribute

store: Store | None = attrs.field(default=None, repr=False)

store_root class-attribute instance-attribute

store_root: Path | None = None

tags cached property

tags: list[str]

Tags parsed from the CHERRIES_TAGS environment variable.

working_dir cached property

working_dir: Path

Directory used to resolve data, temporary, log, and local snapshot paths.

abort_start

abort_start(error: BaseException) -> None

Retain an incomplete stage when source or startup recording fails.

Source code in src/liblaf/cherries/core/_run.py
def abort_start(self, error: BaseException) -> None:
    """Retain an incomplete stage when source or startup recording fails."""
    self._close_logging()
    if (
        self.store is not None
        and (self.store.root / "pending" / f"{self.run_id}.json").exists()
    ):
        self.store.append_event(
            "recording-incomplete", self.run_id, {"reason": str(error)}
        )
    self._assets.active = False
    self.active = False

end

end(exc: BaseException | None = None) -> None

Persist required local evidence; recording failures propagate.

Source code in src/liblaf/cherries/core/_run.py
def end(self, exc: BaseException | None = None) -> None:
    """Persist required local evidence; recording failures propagate."""
    if not self.active or self.store is None:
        msg = "no active Cherries run"
        raise RuntimeError(msg)
    if isinstance(exc, SystemExit) and exc.code in (None, 0):
        # ``sys.exit(0)`` has the same process outcome as returning from a
        # normal Python experiment.  Seal it before main() re-raises the
        # exception so the interpreter can still exit successfully.
        exc = None
    self.log_other("cherries/end_time", datetime.now().astimezone())
    try:
        self._join_writers()
        if exc is not None:
            diagnostic = "".join(traceback.format_exception(exc))[-65536:]
            self.log_other("cherries/exception", diagnostic)
            self.plugins.delegate("end", exc=exc)
            self._close_logging()
            failure = {
                "exception": diagnostic,
                "execution": "failed",
                "name": self.run_name,
                "source": self._source_evidence,
            }
            if (
                self._settings.get("execution", {}).get(
                    "failure_payload", "discard"
                )
                == "discard"
            ):
                self.store.cancel_failed_work(self.run_id, failure)
            else:
                self.store.append_event("execution-failed", self.run_id, failure)
            return
        self._assets.end()
        end_source = capture_source(
            self.project_dir,
            self.entrypoint,
            self.working_dir / "source-end",
            self._settings,
        )
        stable = self._source_evidence.get("fingerprint") == end_source.get(
            "fingerprint"
        )
        config = self.working_dir / "config"
        config.mkdir(parents=True, exist_ok=True)
        (config / "resolved.json").write_text(
            json.dumps(
                _json_safe(self.get_params()),
                default=str,
                sort_keys=True,
                indent=2,
                allow_nan=False,
            )
            + "\n"
        )
        (config / "bindings.json").write_text(
            json.dumps(self._assets.bindings, default=str, sort_keys=True, indent=2)
            + "\n"
        )
        metrics = self.get_metrics().to_dicts() if self._metrics.metrics else []
        (self.working_dir / "logs/metrics.json").write_text(
            json.dumps(
                _json_safe(metrics),
                default=str,
                sort_keys=True,
                indent=2,
                allow_nan=False,
            )
            + "\n"
        )
        (self.working_dir / "RUN.md").write_text(
            f"# {self.run_name}\n\nRun: `{self.run_id}`\n\nExecution: succeeded. Review: unreviewed.\n\nSource stable across execution: {stable}. Replay has not been verified.\n"
        )
        self.plugins.delegate("end", exc=None)
        self._close_logging()
        self.record_result = self.store.seal(
            self.run_id,
            {
                "kind": "experiment",
                "name": self.run_name,
                "tags": self.tags,
                "command": shlex.join(sys.orig_argv),
                "argv": sys.argv[1:],
                "entrypoint": str(
                    relative_or_absolute(self.entrypoint, self.project_dir)
                ),
                "execution": {"status": "succeeded", "exit_code": 0},
                "validation": {"status": "not_evaluated"},
                "params": _json_safe(self.get_params()),
                "others": json.loads(
                    json.dumps(
                        _json_safe(self.get_others()), default=str, allow_nan=False
                    )
                ),
                "input_bindings": self._assets.bindings,
                "bundles": self._assets.retained_bundles,
                "source": self._source_evidence,
                "source_stability": stable,
                "replay_verified": False,
            },
            self.working_dir,
        )
        logger.info("Saved Cherries run %s", self.run_id)
    except BaseException as failure:
        self.store.append_event(
            "recording-incomplete",
            self.run_id,
            {"reason": str(failure), "work": str(self.working_dir)},
        )
        raise
    finally:
        self._close_logging()
        self._assets.active = False
        self.active = False

get_metric

get_metric(name: str) -> DataFrame

Return one metric series.

Source code in src/liblaf/cherries/core/_run.py
def get_metric(self, name: str) -> pl.DataFrame:
    """Return one metric series."""
    return self._metrics.get_metric(name)

get_metrics

get_metrics(
    metrics: Iterator[str] | None = None,
) -> DataFrame

Return selected metric series concatenated into one dataframe.

Source code in src/liblaf/cherries/core/_run.py
def get_metrics(self, metrics: Iterator[str] | None = None) -> pl.DataFrame:
    """Return selected metric series concatenated into one dataframe."""
    return self._metrics.get_metrics(metrics)

get_other

get_other(name: str) -> Any

Return one flattened metadata value.

Source code in src/liblaf/cherries/core/_run.py
def get_other(self, name: str) -> Any:
    """Return one flattened metadata value."""
    return self._others.get_other(name)

get_others

get_others() -> dict[str, Any]

Return logged metadata as a nested dictionary.

Source code in src/liblaf/cherries/core/_run.py
def get_others(self) -> dict[str, Any]:
    """Return logged metadata as a nested dictionary."""
    return self._others.get_others()

get_param

get_param(name: str) -> Any

Return one flattened parameter value.

Source code in src/liblaf/cherries/core/_run.py
def get_param(self, name: str) -> Any:
    """Return one flattened parameter value."""
    return self._params.get_param(name)

get_params

get_params() -> dict[str, Any]

Return logged parameters as a nested dictionary.

Source code in src/liblaf/cherries/core/_run.py
def get_params(self) -> dict[str, Any]:
    """Return logged parameters as a nested dictionary."""
    return self._params.get_params()

get_step

get_step() -> int

Return the default metric step.

Source code in src/liblaf/cherries/core/_run.py
def get_step(self) -> int:
    """Return the default metric step."""
    return self.step

input

input(
    path: StrPath,
    *,
    name: StrPath | None = None,
    source_run: str | None = None,
    metadata: Mapping[str, Any] | None = None,
) -> Path

Copy verified input bytes into the active run and record their origin.

Source code in src/liblaf/cherries/core/_run.py
def input(
    self,
    path: StrPath,
    *,
    name: StrPath | None = None,
    source_run: str | None = None,
    metadata: Mapping[str, Any] | None = None,
) -> Path:
    """Copy verified input bytes into the active run and record their origin."""
    return self._assets.input(
        path, name=name, source_run=source_run, metadata=metadata
    )

log_asset

log_asset(
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path

Retain an explicit artifact as an independent file inside the active run.

Source code in src/liblaf/cherries/core/_run.py
def log_asset(
    self,
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path:
    """Retain an explicit artifact as an independent file inside the active run."""
    return self._assets.log_asset(path, metadata=metadata, name=name)

log_input

log_input(
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path

Copy and retain an existing input in the active run.

Source code in src/liblaf/cherries/core/_run.py
def log_input(
    self,
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path:
    """Copy and retain an existing input in the active run."""
    return self._assets.log_input(path, metadata=metadata, name=name)

log_metric

log_metric(
    name: str,
    value: SupportsFloat,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None

Log one scalar metric.

Source code in src/liblaf/cherries/core/_run.py
def log_metric(
    self,
    name: str,
    value: SupportsFloat,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None:
    """Log one scalar metric."""
    self._metrics.log_metric(name, value, step=step, time=time)

log_metrics

log_metrics(
    metrics: MetricsLike,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None

Log multiple scalar metrics, flattening nested mappings with /.

Source code in src/liblaf/cherries/core/_run.py
def log_metrics(
    self,
    metrics: MetricsLike,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None:
    """Log multiple scalar metrics, flattening nested mappings with `/`."""
    self._metrics.log_metrics(metrics, step=step, time=time)

log_other

log_other(name: str, value: Any) -> None

Log one metadata value.

Source code in src/liblaf/cherries/core/_run.py
def log_other(self, name: str, value: Any) -> None:
    """Log one metadata value."""
    self._others.log_other(name, value)

log_others

log_others(others: Mapping[str, Any]) -> None

Log multiple metadata values.

Source code in src/liblaf/cherries/core/_run.py
def log_others(self, others: Mapping[str, Any]) -> None:
    """Log multiple metadata values."""
    self._others.log_others(others)

log_output

log_output(
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path

Copy or register an existing output inside the active run.

Source code in src/liblaf/cherries/core/_run.py
def log_output(
    self,
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path:
    """Copy or register an existing output inside the active run."""
    return self._assets.log_output(path, metadata=metadata, name=name)

log_param

log_param(name: str, value: Any) -> None

Log one parameter value.

Source code in src/liblaf/cherries/core/_run.py
def log_param(self, name: str, value: Any) -> None:
    """Log one parameter value."""
    self._params.log_param(name, value)

log_params

log_params(params: Mapping[str, Any]) -> None

Log multiple parameter values.

Source code in src/liblaf/cherries/core/_run.py
def log_params(self, params: Mapping[str, Any]) -> None:
    """Log multiple parameter values."""
    self._params.log_params(params)

log_temp

log_temp(
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path

Promote a temporary file into retained run artifacts.

Source code in src/liblaf/cherries/core/_run.py
def log_temp(
    self,
    path: StrPath,
    metadata: Mapping[str, Any] | None = None,
    *,
    name: StrPath | None = None,
) -> Path:
    """Promote a temporary file into retained run artifacts."""
    return self._assets.log_temp(path, metadata=metadata, name=name)

output

output(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path

Declare a required output in the active run; missing outputs fail saving.

Source code in src/liblaf/cherries/core/_run.py
def output(
    self,
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path:
    """Declare a required output in the active run; missing outputs fail saving."""
    return self._assets.output(path, metadata=metadata, mkdir=mkdir)

set_step

set_step(step: int) -> None

Set the default metric step.

Source code in src/liblaf/cherries/core/_run.py
def set_step(self, step: int) -> None:
    """Set the default metric step."""
    self.step = step

start

start() -> None

Allocate local work and capture evidence before calling user code.

Source code in src/liblaf/cherries/core/_run.py
def start(self) -> None:
    """Allocate local work and capture evidence before calling user code."""
    if self.active:
        msg = "a Cherries run is already active"
        raise RuntimeError(msg)
    self.run_id = str(uuid.uuid4())
    self.start_time = datetime.now().astimezone()
    self._settings = load_settings(self.project_dir)
    self.store = Store(self.store_root or storage_root(self.project_dir))
    self.store.ensure_initialized(self._settings.get("collection", {}).get("id"))
    self.working_dir = self.store.start_work(
        self.run_id,
        {
            "pid": os.getpid(),
            "name": self.run_name,
            "machine_id": self.store.machine_id,
        },
    )
    parent = os.environ.get("CHERRIES_PARENT_RUN")
    if parent:
        self.store.register_parent(self.run_id, self.store.resolve_id(parent))
    self._assets = AssetsManager(
        working_dir=self.working_dir,
        plugins=cast("AssetPluginProtocol", self.plugins),
        active=True,
        store=self.store,
        run_id=self.run_id,
    )
    self._metrics = self._default_metrics()
    self._params = self._default_params()
    self._others = self._default_others()
    self.record_result = None
    self.active = True
    logs = self.working_dir / "logs"
    logs.mkdir(parents=True)
    handler = logging.FileHandler(logs / "run.log", encoding="utf-8")
    handler.setFormatter(
        logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
    )
    logging.getLogger().addHandler(handler)
    self._file_handler = handler
    self._source_evidence = capture_source(
        self.project_dir,
        self.entrypoint,
        self.working_dir / "source",
        self._settings,
    )
    capture_environment(self.project_dir, self.working_dir / "environment")
    self.plugins.delegate("start")
    self._threads = tuple(threading.enumerate())
    self._child_pids = self._children()
    self.log_other("cherries/cmd", shlex.join(sys.orig_argv))
    self.log_other(
        "cherries/entrypoint",
        relative_or_absolute(self.entrypoint, self.project_dir),
    )
    self.log_other("cherries/exp_dir", self.working_dir)
    self.log_other("cherries/start_time", self.start_time)
    self.log_other("cherries/run_id", self.run_id)

summary

summary(prefix: StrPath | None = None) -> dict[str, Any]

Build a JSON/YAML-friendly run summary.

Parameters:

  • prefix (StrPath | None, default: None ) –

    Optional directory to strip from artifact paths.

Returns:

  • dict[str, Any] –

    Run metadata, parameters, artifact paths, and user metadata.

Source code in src/liblaf/cherries/core/_run.py
def summary(self, prefix: StrPath | None = None) -> dict[str, Any]:
    """Build a JSON/YAML-friendly run summary.

    Args:
        prefix: Optional directory to strip from artifact paths.

    Returns:
        Run metadata, parameters, artifact paths, and user metadata.
    """
    summary: dict[str, Any] = {"name": self.run_name}
    if self.tags:
        summary["tags"] = self.tags
    others: dict[str, Any] = self.get_others()
    summary.update(others.pop("cherries"))
    summary["params"] = self.get_params()
    summary.update(self._assets.summary.to_dict(prefix=prefix))
    summary["others"] = others
    return summary

temp

temp(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path

Return a disposable scratch path in the active run.

Source code in src/liblaf/cherries/core/_run.py
def temp(
    self,
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path:
    """Return a disposable scratch path in the active run."""
    return self._assets.temp(path, metadata=metadata, mkdir=mkdir)

RunAccessor

RunAccessor(
    run_id: str,
    *,
    workspace: Path | None = None,
    store: Store | None = None,
)

Read verified assets while holding the source record locally.

Methods:

Attributes:

Source code in src/liblaf/cherries/_access.py
def __init__(
    self, run_id: str, *, workspace: Path | None = None, store: Store | None = None
) -> None:
    self.store = store or Store(storage_root())
    self.store.ensure_initialized()
    self.remote = configured_remote(Path.cwd())
    try:
        self.run_id = self.store.resolve_id(run_id)
    except NotFoundError:
        if self.remote is None:
            raise
        self.remote.import_metadata(self.store)
        self.run_id = self.store.resolve_id(run_id)
    self.reason = "read:" + str(uuid.uuid4())
    self.closed = False
    self.workspace = workspace
    self.store.hold(self.run_id, self.reason)
    try:
        if workspace is not None:
            self._bind_workspace(Path(workspace))
    except BaseException:
        self.store.release_hold(self.run_id, self.reason)
        raise

closed instance-attribute

closed = False

reason instance-attribute

reason = 'read:' + str(uuid.uuid4())

record property

record: dict[str, Any]

remote instance-attribute

remote = configured_remote(Path.cwd())

run_id instance-attribute

run_id = self.store.resolve_id(run_id)

store instance-attribute

store = store or Store(storage_root())

workspace instance-attribute

workspace = workspace

__enter__

__enter__() -> Self
Source code in src/liblaf/cherries/_access.py
def __enter__(self) -> Self:
    return self

__exit__

__exit__(*_args) -> None
Source code in src/liblaf/cherries/_access.py
def __exit__(self, *_args) -> None:
    self.close()

close

close() -> None
Source code in src/liblaf/cherries/_access.py
def close(self) -> None:
    if not self.closed:
        self.store.release_hold(self.run_id, self.reason)
    self.closed = True

path

path(relative: str) -> Path
Source code in src/liblaf/cherries/_access.py
def path(self, relative: str) -> Path:
    if self.closed:
        msg = "record accessor is closed"
        raise RuntimeError(msg)
    if not relative or Path(relative).is_absolute() or ".." in Path(relative).parts:
        msg = "asset path must be relative and contained"
        raise ValueError(msg)
    files = self.store.read_manifest(self.run_id)["files"]
    selected = [
        entry
        for entry in files
        if entry["path"] == relative
        or entry["path"].startswith(relative.rstrip("/") + "/")
    ]
    bundle_path = self._bundle_path(relative)
    if bundle_path is not None:
        return bundle_path
    if not selected:
        raise FileNotFoundError(relative)
    from .core.assets.bundle import bundles

    for entry in selected:
        local = self._materialize(entry["path"])
        for companion_name, optional in bundles.ls_files(local):
            companion = Path(companion_name)
            if not companion.resolve().is_relative_to(local.parent.resolve()):
                msg = "asset companion escapes its bundle"
                raise ValueError(msg)
            logical = companion.relative_to(
                self.store.root / "runs" / self.run_id
            ).as_posix()
            try:
                self._materialize(logical)
            except (FileNotFoundError, NotFoundError):
                if not optional:
                    raise
    return self.store.root / "runs" / self.run_id / relative

end

end() -> None

End the process-global run.

This is useful for scripts that call start manually instead of using main.

Source code in src/liblaf/cherries/_main/_end.py
def end() -> None:
    """End the process-global run.

    This is useful for scripts that call [`start`][liblaf.cherries.start]
    manually instead of using [`main`][liblaf.cherries.main].
    """
    core.run.end()

get_metric

get_metric(name: str) -> DataFrame

get_metrics

get_metrics(
    metrics: Iterator[str] | None = None,
) -> DataFrame

get_other

get_other(name: str) -> Any

get_others

get_others() -> dict[str, Any]

get_param

get_param(name: str) -> Any

get_params

get_params() -> dict[str, Any]

get_step

get_step() -> int

input

input(
    path: StrPath,
    *,
    name: StrPath | None = None,
    source_run: str | None = None,
    metadata: Mapping[str, Any] | None = None,
) -> Path

log_asset

log_asset(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    name: StrPath | None = None,
) -> Path

log_input

log_input(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    name: StrPath | None = None,
) -> Path

log_metric

log_metric(
    name: str,
    value: SupportsFloat,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None

log_metrics

log_metrics(
    metrics: MetricsLike,
    *,
    step: int | None = None,
    time: datetime | None = None,
) -> None

log_other

log_other(name: str, value: Any) -> None

log_others

log_others(others: Mapping[str, Any]) -> None

log_output

log_output(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    name: StrPath | None = None,
) -> Path

log_param

log_param(name: str, value: Any) -> None

log_params

log_params(params: Mapping[str, Any]) -> None

log_temp

log_temp(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    name: StrPath | None = None,
) -> Path

main

main[T](
    main: Callable[..., Awaitable[T]],
    *,
    profile: ProfileLike | None = None,
) -> T
main[T](
    main: Callable[..., T],
    *,
    profile: ProfileLike | None = None,
) -> T

Run an experiment callable inside a Cherries profile.

Missing positional and keyword arguments are built from their annotations when possible. Pydantic models are logged as parameters before the callable runs. Coroutine results are awaited with asyncio.run().

Parameters:

  • main (Callable[..., Any]) –

    Experiment callable.

  • profile (ProfileLike | None, default: None ) –

    Profile name, profile instance, or profile class.

Returns:

  • Any –

    The callable result. If the callable returns a coroutine, Cherries waits

  • Any –

    for it with asyncio.run() and returns the awaited value.

Raises:

  • BaseException –

    Re-raises any exception from the experiment after ending the run with the captured exception.

Examples:

Use a typed config object and a queued output path in an experiment:

from pathlib import Path

from liblaf import cherries


class Config(cherries.BaseConfig):
    name: str = "world"


def experiment(cfg: Config) -> None:
    output = cherries.output("hello.txt")
    output.write_text(f"Hello, {cfg.name}!\\n")
    cherries.log_metric("message_length", len(cfg.name))


cherries.main(experiment, profile="debug")
Source code in src/liblaf/cherries/_main/_main.py
def main[T](
    main: Callable[..., Any],
    *,
    profile: profiles.ProfileLike | None = None,
) -> Any:
    r"""Run an experiment callable inside a Cherries profile.

    Missing positional and keyword arguments are built from their annotations
    when possible. Pydantic models are logged as parameters before the callable
    runs. Coroutine results are awaited with `asyncio.run()`.

    Args:
        main: Experiment callable.
        profile: Profile name, profile instance, or profile class.

    Returns:
        The callable result. If the callable returns a coroutine, Cherries waits
        for it with `asyncio.run()` and returns the awaited value.

    Raises:
        BaseException: Re-raises any exception from the experiment after ending
            the run with the captured exception.

    Examples:
        Use a typed config object and a queued output path in an experiment:

        ```python
        from pathlib import Path

        from liblaf import cherries


        class Config(cherries.BaseConfig):
            name: str = "world"


        def experiment(cfg: Config) -> None:
            output = cherries.output("hello.txt")
            output.write_text(f"Hello, {cfg.name}!\\n")
            cherries.log_metric("message_length", len(cfg.name))


        cherries.main(experiment, profile="debug")
        ```
    """
    # CLI help and invalid configuration must exit before creating work or
    # recording a successful SystemExit(0) as an experiment.
    args, kwargs = _make_args(main)
    run: core.Run = start(profile=profile)
    try:
        with _capture_stdio(run):
            configs: list[pydantic.BaseModel] = [
                arg
                for arg in (*args, *kwargs.values())
                if isinstance(arg, pydantic.BaseModel)
            ]
            original_configs = [config.model_dump(mode="json") for config in configs]
            for value in original_configs:
                run.log_params(value)
            result: Any = main(*args, **kwargs)
            if asyncio.iscoroutine(result):
                result = asyncio.run(result)
            for config, original in zip(configs, original_configs, strict=True):
                resolved = config.model_dump(mode="json")
                if resolved != original:
                    run.log_params(resolved)
    except BaseException as exc:
        run.end(exc=exc)
        raise
    else:
        run.end()
        return result

open_run

open_run(
    run_id: str,
    *,
    workspace: Path | None = None,
    store: Store | None = None,
) -> RunAccessor
Source code in src/liblaf/cherries/_access.py
def open_run(
    run_id: str, *, workspace: Path | None = None, store: Store | None = None
) -> RunAccessor:
    return RunAccessor(run_id, workspace=workspace, store=store)

output

output(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path

set_step

set_step(step: int) -> None

start

start(profile: ProfileLike | None = None) -> Run

Create, configure, and start a run from profile.

Parameters:

  • profile (ProfileLike | None, default: None ) –

    Profile name, instance, class, or None for environment-based selection.

Returns:

  • Run –

    Started run.

Source code in src/liblaf/cherries/_main/_start.py
def start(profile: ProfileLike | None = None) -> core.Run:
    """Create, configure, and start a run from `profile`.

    Args:
        profile: Profile name, instance, class, or `None` for environment-based
            selection.

    Returns:
        Started run.
    """
    profile: Profile = profiles.factory(profile)
    run: core.Run = profile.init()
    try:
        run.start()
    except BaseException as exc:
        if isinstance(run, core.Run):
            run.abort_start(exc)
        raise
    return run

temp

temp(
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path