""" 应用配置管理 使用 Pydantic Settings 进行类型安全的配置管理, 集成 ConfigLoader 支持环境变量和 YAML 配置文件。 配置优先级(从高到低): 1. 环境变量(前缀 SATONANO_) 2. config.yaml 文件 3. .env 文件 4. 默认值 """ from functools import lru_cache from pathlib import Path from typing import Any, Literal from pydantic import Field, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict from .config_loader import create_config_loader def _yaml_settings_source() -> dict[str, Any]: """ YAML 配置源工厂函数 为 Pydantic Settings 提供 YAML 配置数据 """ # 查找配置文件路径 config_paths = [ Path("config.yaml"), Path("config.yml"), Path(__file__).parent.parent.parent / "config.yaml", Path(__file__).parent.parent.parent / "config.yml", ] yaml_path = None for path in config_paths: if path.exists(): yaml_path = path break if yaml_path is None: return {} # 使用 ConfigLoader 加载配置(不带环境变量前缀,让 pydantic 处理) loader = create_config_loader( yaml_path=yaml_path, env_prefix="", # 不读取环境变量,由 pydantic-settings 处理 yaml_required=False, ) return loader.load() class Settings(BaseSettings): """应用配置""" model_config = SettingsConfigDict( env_prefix="SATONANO_", env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # 应用基础配置 app_name: str = "SatoNano" app_version: str = "0.1.0" debug: bool = False environment: Literal["development", "staging", "production"] = "development" # API 配置 api_v1_prefix: str = "/api/v1" # 安全配置 secret_key: str = Field( default="CHANGE-THIS-SECRET-KEY-IN-PRODUCTION", description="JWT 签名密钥,生产环境必须更改", ) algorithm: str = "HS256" access_token_expire_minutes: int = 30 refresh_token_expire_days: int = 7 # 数据库配置 database_type: Literal["sqlite", "mysql", "postgresql"] = "sqlite" database_host: str = "localhost" database_port: int = 3306 database_name: str = "satonano" database_username: str = "" database_password: str = "" database_sqlite_path: str = "./satonano.db" database_echo: bool = False # OAuth2 配置 (Linux.do) oauth2_client_id: str = "" oauth2_client_secret: str = "" oauth2_callback_path: str = "/oauth2/callback" # 首选端点 oauth2_authorize_endpoint: str = "https://connect.linux.do/oauth2/authorize" oauth2_token_endpoint: str = "https://connect.linux.do/oauth2/token" oauth2_user_info_endpoint: str = "https://connect.linux.do/api/user" # 备用端点 oauth2_authorize_endpoint_reserve: str = "https://connect.linuxdo.org/oauth2/authorize" oauth2_token_endpoint_reserve: str = "https://connect.linuxdo.org/oauth2/token" oauth2_user_info_endpoint_reserve: str = "https://connect.linuxdo.org/api/user" oauth2_request_timeout: int = 10 # 请求超时时间(秒) # 密码策略 password_min_length: int = 8 password_max_length: int = 128 password_require_uppercase: bool = True password_require_lowercase: bool = True password_require_digit: bool = True password_require_special: bool = False # 用户名策略 username_min_length: int = 3 username_max_length: int = 32 # 前端静态文件配置 frontend_static_path: str = "./frontend/out" @computed_field @property def database_url(self) -> str: """ 动态构建数据库连接 URL 根据 database_type 自动生成对应的连接字符串 """ if self.database_type == "sqlite": return f"sqlite+aiosqlite:///{self.database_sqlite_path}" if self.database_type == "mysql": return ( f"mysql+aiomysql://{self.database_username}:{self.database_password}" f"@{self.database_host}:{self.database_port}/{self.database_name}" ) if self.database_type == "postgresql": return ( f"postgresql+asyncpg://{self.database_username}:{self.database_password}" f"@{self.database_host}:{self.database_port}/{self.database_name}" ) return f"sqlite+aiosqlite:///{self.database_sqlite_path}" @computed_field @property def is_production(self) -> bool: """是否为生产环境""" return self.environment == "production" @classmethod def settings_customise_sources( cls, settings_cls: type[BaseSettings], init_settings: Any, env_settings: Any, dotenv_settings: Any, file_secret_settings: Any, ) -> tuple[Any, ...]: """ 自定义配置源优先级 优先级顺序(从高到低): 1. 初始化参数 2. 环境变量 3. YAML 文件 4. .env 文件 5. 文件密钥 """ class YamlConfigSettingsSource: """YAML 配置源适配器""" def __init__(self, settings_cls: type[BaseSettings]) -> None: self.settings_cls = settings_cls self._yaml_data: dict[str, Any] | None = None def __call__(self) -> dict[str, Any]: if self._yaml_data is None: self._yaml_data = _yaml_settings_source() return self._yaml_data return ( init_settings, env_settings, YamlConfigSettingsSource(settings_cls), dotenv_settings, file_secret_settings, ) @lru_cache def get_settings() -> Settings: """获取配置单例""" return Settings() settings = get_settings()