Files
meeting-room/meeting_room/config.py
vitya d0507b3b0c feat: extract config.py, refactor CLI to use event-driven engine, add deprecation shim
- config.py: extracted load_config(), find_workspace_config(),
  load_discussion_config() from cli.py; fixed Windows root detection
- cli.py: run_discussion() now creates DiscussionEngine + EventEmitter,
  subscribes to events with ANSI-color output (identical to before),
  delegates to engine.run()
- backends/custom/run.py: deprecation shim with DeprecationWarning,
  delegates to DiscussionEngine
- 16 new config tests, all 184 tests passing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:33:07 +03:00

115 lines
3.2 KiB
Python

"""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,
)