ZhangNy's picture
Add Space app files
75db650
"""
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
@staticmethod
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)