Skip to content
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,42 @@ Last match wins. Patterns are matched against both stdout and stderr. Stored in

Supported types: `"float"` (default), `"int"`, `"str"`, `"timedelta"`.

## Pre/Post commands

Run auxiliary commands before or after the main subprocess. Useful for capturing
build steps, environment snapshots, or post-run summaries:

<!-- blacken-docs:off -->

```python
from lite_runner import Command

runner = Runner(
command="python train.py",
pre_commands=[
Command("uv-sync", "uv sync -v"), # sync env before run
Command("ls-out", "ls -la $output", env={"LC_ALL": "C"}), # $output is interpolated
],
post_commands=[
Command("uv-pip-list", "uv pip list"), # snapshot resolved versions
],
)
```

<!-- blacken-docs:on -->

- Each command's stdout/stderr is streamed to the terminal and saved to
`<phase>_<name>.log` (combined), `<phase>_<name>_stdout.log`, and
`<phase>_<name>_stderr.log` in the run's output directory; the log files are
also uploaded to W&B.
- Commands run sequentially in the order they appear in the list.
- `$output` is interpolated in both command tokens and `env` values.
- Each `Command` has its own optional `env` (None to unset). Aux commands do
**not** inherit `Runner.env` or `Runner.secret_env` — they get a clean env
built from `os.environ` plus per-command overrides.
- A non-zero exit from a **pre-command** aborts the run (main and post are skipped).
- A non-zero exit from a **post-command** is logged but does not change the run outcome.

## Sweeps

Loop with `override()`. Runs are grouped in W&B for easy comparison:
Expand Down Expand Up @@ -205,6 +241,8 @@ Runner(
secret_env={
"HF_TOKEN": "hf_xxx",
}, # like env, but redacted in logs / recorded config
pre_commands=[Command("build", "make")], # run before main; failure aborts
post_commands=[Command("du", "du -sh ./")], # run after main; failure logged
project="my-project", # default: git repo name
run_group="my-sweep", # W&B run group for sweeps (None = no grouping)
)
Expand Down
10 changes: 9 additions & 1 deletion examples/run_example.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Run with: uv run python examples/run_example.py
"""Example run config for the fake model."""

from lite_runner import Metric, Output, Param, Runner
from lite_runner import Command, Metric, Output, Param, Runner

runner = Runner(
command="python examples/fake_model.py",
Expand Down Expand Up @@ -39,6 +39,14 @@
],
tags=["example"],
env={"FAKE_MODEL_DEBUG": "1"},
pre_commands=[
# Sync the env before the run; abort if it fails.
Command("uv-sync", "uv sync -v"),
],
post_commands=[
# Snapshot the resolved package versions for reproducibility.
Command("uv-pip-list", "uv pip list"),
],
)

if __name__ == "__main__":
Expand Down
3 changes: 2 additions & 1 deletion src/lite_runner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@

from ._version import version as _version
from .backends import JsonBackend, LogBackend, WandbBackend
from .params import UNSET, Metric, Output, Param, ParamType
from .params import UNSET, Command, Metric, Output, Param, ParamType
from .runner import Runner, RunResult

__version__ = _version
logging.getLogger(__name__).addHandler(logging.NullHandler())

__all__ = [
"UNSET",
"Command",
"JsonBackend",
"LogBackend",
"Metric",
Expand Down
39 changes: 38 additions & 1 deletion src/lite_runner/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from __future__ import annotations

import logging
import shlex
from collections.abc import Sequence
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Any, Literal, TypeGuard

import questionary
Expand Down Expand Up @@ -316,3 +317,39 @@ class Metric:
name: str
pattern: str
type: str = "float"


@dataclass
class Command:
"""An auxiliary command to run before or after the main subprocess.

Useful for setup/teardown work whose output should be captured for
reproducibility (e.g. ``uv pip list`` to snapshot the environment, ``make``
to build before running). Output is streamed to the terminal and saved to
``<phase>_<name>{,_stdout,_stderr}.log`` in the run's output directory.

``$output`` is interpolated to the run's output directory in both the
command tokens and ``env`` values.

Args:
name: Identifier used in log filenames; must be filesystem-safe.
command: Shell command (str is split via shlex; list is used as-is).
env: Per-command environment variables (None unsets a key). Layered
on top of ``os.environ``; does NOT inherit ``Runner.env`` or
``Runner.secret_env`` — aux commands get their own clean env.
"""

name: str
command: str | list[str]
env: dict[str, str | None] = field(default_factory=dict)

def __post_init__(self) -> None:
"""Split string command into a list and validate name."""
if isinstance(self.command, str):
self.command = shlex.split(self.command)
if not self.name or "/" in self.name or "\\" in self.name:
msg = (
f"Command name {self.name!r} must be non-empty and not contain "
"'/' or '\\'"
)
raise ValueError(msg)
Loading
Loading