Skip to content

_stage

_stage

Stage-line rendering — one format, one colour palette, one column width.

The setup aggregator prints one <label> <marker> (<detail>) line per phase. Frontends (terok's terok setup, terok-executor CLIs, future CI reporters) can mix stage lines of their own — desktop-entry install, credential-DB purge, etc. — by importing the public symbols from terok_sandbox; keeping the renderer central guarantees the mixed log reads as one continuous column with aligned status markers and coherent colours.

Kept as an underscore-prefixed submodule because the package's __init__ already re-exports the surface — a public terok_sandbox.stage submodule name would collide with the re-exported stage() function.

Colour is auto-detected from NO_COLOR / FORCE_COLOR / sys.stdout.isatty() (the no-color.org contract). Terminals that don't report TTY — typical CI logs — fall through to plain text.

STAGE_WIDTH = 21 module-attribute

Marker

Bases: StrEnum

Status tokens rendered in each stage line.

A StrEnum so a typo ("Warn" vs "WARN") is a load-time error, not silent drift that test assertions happen to keep passing. Each value maps to an ANSI colour in _PALETTE.

OK = 'ok' class-attribute instance-attribute

WARN = 'WARN' class-attribute instance-attribute

FAIL = 'FAIL' class-attribute instance-attribute

MISSING = 'MISSING' class-attribute instance-attribute

SKIP = 'skip' class-attribute instance-attribute

StageLine(label)

Context-managed progressive stage line.

Couples stage_begin and stage_end at one call site so the begin/end pairing is structurally visible — a missing or misplaced end becomes impossible rather than a bug waiting to happen.

Use like::

with stage_line("Vault") as s:
    do_work()  # slow; label shows immediately
    s.ok("systemd, socket, reachable")  # marker + detail

Set the marker via ok, warn, fail, missing, or skip; only the most recent call wins (the single-line output has room for one marker). The caller can return early — the context manager's __exit__ still runs and emits whatever marker was last set.

Exception paths: if an exception escapes the with block the line is always completed as FAIL (<exception>) — an uncaught exception dominates any marker the caller set earlier. This catches the "optimistic early marker" bug where a caller writes s.ok("reachable") before a final check that turns out to raise; without this precedence rule the log would misleadingly read ok while the actual run failed. Callers that want their own message in the log should catch the exception, call fail with the wanted detail, and return normally — that path emits the caller's message with no exception to contend with. A block that exits with no marker set and no exception is a caller bug; the line is completed as FAIL (no marker set) to make the omission loud rather than leaving the label column dangling mid-line.

Capture label; deferred rendering until __enter__.

Source code in src/terok_sandbox/_stage.py
def __init__(self, label: str) -> None:
    """Capture *label*; deferred rendering until [`__enter__`][terok_sandbox.SSHManager.__enter__]."""
    self._label = label
    self._marker: Marker | None = None
    self._detail = ""

__enter__()

Emit the padded label column without a trailing newline.

Source code in src/terok_sandbox/_stage.py
def __enter__(self) -> StageLine:
    """Emit the padded label column without a trailing newline."""
    stage_begin(self._label)
    return self

__exit__(_exc_type, exc, _tb)

Emit the line: exception (if any) wins over stored marker; never suppresses.

Source code in src/terok_sandbox/_stage.py
def __exit__(
    self,
    _exc_type: type[BaseException] | None,
    exc: BaseException | None,
    _tb: TracebackType | None,
) -> None:
    """Emit the line: exception (if any) wins over stored marker; never suppresses."""
    if exc is not None:
        stage_end(Marker.FAIL, str(exc))
    elif self._marker is not None:
        stage_end(self._marker, self._detail)
    else:
        stage_end(Marker.FAIL, "no marker set")

ok(detail='')

Mark the line as ok with optional detail.

Source code in src/terok_sandbox/_stage.py
def ok(self, detail: str = "") -> None:
    """Mark the line as ``ok`` with optional detail."""
    self._marker, self._detail = Marker.OK, detail

warn(detail='')

Mark the line as WARN with optional detail.

Source code in src/terok_sandbox/_stage.py
def warn(self, detail: str = "") -> None:
    """Mark the line as ``WARN`` with optional detail."""
    self._marker, self._detail = Marker.WARN, detail

fail(detail='')

Mark the line as FAIL with optional detail.

Source code in src/terok_sandbox/_stage.py
def fail(self, detail: str = "") -> None:
    """Mark the line as ``FAIL`` with optional detail."""
    self._marker, self._detail = Marker.FAIL, detail

missing(detail='')

Mark the line as MISSING with optional detail.

Source code in src/terok_sandbox/_stage.py
def missing(self, detail: str = "") -> None:
    """Mark the line as ``MISSING`` with optional detail."""
    self._marker, self._detail = Marker.MISSING, detail

skip(detail='')

Mark the line as skip with optional detail.

Source code in src/terok_sandbox/_stage.py
def skip(self, detail: str = "") -> None:
    """Mark the line as ``skip`` with optional detail."""
    self._marker, self._detail = Marker.SKIP, detail

stage(label, marker, detail='')

Write one complete stage line: ' <label> <marker>[ (<detail>)]'.

Matches stage_begin + stage_end when the caller doesn't need progressive output. The marker is ANSI-coloured according to _PALETTE when colour is enabled.

Multi-line detail is split: only the first line lands inside the parentheses; remaining lines are emitted as an indented continuation block underneath. See stage_end for the rationale (callers passing a multi-line exception str()).

Source code in src/terok_sandbox/_stage.py
def stage(label: str, marker: Marker, detail: str = "") -> None:
    """Write one complete stage line: ``'  <label>  <marker>[ (<detail>)]'``.

    Matches [`stage_begin`][terok_sandbox._stage.stage_begin] + [`stage_end`][terok_sandbox._stage.stage_end] when the caller
    doesn't need progressive output.  The marker is ANSI-coloured
    according to `_PALETTE` when colour is enabled.

    Multi-line *detail* is split: only the first line lands inside the
    parentheses; remaining lines are emitted as an indented
    continuation block underneath.  See [`stage_end`][terok_sandbox._stage.stage_end] for the
    rationale (callers passing a multi-line exception ``str()``).
    """
    head, tail = _split_detail(detail)
    suffix = f" ({head})" if head else ""
    print(f"  {label:<{STAGE_WIDTH}} {_render_marker(marker)}{suffix}")
    _print_continuation(tail)

stage_begin(label)

Write the label column and flush — no newline, no marker.

Pairs with stage_end. Use when the phase takes long enough that the operator benefits from seeing which step is running before the marker lands. Without this, a slow systemctl --user restart looks like a frozen terminal.

Source code in src/terok_sandbox/_stage.py
def stage_begin(label: str) -> None:
    """Write the label column and flush — no newline, no marker.

    Pairs with [`stage_end`][terok_sandbox._stage.stage_end].  Use when the phase takes long enough
    that the operator benefits from seeing *which* step is running
    before the marker lands.  Without this, a slow
    ``systemctl --user restart`` looks like a frozen terminal.
    """
    print(f"  {label:<{STAGE_WIDTH}}", end="", flush=True)

stage_end(marker, detail='')

Write the marker and optional detail with trailing newline.

The sibling of stage_begin; together they render the same line stage would.

Multi-line detail is split: only the first line lands inside the parentheses on the marker line; the remainder is emitted as an indented continuation block underneath. This guards against callers passing exception str() whose help text spans several lines (e.g. a SystemExit carrying paths and remediation hints) — without the split, the dotted-column layout smears across the log and the closing paren lands on the last line of help text.

Source code in src/terok_sandbox/_stage.py
def stage_end(marker: Marker, detail: str = "") -> None:
    """Write the marker and optional detail with trailing newline.

    The sibling of [`stage_begin`][terok_sandbox._stage.stage_begin]; together they render the same
    line [`stage`][terok_sandbox._stage.stage] would.

    Multi-line *detail* is split: only the first line lands inside the
    parentheses on the marker line; the remainder is emitted as an
    indented continuation block underneath.  This guards against
    callers passing exception ``str()`` whose help text spans several
    lines (e.g. a ``SystemExit`` carrying paths and remediation hints)
    — without the split, the dotted-column layout smears across the
    log and the closing paren lands on the last line of help text.
    """
    head, tail = _split_detail(detail)
    suffix = f" ({head})" if head else ""
    print(f" {_render_marker(marker)}{suffix}")
    _print_continuation(tail)

stage_line(label)

Return a StageLine context manager for progressive rendering.

Thin factory so the call site reads with stage_line("Vault") as s: rather than the class name.

Source code in src/terok_sandbox/_stage.py
def stage_line(label: str) -> StageLine:
    """Return a [`StageLine`][terok_sandbox._stage.StageLine] context manager for progressive rendering.

    Thin factory so the call site reads ``with stage_line("Vault") as
    s:`` rather than the class name.
    """
    return StageLine(label)

supports_color()

Return whether ANSI colour should be emitted to stdout.

Follows the no-color.org <https://no-color.org>_ contract: NO_COLOR always wins; FORCE_COLOR (set to anything but "0") opts back in even on non-TTY streams; otherwise sys.stdout.isatty() decides. Cached at module-import time so the verdict is stable for the life of the process — tests that need a different answer set the env vars before importing.

Source code in src/terok_sandbox/_stage.py
def supports_color() -> bool:
    """Return whether ANSI colour should be emitted to stdout.

    Follows the `no-color.org <https://no-color.org>`_ contract:
    ``NO_COLOR`` always wins; ``FORCE_COLOR`` (set to anything but
    ``"0"``) opts back in even on non-TTY streams; otherwise
    ``sys.stdout.isatty()`` decides.  Cached at module-import time so
    the verdict is stable for the life of the process — tests that
    need a different answer set the env vars before importing.
    """
    return _COLOUR_ON

bold(text)

Return text wrapped in ANSI bold when supports_color is true.

Source code in src/terok_sandbox/_stage.py
def bold(text: str) -> str:
    """Return *text* wrapped in ANSI bold when [`supports_color`][terok_sandbox._stage.supports_color] is true."""
    return _color(text, "1")

red(text)

Return text wrapped in ANSI red for failure banners when colour is on.

Source code in src/terok_sandbox/_stage.py
def red(text: str) -> str:
    """Return *text* wrapped in ANSI red for failure banners when colour is on."""
    return _color(text, "31")

yellow(text)

Return text wrapped in ANSI yellow for warning banners when colour is on.

Source code in src/terok_sandbox/_stage.py
def yellow(text: str) -> str:
    """Return *text* wrapped in ANSI yellow for warning banners when colour is on."""
    return _color(text, "33")