from __future__ import annotations import datetime import sys import threading import time def log(enabled: bool, msg: str) -> None: if not enabled: return ts = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") print(f"[{ts}] {msg}") JSON = dict[str, object] class Heartbeat: def __init__(self, enabled: bool, label: str, interval_s: float = 5.0) -> None: self._enabled = enabled self._label = label self._interval_s = interval_s self._stop = threading.Event() self._t: threading.Thread | None = None def __enter__(self) -> Heartbeat: if not self._enabled: return self def run() -> None: start = time.time() while not self._stop.wait(self._interval_s): elapsed = int(time.time() - start) sys.stderr.write(f"[heartbeat] {self._label} ({elapsed}s)\n") sys.stderr.flush() self._t = threading.Thread(target=run, daemon=True) self._t.start() return self def __exit__(self, exc_type, exc, tb) -> None: _ = (exc_type, exc, tb) if not self._enabled: return self._stop.set() if self._t is not None: self._t.join(timeout=1.0) class Progress: def __init__( self, enabled: bool, total: int, label: str = "", stream=None, ) -> None: self._enabled = enabled self._total = max(int(total), 1) self._label = label self._stream = stream or sys.stderr self._current = 0 self._last_len = 0 def update(self, step: str, inc: int = 1) -> None: if not self._enabled: return self._current = min(self._total, self._current + max(int(inc), 0)) pct = int((self._current / self._total) * 100) bar_len = 24 filled = int(bar_len * self._current / self._total) bar = "#" * filled + "-" * (bar_len - filled) msg = f"[progress] {self._label} [{bar}] {pct:3d}% {step}" pad = " " * max(0, self._last_len - len(msg)) self._stream.write("\r" + msg + pad) self._stream.flush() self._last_len = len(msg) def done(self, step: str = "done") -> None: if not self._enabled: return self._current = self._total self.update(step, inc=0) self._stream.write("\n") self._stream.flush()