mirror of
https://github.com/Manoj-HV30/clawrity.git
synced 2026-05-16 19:35:21 +00:00
159 lines
4.5 KiB
Python
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)
|