1"""Shared configuration, paths and logging for the WP39 pipeline."""2from __future__ import annotations34import logging5import os6from dataclasses import dataclass7from pathlib import Path89import yaml1011ROOT = Path(__file__).resolve().parents[2]121314@dataclass(frozen=True)15class Paths:16 root: Path17 configs: Path18 artifacts: Path19 manifests: Path20 logs: Path21 evaluation: Path22 models: Path23 processed: Path24 results: Path25 figures: Path26 paper_tables: Path27 paper_figures: Path28 cache: Path2930 @classmethod31 def default(cls) -> "Paths":32 cache = Path(os.path.expanduser(os.environ.get("WP39_CACHE", "~/wp39-cache")))33 p = cls(root=ROOT, configs=ROOT / "configs", artifacts=ROOT / "artifacts", manifests=ROOT / "artifacts/manifests",34 logs=ROOT / "artifacts/logs", evaluation=ROOT / "artifacts/evaluation", models=ROOT / "artifacts/models",35 processed=ROOT / "data/processed", results=ROOT / "results", figures=ROOT / "figures",36 paper_tables=ROOT / "paper/tables", paper_figures=ROOT / "paper/figures", cache=cache)37 for d in (p.manifests, p.logs, p.evaluation, p.models, p.processed, p.results, p.figures, p.paper_tables,38 p.paper_figures, p.cache / "bars", p.cache / "fundamentals", p.cache / "features", p.cache / "external"):39 d.mkdir(parents=True, exist_ok=True)40 return p414243def load_config(name: str) -> dict:44 with open(ROOT / "configs" / f"{name}.yaml") as fh:45 return yaml.safe_load(fh)464748def get_logger(name: str, logfile: Path | None = None) -> logging.Logger:49 log = logging.getLogger(name)50 if log.handlers:51 return log52 log.setLevel(logging.INFO)53 fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")54 h = logging.StreamHandler()55 h.setFormatter(fmt)56 log.addHandler(h)57 if logfile:58 logfile.parent.mkdir(parents=True, exist_ok=True)59 fh = logging.FileHandler(logfile)60 fh.setFormatter(fmt)61 log.addHandler(fh)62 return log63