"""Single-source config loader for all Sanad subsystems. Each subsystem (core, voice, motion, dashboard) has its own JSON file at `config/_config.json`. This module loads them on demand, caches the result, and exposes helpers for pulling nested sections. Usage: from Project.Sanad.core.config_loader import load, get cfg = load("voice") # full voice config dict threshold = get("voice", "barge_in.threshold", 500) rates = get("voice", "sanad_voice", {}) # whole section Why JSON (not TOML/YAML): standard library only, editable in any text editor, commented via "_comment" keys. No third-party dep. """ from __future__ import annotations import json import threading from pathlib import Path from typing import Any from Project.Sanad.core.logger import get_logger log = get_logger("config_loader") # Resolved at first-load time (avoids circular import with config.py) _BASE_DIR: Path | None = None _CONFIG_DIR: Path | None = None _CACHE: dict[str, dict[str, Any]] = {} _LOCK = threading.Lock() def _resolve_dirs() -> tuple[Path, Path]: """Find Sanad's root and config/ directory (lazy + cached).""" global _BASE_DIR, _CONFIG_DIR if _BASE_DIR is not None and _CONFIG_DIR is not None: return _BASE_DIR, _CONFIG_DIR here = Path(__file__).resolve().parent # Sanad/core base = here.parent # Sanad/ _BASE_DIR = base _CONFIG_DIR = base / "config" return _BASE_DIR, _CONFIG_DIR def _strip_comments(d: Any) -> Any: """Remove top-level "_comment"/"_description" keys — noise for callers.""" if isinstance(d, dict): return { k: _strip_comments(v) for k, v in d.items() if not (isinstance(k, str) and k.startswith("_")) } if isinstance(d, list): return [_strip_comments(x) for x in d] return d def load(subsystem: str) -> dict[str, Any]: """Load + cache config/_config.json. Returns a dict with all leading-underscore keys stripped. Missing file returns an empty dict (callers supply their own defaults via `get(..., default)`). """ with _LOCK: if subsystem in _CACHE: return _CACHE[subsystem] _, cfg_dir = _resolve_dirs() path = cfg_dir / f"{subsystem}_config.json" if not path.exists(): log.warning("config file missing: %s — using empty dict", path) _CACHE[subsystem] = {} return _CACHE[subsystem] try: raw = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: log.error("config file %s unreadable: %s", path, exc) _CACHE[subsystem] = {} return _CACHE[subsystem] cleaned = _strip_comments(raw) _CACHE[subsystem] = cleaned return cleaned def get(subsystem: str, dotted_key: str, default: Any = None) -> Any: """Fetch a nested key. Supports dotted-paths: 'barge_in.threshold'.""" cfg = load(subsystem) parts = dotted_key.split(".") cur: Any = cfg for p in parts: if not isinstance(cur, dict) or p not in cur: return default cur = cur[p] return cur def section(subsystem: str, name: str) -> dict[str, Any]: """Convenience — load one top-level section, always returning a dict. Example: `section("voice", "sanad_voice")` → dict of that section. """ s = get(subsystem, name, {}) return s if isinstance(s, dict) else {} def reload(subsystem: str | None = None) -> None: """Drop cached config so next load() re-reads from disk.""" with _LOCK: if subsystem is None: _CACHE.clear() else: _CACHE.pop(subsystem, None) def config_dir() -> Path: """Absolute path to Sanad/config/.""" _, d = _resolve_dirs() return d