Files
SatoNano/app/core/config.py
2026-01-06 23:49:23 +08:00

203 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
应用配置管理
使用 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()