"""配置加载与设置模型。 优先级(高 -> 低): 1. 环境变量 SMARTEDT_CONFIG 指定的配置文件 2. 在若干候选位置寻找 config.ini(兼容 PyInstaller 打包运行态) 3. 环境变量的 fallback 4. 内置默认值 """ from __future__ import annotations import configparser import os from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class ServerSettings: """服务监听配置。""" host: str = "0.0.0.0" port: int = 5000 debug: bool = False @dataclass(frozen=True) class FileSettings: """文件存储相关配置。""" root_path: Path @dataclass(frozen=True) class DatabaseSettings: """数据库连接相关配置。""" url: str timescaledb: bool = True @dataclass(frozen=True) class UnitySettings: host: str = "127.0.0.1" port: int = 6000 @dataclass(frozen=True) class AppSettings: """应用聚合配置。""" server: ServerSettings files: FileSettings database: DatabaseSettings unity: UnitySettings import sys def _find_config_file() -> Path | None: """尝试从若干候选位置定位 config.ini(包含 PyInstaller 运行态)。""" # Handle PyInstaller frozen state if getattr(sys, 'frozen', False): # If onefile, _MEIPASS. If onedir, executable dir or _internal # With onedir, the exe is in root, internal files in _internal. # But our spec put config in backend/config # Check relative to executable exe_dir = Path(sys.executable).parent candidates = [ exe_dir / "backend" / "config" / "config.ini", exe_dir / "_internal" / "backend" / "config" / "config.ini", exe_dir / "config.ini", ] for path in candidates: if path.exists(): return path candidates = [ Path(__file__).resolve().parent / "config.ini", Path(__file__).resolve().parents[1] / "config.ini", Path(__file__).resolve().parents[2] / "config.ini", ] for path in candidates: if path.exists(): return path return None def load_settings() -> AppSettings: """加载并返回应用配置。""" config = configparser.ConfigParser() config_path = os.getenv("SMARTEDT_CONFIG") if config_path: config.read(config_path, encoding="utf-8") else: found = _find_config_file() if found: config.read(found, encoding="utf-8") server = ServerSettings( host=config.get("SERVER", "host", fallback=os.getenv("SMARTEDT_HOST", "0.0.0.0")), port=config.getint("SERVER", "port", fallback=int(os.getenv("SMARTEDT_PORT", "5000"))), debug=config.getboolean("SERVER", "debug", fallback=os.getenv("SMARTEDT_DEBUG", "False").lower() == "true"), ) default_root = Path(os.getenv("SMARTEDT_FILE_ROOT", "data")) root_value = config.get("FILEPATH", "path", fallback=str(default_root)) root_path = Path(root_value) if not root_path.is_absolute(): root_path = (Path(__file__).resolve().parents[1] / root_path).resolve() database_url = config.get("DATABASE", "url", fallback=os.getenv("SMARTEDT_DATABASE_URL", "")).strip() if not database_url: database_url = "postgresql+psycopg://smartedt:CHANGE_ME@127.0.0.1:5432/smartedt" timescaledb = config.getboolean( "DATABASE", "timescaledb", fallback=os.getenv("SMARTEDT_TIMESCALEDB", "True").lower() == "true", ) unity = UnitySettings( host=config.get("UNITY", "host", fallback=os.getenv("SMARTEDT_UNITY_HOST", "127.0.0.1")), port=config.getint("UNITY", "port", fallback=int(os.getenv("SMARTEDT_UNITY_PORT", "6000"))), ) return AppSettings( server=server, files=FileSettings(root_path=root_path), database=DatabaseSettings(url=database_url, timescaledb=timescaledb), unity=unity, )