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>
This commit is contained in:
115
meeting_room/config.py
Normal file
115
meeting_room/config.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""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,
|
||||
)
|
||||
Reference in New Issue
Block a user