Files
clawrity/config/client_loader.py
T
2026-05-04 22:00:38 +05:30

159 lines
4.5 KiB
Python

"""
Clawrity — Client Configuration Loader
Scans config/clients/ for YAML files and parses each into a ClientConfig model.
Supports ${ENV_VAR} interpolation in YAML values.
New client = new YAML file. Zero code changes.
"""
import os
import re
import glob
import logging
from typing import Dict, List, Optional
from pathlib import Path
import yaml
from pydantic import BaseModel
from config.settings import get_settings
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Pydantic models for client YAML structure
# ---------------------------------------------------------------------------
class DataSourceConfig(BaseModel):
type: str = "csv"
path: str = ""
class DatabaseConfig(BaseModel):
url: str = ""
schema_name: str = "" # 'schema' is a Pydantic reserved attr
class ScoutConfig(BaseModel):
sector: str = ""
competitors: List[str] = []
keywords: List[str] = []
news_lookback_days: int = 1
class ClientConfig(BaseModel):
client_id: str
client_name: str = ""
data_source: DataSourceConfig = DataSourceConfig()
database: DatabaseConfig = DatabaseConfig()
countries: List[str] = []
risk_threshold: float = 0.15
hallucination_threshold: float = 0.75
digest_schedule: str = "08:00"
timezone: str = "UTC"
channels: Dict[str, str] = {}
soul_file: str = ""
heartbeat_file: str = ""
column_mapping: Dict[str, str] = {}
scout: ScoutConfig = ScoutConfig()
# Runtime: workspace/team ID → client_id mapping for ProtocolAdapter
slack_workspace_ids: List[str] = []
# ---------------------------------------------------------------------------
# Environment variable interpolation
# ---------------------------------------------------------------------------
_ENV_PATTERN = re.compile(r"\$\{(\w+)\}")
def _interpolate_env(value: str) -> str:
"""Replace ${ENV_VAR} placeholders with actual environment variable values."""
def _replace(match):
var_name = match.group(1)
return os.environ.get(var_name, match.group(0))
if isinstance(value, str):
return _ENV_PATTERN.sub(_replace, value)
return value
def _interpolate_dict(d: dict) -> dict:
"""Recursively interpolate environment variables in a dictionary."""
result = {}
for key, value in d.items():
if isinstance(value, dict):
result[key] = _interpolate_dict(value)
elif isinstance(value, list):
result[key] = [
_interpolate_env(v) if isinstance(v, str) else v
for v in value
]
elif isinstance(value, str):
result[key] = _interpolate_env(value)
else:
result[key] = value
return result
# ---------------------------------------------------------------------------
# Loader
# ---------------------------------------------------------------------------
def load_client_configs(config_dir: Optional[str] = None) -> Dict[str, ClientConfig]:
"""
Load all client YAML files from the config directory.
Returns:
Dict mapping client_id → ClientConfig
"""
if config_dir is None:
config_dir = get_settings().clients_config_dir
configs: Dict[str, ClientConfig] = {}
yaml_pattern = os.path.join(config_dir, "*.yaml")
for yaml_path in glob.glob(yaml_pattern):
try:
with open(yaml_path, "r") as f:
raw = yaml.safe_load(f)
if not raw or "client_id" not in raw:
logger.warning(f"Skipping {yaml_path}: missing client_id")
continue
# Interpolate environment variables
interpolated = _interpolate_dict(raw)
# Handle 'schema' → 'schema_name' mapping for Pydantic
if "database" in interpolated and "schema" in interpolated["database"]:
interpolated["database"]["schema_name"] = interpolated["database"].pop("schema")
config = ClientConfig(**interpolated)
configs[config.client_id] = config
logger.info(f"Loaded client config: {config.client_id} from {yaml_path}")
except Exception as e:
logger.error(f"Error loading {yaml_path}: {e}")
if not configs:
logger.warning(f"No client configs found in {config_dir}")
return configs
def get_client_config(client_id: str, configs: Optional[Dict[str, ClientConfig]] = None) -> Optional[ClientConfig]:
"""Get a specific client config by ID."""
if configs is None:
configs = load_client_configs()
return configs.get(client_id)