mirror of
https://github.com/Manoj-HV30/clawrity.git
synced 2026-05-16 19:35:21 +00:00
prototype
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user