Spaces:
Running on Zero
Running on Zero
| """ | |
| Config loader with `${ENV_VAR}` and `${ENV_VAR:default}` interpolation. | |
| We intentionally keep config logic lightweight so it works well in Spaces. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| from pathlib import Path | |
| from typing import Any, Dict, Optional | |
| import yaml | |
| class Config: | |
| """Load and access YAML config with env var interpolation.""" | |
| def __init__(self, config_path: str): | |
| self.config_path = Path(config_path) | |
| self._config = self._load_config() | |
| self._config = self._recursive_resolve(self._config) | |
| def _load_config(self) -> Dict[str, Any]: | |
| if not self.config_path.exists(): | |
| raise FileNotFoundError(f"Config file not found: {self.config_path}") | |
| with open(self.config_path, "r", encoding="utf-8") as f: | |
| data = yaml.safe_load(f) or {} | |
| if not isinstance(data, dict): | |
| raise ValueError("Config root must be a mapping/dict") | |
| return data | |
| def _resolve_string(value: str) -> str: | |
| # Pattern: ${VAR_NAME} or ${VAR_NAME:default_value} | |
| # NOTE: default_value may be empty, e.g. `${API_KEY:}`. Use `*` (not `+`) to allow empty. | |
| pattern = r"\$\{([^:}]+)(?::([^}]*))?\}" | |
| def replace(match: re.Match) -> str: | |
| var_name = match.group(1) | |
| default_value = match.group(2) if match.group(2) is not None else "" | |
| return os.getenv(var_name, default_value) | |
| return re.sub(pattern, replace, value) | |
| def _recursive_resolve(self, obj: Any) -> Any: | |
| if isinstance(obj, dict): | |
| return {k: self._recursive_resolve(v) for k, v in obj.items()} | |
| if isinstance(obj, list): | |
| return [self._recursive_resolve(v) for v in obj] | |
| if isinstance(obj, str): | |
| return self._resolve_string(obj) | |
| return obj | |
| def get(self, key: str, default: Any = None) -> Any: | |
| keys = key.split(".") | |
| value: Any = self._config | |
| for k in keys: | |
| if isinstance(value, dict) and k in value: | |
| value = value[k] | |
| else: | |
| return default | |
| return value | |
| def get_str(self, key: str, default: str = "") -> str: | |
| v = self.get(key, default) | |
| return default if v is None else str(v) | |
| def get_int(self, key: str, default: int = 0) -> int: | |
| v = self.get(key, default) | |
| if v is None: | |
| return default | |
| if isinstance(v, int): | |
| return v | |
| try: | |
| return int(str(v).strip()) | |
| except Exception: | |
| return default | |
| def get_float(self, key: str, default: float = 0.0) -> float: | |
| v = self.get(key, default) | |
| if v is None: | |
| return default | |
| if isinstance(v, (int, float)): | |
| return float(v) | |
| try: | |
| return float(str(v).strip()) | |
| except Exception: | |
| return default | |
| def get_bool(self, key: str, default: bool = False) -> bool: | |
| v = self.get(key, default) | |
| if isinstance(v, bool): | |
| return v | |
| if v is None: | |
| return default | |
| s = str(v).strip().lower() | |
| if s in {"1", "true", "yes", "y", "on"}: | |
| return True | |
| if s in {"0", "false", "no", "n", "off"}: | |
| return False | |
| return default | |
| def as_dict(self) -> Dict[str, Any]: | |
| return dict(self._config) | |