203 lines
5.8 KiB
Python
203 lines
5.8 KiB
Python
"""
|
||
应用配置管理
|
||
|
||
使用 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()
|