""" arma_modlist_tools.config ~~~~~~~~~~~~~~~~~~~~~~~~~ Load and expose project configuration from ``config.json``. Search order for the config file: 1. Explicit path passed to :func:`load_config` 2. ``config.json`` in the current working directory 3. ``config.json`` two levels above this module (project root) Typical usage:: from arma_modlist_tools.config import load_config cfg = load_config() print(cfg.server_url) print(cfg.arma_dir) """ from __future__ import annotations import json from pathlib import Path class Config: """Typed wrapper around the parsed ``config.json`` dict.""" def __init__(self, data: dict) -> None: # Validate required keys immediately so callers get a clear error at # load time rather than a confusing AttributeError deep in the pipeline. _ = data["server"]["base_url"] _ = data["server"]["username"] _ = data["server"]["password"] _ = data["paths"]["arma_dir"] _ = data["paths"]["downloads"] _ = data["paths"]["modlist_html"] _ = data["paths"]["modlist_json"] self._data = data # ---- server ---- @property def server_url(self) -> str: return self._data["server"]["base_url"] @property def server_auth(self) -> tuple[str, str]: return (self._data["server"]["username"], self._data["server"]["password"]) # ---- paths ---- @property def arma_dir(self) -> Path: return Path(self._data["paths"]["arma_dir"]) @property def downloads(self) -> Path: return Path(self._data["paths"]["downloads"]) @property def modlist_html(self) -> Path: return Path(self._data["paths"]["modlist_html"]) @property def modlist_json(self) -> Path: return Path(self._data["paths"]["modlist_json"]) # ---- derived paths ---- @property def comparison(self) -> Path: return self.modlist_json / "comparison.json" @property def missing_report(self) -> Path: return self.modlist_json / "missing_report.json" def load_config(path: Path | str | None = None) -> Config: """ Load ``config.json`` and return a :class:`Config` instance. :param path: Explicit path to the config file. If ``None``, the function searches the current working directory then the project root. :raises FileNotFoundError: If no config file can be located. :raises KeyError: If required keys are absent from the config file. """ if path is not None: config_path = Path(path) else: # Try CWD first, then project root (two levels above this file) cwd_path = Path.cwd() / "config.json" root_path = Path(__file__).parent.parent / "config.json" if cwd_path.exists(): config_path = cwd_path elif root_path.exists(): config_path = root_path else: raise FileNotFoundError( "config.json not found. " f"Looked in:\n {cwd_path}\n {root_path}\n" "Create config.json in the project root (copy from the template)." ) with open(config_path, encoding="utf-8") as f: data = json.load(f) return Config(data)