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.

Functions:

Attributes:

__commit_id__ module-attribute

__commit_id__: str | None = None

__version__ module-attribute

__version__: str = '3.0.1'

__version_tuple__ module-attribute

__version_tuple__: tuple[int | str, ...] = (3, 0, 1)

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:

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

  • end

    Flush artifacts, record shutdown metadata, and end plugins.

  • 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

    Resolve and immediately log an input below data/.

  • log_asset

    Log an existing generic artifact immediately.

  • log_input

    Log an existing input artifact immediately.

  • 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

    Log an existing output artifact immediately.

  • log_param

    Log one parameter value.

  • log_params

    Log multiple parameter values.

  • log_temp

    Log an existing temporary artifact immediately.

  • output

    Resolve an output below data/ and queue it until run end.

  • set_step

    Set the default metric step.

  • start

    Start plugins and record Cherries run metadata.

  • summary

    Build a JSON/YAML-friendly run summary.

  • temp

    Resolve a temporary artifact below tmp/ and queue it until run end.

Attributes:

entrypoint cached property

entrypoint: Path

Python entrypoint used to derive the experiment name and folders.

plugins class-attribute instance-attribute

plugins: PluginManager = 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.

repo cached property

repo: Repo | None

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.

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.

end

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

Flush artifacts, record shutdown metadata, and end plugins.

Parameters:

  • exc (BaseException | None, default: None ) –

    Exception raised by the experiment, if any.

Source code in src/liblaf/cherries/core/_run.py
def end(self, exc: BaseException | None = None) -> None:
    """Flush artifacts, record shutdown metadata, and end plugins.

    Args:
        exc: Exception raised by the experiment, if any.
    """
    self.log_other("cherries/end_time", datetime.now().astimezone())
    if exc is not None:
        self.log_other(
            "cherries/exception", "\n".join(traceback.format_exception_only(exc))
        )
    self._assets.end()
    self.plugins.delegate("end", exc=exc)

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,
    *,
    metadata: Mapping[str, Any] | None = None,
) -> Path

Resolve and immediately log an input below data/.

Source code in src/liblaf/cherries/core/_run.py
def input(
    self, path: StrPath, *, metadata: Mapping[str, Any] | None = None
) -> Path:
    """Resolve and immediately log an input below `data/`."""
    return self._assets.input(path, metadata=metadata)

log_asset

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

Log an existing generic artifact immediately.

Source code in src/liblaf/cherries/core/_run.py
def log_asset(
    self, path: StrPath, metadata: Mapping[str, Any] | None = None
) -> None:
    """Log an existing generic artifact immediately."""
    self._assets.log_asset(path, metadata=metadata)

log_input

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

Log an existing input artifact immediately.

Source code in src/liblaf/cherries/core/_run.py
def log_input(
    self, path: StrPath, metadata: Mapping[str, Any] | None = None
) -> None:
    """Log an existing input artifact immediately."""
    self._assets.log_input(path, metadata=metadata)

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

Log an existing output artifact immediately.

Source code in src/liblaf/cherries/core/_run.py
def log_output(
    self, path: StrPath, metadata: Mapping[str, Any] | None = None
) -> None:
    """Log an existing output artifact immediately."""
    self._assets.log_output(path, metadata=metadata)

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

Log an existing temporary artifact immediately.

Source code in src/liblaf/cherries/core/_run.py
def log_temp(
    self, path: StrPath, metadata: Mapping[str, Any] | None = None
) -> None:
    """Log an existing temporary artifact immediately."""
    self._assets.log_temp(path, metadata=metadata)

output

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

Resolve an output below data/ and queue it until run end.

Source code in src/liblaf/cherries/core/_run.py
def output(
    self,
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path:
    """Resolve an output below `data/` and queue it until run end."""
    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

Start plugins and record Cherries run metadata.

Source code in src/liblaf/cherries/core/_run.py
def start(self) -> None:
    """Start plugins and record Cherries run metadata."""
    self.plugins.delegate("start")
    entrypoint: Path = relative_or_absolute(self.entrypoint, self.project_dir)
    exp_dir: Path = relative_or_absolute(self.working_dir, self.project_dir)
    self.log_other("cherries/cmd", shlex.join(sys.orig_argv))
    self.log_other("cherries/entrypoint", entrypoint)
    self.log_other("cherries/exp_dir", exp_dir)
    self.log_other("cherries/start_time", self.start_time)

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

Resolve a temporary artifact below tmp/ and queue it until run end.

Source code in src/liblaf/cherries/core/_run.py
def temp(
    self,
    path: StrPath,
    *,
    metadata: Mapping[str, Any] | None = None,
    mkdir: bool = True,
) -> Path:
    """Resolve a temporary artifact below `tmp/` and queue it until run end."""
    return self._assets.temp(path, metadata=metadata, mkdir=mkdir)

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,
    *,
    metadata: Mapping[str, Any] | None = None,
) -> Path

log_asset

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

log_input

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

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

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

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"
    output: Path = cherries.output("hello.txt", mkdir=True)


def experiment(cfg: Config) -> None:
    cfg.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"
            output: Path = cherries.output("hello.txt", mkdir=True)


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


        cherries.main(experiment, profile="debug")
        ```
    """
    run: core.Run = start(profile=profile)
    args, kwargs = _make_args(main)
    configs: list[pydantic.BaseModel] = [
        arg for arg in (*args, *kwargs.values()) if isinstance(arg, pydantic.BaseModel)
    ]
    for config in configs:
        run.log_params(config.model_dump(mode="json"))
    try:
        result: Any = main(*args, **kwargs)
        if asyncio.iscoroutine(result):
            result: Any = asyncio.run(result)
    except BaseException as exc:
        run.end(exc=exc)
        raise
    else:
        run.end()
        return result

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()
    run.start()
    return run

temp

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