aboutsummaryrefslogtreecommitdiffstats
path: root/jprov/runner.py
diff options
context:
space:
mode:
authorJan Tuomi <jan@jantuomi.fi>2026-02-16 12:59:14 +0200
committerJan Tuomi <jan@jantuomi.fi>2026-02-16 12:59:14 +0200
commit5f46fde558c467414fa151ad962caaf7f07628fa (patch)
tree247d6fd45ddbc811fb71d2009fbbd2042e158791 /jprov/runner.py
Initial commit
Diffstat (limited to 'jprov/runner.py')
-rw-r--r--jprov/runner.py63
1 files changed, 63 insertions, 0 deletions
diff --git a/jprov/runner.py b/jprov/runner.py
new file mode 100644
index 0000000..88dfe7f
--- /dev/null
+++ b/jprov/runner.py
@@ -0,0 +1,63 @@
+"""Subprocess runner and logging utilities."""
+
+from __future__ import annotations
+
+import os
+import subprocess
+from dataclasses import dataclass
+from datetime import datetime, timezone
+from typing import Iterable, Mapping
+
+
+@dataclass
+class RunResult:
+ cmd: list[str]
+ returncode: int
+ stdout: str
+ stderr: str
+
+
+def log_line(level: str, scope: str, message: str) -> None:
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+ print(f"{ts} {level:<5} {scope}: {message}")
+
+
+def run(
+ cmd: Iterable[str],
+ *,
+ log_file: str | None = None,
+ env: Mapping[str, str] | None = None,
+ allow_fail: bool = False,
+ dry_run: bool = False,
+) -> RunResult:
+ """Run a command and optionally tee output to a log file."""
+ cmd_list = list(cmd)
+ if dry_run:
+ if log_file:
+ with open(log_file, "a", encoding="utf-8") as fh:
+ fh.write(f"[dry-run] {' '.join(cmd_list)}\n")
+ return RunResult(cmd=cmd_list, returncode=0, stdout="", stderr="")
+ env_map = os.environ.copy()
+ if env:
+ env_map.update(env)
+
+ proc = subprocess.Popen(
+ cmd_list,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ env=env_map,
+ )
+ stdout, stderr = proc.communicate()
+
+ if log_file:
+ with open(log_file, "a", encoding="utf-8") as fh:
+ if stdout:
+ fh.write(stdout)
+ if stderr:
+ fh.write(stderr)
+
+ if proc.returncode != 0 and not allow_fail:
+ raise RuntimeError(f"command failed: {' '.join(cmd_list)} (exit {proc.returncode})")
+
+ return RunResult(cmd=cmd_list, returncode=proc.returncode, stdout=stdout, stderr=stderr)