from __future__ import annotations import json from dataclasses import dataclass from typing import Any from huggingface_hub import HfFileSystem from .config import settings from .security import redact @dataclass(frozen=True) class RunPaths: run_id: str bucket_uri: str = settings.bucket_uri @property def root(self) -> str: return f"{self.bucket_uri}/runs/{self.run_id}" @property def state(self) -> str: return f"{self.root}/state.json" @property def events(self) -> str: return f"{self.root}/events.jsonl" @property def report(self) -> str: return f"{self.root}/report.md" def _fs(token: str | None = None) -> HfFileSystem: return HfFileSystem(token=token) def read_text(path: str, token: str | None = None) -> str | None: fs = _fs(token) try: with fs.open(path, "r") as f: return f.read() except FileNotFoundError: return None except Exception as exc: # noqa: BLE001 - surface readable error in UI return f"[Could not read {path}: {exc}]" def read_json(path: str, token: str | None = None) -> dict[str, Any] | None: content = read_text(path, token=token) if not content or content.startswith("[Could not read"): return None try: return json.loads(content) except json.JSONDecodeError: return {"_error": "Invalid JSON", "raw": redact(content)} def read_events(run_id: str, token: str | None = None) -> list[dict[str, Any]]: paths = RunPaths(run_id) content = read_text(paths.events, token=token) if not content: return [] events: list[dict[str, Any]] = [] for line in content.splitlines(): line = line.strip() if not line: continue try: events.append(json.loads(line)) except json.JSONDecodeError: events.append({"step": "parse_events", "status": "warning", "message": redact(line)}) return events def read_run_bundle(run_id: str, token: str | None = None) -> dict[str, Any]: paths = RunPaths(run_id) return { "paths": { "root": paths.root, "state": paths.state, "events": paths.events, "report": paths.report, }, "state": read_json(paths.state, token=token), "events": read_events(run_id, token=token), "report": redact(read_text(paths.report, token=token) or ""), }