"""Configuration loading for Meeting Room. Provides load_config(), find_workspace_config(), and load_discussion_config() which converts a raw YAML dict into a typed DiscussionConfig. """ from __future__ import annotations import os import re import yaml from meeting_room.models import DefaultsConfig, DiscussionConfig, ProviderConfig, RoleConfig PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) def load_config(config_path: str | None = None) -> dict: """Load config from file, resolve env vars, apply overrides. Parameters ---------- config_path : str | None Path to the YAML config file. When *None*, loads the package default config at ``meeting_room/config/config.yaml``. Returns ------- dict Raw configuration dictionary (the result of ``yaml.safe_load``). """ if config_path is None: config_path = os.path.join(PACKAGE_DIR, "config", "config.yaml") with open(config_path, encoding="utf-8") as f: raw = f.read() # Resolve ${ENV_VAR} placeholders def _env_replace(match: re.Match) -> str: var = match.group(1) return os.environ.get(var, match.group(0)) raw = re.sub(r"\$\{(\w+)\}", _env_replace, raw) config = yaml.safe_load(raw) # Override API keys from environment if set for prov_name, prov_cfg in config.get("providers", {}).items(): env_key = f"MEETING_ROOM_{prov_name.upper()}_API_KEY" if os.environ.get(env_key): prov_cfg["api_key"] = os.environ[env_key] env_url = f"MEETING_ROOM_{prov_name.upper()}_BASE_URL" if os.environ.get(env_url): prov_cfg["base_url"] = os.environ[env_url] return config def find_workspace_config() -> str | None: """Search for .meeting-room/config.yaml starting from CWD upward. Returns ------- str | None Absolute path to the workspace config file, or *None* if not found. """ current = os.getcwd() while True: candidate = os.path.join(current, ".meeting-room", "config.yaml") if os.path.exists(candidate): return candidate parent = os.path.dirname(current) if parent == current: # Reached filesystem root break current = parent return None def load_discussion_config(config_path: str | None = None) -> DiscussionConfig: """Load config and return a typed DiscussionConfig. Convenience wrapper that calls :func:`load_config` and then coerces the raw dict into :class:`DiscussionConfig`. Parameters ---------- config_path : str | None Path to the YAML config file. When *None*, loads the package default. Returns ------- DiscussionConfig Typed configuration object ready for use by DiscussionEngine. """ raw = load_config(config_path) providers = { name: ProviderConfig(**vals) for name, vals in raw.get("providers", {}).items() } roles = { name: RoleConfig(**vals) for name, vals in raw.get("roles", {}).items() } defaults = DefaultsConfig(**raw.get("defaults", {})) return DiscussionConfig( providers=providers, roles=roles, defaults=defaults, )