"""Config loading for the m2-market CLI (docs/config.md, contracts/cli.md).""" from __future__ import annotations import os import tomllib from dataclasses import dataclass from pathlib import Path DEFAULT_CONFIG_PATH = Path.home() / ".m2-market" / "config.toml" REQUIRED_KEYS = ( "operator_id", "tenant", "ledger_url", "ledger_api_key", "memory_api_url", "memory_api_key", "forgejo_url", "forgejo_token", ) class ConfigError(Exception): """Raised when config.toml is missing, unreadable, or invalid.""" @dataclass class Config: operator_id: str tenant: str ledger_url: str ledger_api_key: str memory_api_url: str memory_api_key: str forgejo_url: str forgejo_token: str apply_adapter: str = "local" def config_path() -> Path: """Resolve the config.toml path, honoring the M2_MARKET_CONFIG override.""" override = os.environ.get("M2_MARKET_CONFIG") return Path(override).expanduser() if override else DEFAULT_CONFIG_PATH def load_config(path: Path | None = None) -> Config: """Load and validate ~/.m2-market/config.toml, raising ConfigError on failure.""" path = path or config_path() if not path.is_file(): raise ConfigError( f"config not found: {path} (set M2_MARKET_CONFIG or see docs/config.md)" ) try: with path.open("rb") as f: data = tomllib.load(f) except tomllib.TOMLDecodeError as exc: raise ConfigError(f"invalid TOML in {path}: {exc}") from exc except OSError as exc: raise ConfigError(f"cannot read {path}: {exc}") from exc missing = [key for key in REQUIRED_KEYS if not data.get(key)] if missing: raise ConfigError( f"{path} missing required key(s): {', '.join(missing)} (see docs/config.md)" ) kwargs = {key: data[key] for key in REQUIRED_KEYS} kwargs["apply_adapter"] = data.get("apply_adapter", "local") return Config(**kwargs)