from __future__ import annotations import json from dataclasses import dataclass from pathlib import Path from typing import Any, Callable @dataclass(frozen=True) class ArtifactSpec: id: str title: str kind: str description: str outputs: tuple[str, ...] tags: tuple[str, ...] = () producer: Callable[[Any], None] | None = None aliases: tuple[str, ...] = () class ArtifactRegistry: def __init__(self) -> None: self.specs: list[ArtifactSpec] = [] self._by_name: dict[str, ArtifactSpec] = {} def register(self, spec: ArtifactSpec) -> None: self.specs.append(spec) for name in (spec.id, *spec.aliases): self._by_name[name] = spec def resolve(self, targets: set[str]) -> list[ArtifactSpec]: if not targets or "all" in targets: return [spec for spec in self.specs if spec.producer is not None] resolved: list[ArtifactSpec] = [] seen: set[str] = set() for target in sorted(targets): spec = self._by_name.get(target) if spec is None: raise ValueError(f"unknown artifact target: {target}") if spec.producer is not None and spec.id not in seen: resolved.append(spec) seen.add(spec.id) return resolved def run(self, targets: set[str], ctx: Any) -> list[ArtifactSpec]: selected = self.resolve(targets) for spec in selected: assert spec.producer is not None spec.producer(ctx) return selected def _output_entry(path: str) -> dict[str, str]: suffix = Path(path).suffix.lower().lstrip(".") kind = "data" if suffix in {"png", "pdf", "svg"}: kind = "figure" elif suffix in {"md", "txt"}: kind = "text" elif suffix in {"html"}: kind = "html" return {"path": path, "kind": kind} def write_artifacts_manifest(*, registry: ArtifactRegistry, out_dir: Path) -> None: payload = { "artifacts": [ { "id": spec.id, "title": spec.title, "kind": spec.kind, "description": spec.description, "tags": list(spec.tags), "outputs": [_output_entry(path) for path in spec.outputs], } for spec in registry.specs ] } out_dir.mkdir(parents=True, exist_ok=True) (out_dir / "artifacts_manifest.json").write_text(json.dumps(payload, indent=2), encoding="utf-8") def write_dashboard_html(*, out_dir: Path) -> None: manifest_path = out_dir / "artifacts_manifest.json" manifest_text = manifest_path.read_text(encoding="utf-8") if manifest_path.exists() else '{"artifacts":[]}' html = f"""
Local browser for paper figures, tables, CSVs, and reports.