# modules/common/settings.py import os from typing import Any, Dict, Iterable, Optional def _clean(s: Optional[str]) -> str: s = (s or "").strip() # strip accidental quotes Portainer sometimes adds if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")): s = s[1:-1].strip() return s def _collect_shai_env() -> Dict[str, str]: """ Build a {key_without_prefix_lower: cleaned_value} mapping from all environment variables that start with SHAI_. """ out: Dict[str, str] = {} for k, v in os.environ.items(): if not k.startswith("SHAI_"): continue key = k[5:].lower() # SHAI_MOD_CHANNEL_ID -> mod_channel_id out[key] = _clean(v) return out class ConfigView: """ Unified config view. - Primary: SHAI_* envs (prefix removed, lowercased keys) - Secondary: bot.config['DEFAULT'] (if present) - Helpers: get/int/bool/float/list - Can mirror values back into os.environ as SHAI_* (opt-in) """ def __init__(self, bot=None, *, mirror_to_env: bool = False): self._env_map = _collect_shai_env() # Optional: also look into bot.config['DEFAULT'] as a fallback self._default: Dict[str, Any] = {} try: self._default = (getattr(bot, "config", {}) or {}).get("DEFAULT", {}) or {} except Exception: self._default = {} if mirror_to_env: # Ensure os.environ has SHAI_* for everything we know (don’t clobber existing non-empty) for k, v in self._env_map.items(): env_key = f"SHAI_{k.upper()}" if not os.environ.get(env_key): os.environ[env_key] = v # ---- core accessors ---- def get(self, key: str, default: str = "") -> str: key = key.lower() if key in self._env_map: v = _clean(self._env_map[key]) return v if v != "" else default # Fallback to DEFAULT mapping (ConfigParser-like or our shim) try: v = self._default.get(key, "") except Exception: v = "" v = _clean(str(v)) return v if v != "" else default def int(self, key: str, default: int = 0) -> int: s = self.get(key, "") try: return int(s) except Exception: return default def float(self, key: str, default: float = 0.0) -> float: s = self.get(key, "") try: return float(s) except Exception: return default def bool(self, key: str, default: bool = False) -> bool: s = self.get(key, "") if s == "": return default s = s.lower() if s in ("1", "true", "yes", "on", "y", "t"): return True if s in ("0", "false", "no", "off", "n", "f"): return False return default def list(self, key: str, default: Optional[Iterable[str]] = None, sep: str = ",") -> Iterable[str]: s = self.get(key, "") if s == "": return list(default or []) parts = [p.strip() for p in s.split(sep)] return [p for p in parts if p] # expose the resolved map if you ever want to dump it for debug def to_dict(self) -> Dict[str, str]: d = dict(self._env_map) # Include defaults that aren’t already in env_map for k in getattr(self._default, "keys", lambda: [])(): d.setdefault(k, _clean(str(self._default.get(k, "")))) return d def cfg(bot=None, *, mirror_to_env: bool = False) -> ConfigView: """ Usage in cogs: r = cfg(bot) trigger_id = r.int('trigger_channel_id', 0) prefix = r.get('vc_name_prefix', 'Room') If you want to also ensure SHAI_* are present in os.environ at runtime: r = cfg(bot, mirror_to_env=True) """ return ConfigView(bot, mirror_to_env=mirror_to_env)