Files
trumandu--myboot/myboot/core/config.py
T
2026-07-13 12:34:57 +08:00

226 lines
6.9 KiB
Python

"""
配置管理模块
使用 Dynaconf 提供强大的配置管理功能
支持远程加载文件、配置文件优先级、环境变量覆盖等
"""
import os
import tempfile
from pathlib import Path
from typing import Any, Optional
import requests
from dynaconf import Dynaconf
def _find_project_root() -> str:
"""查找项目根目录"""
current_dir = Path(__file__).parent.absolute()
while current_dir.parent != current_dir:
if (current_dir / 'pyproject.toml').exists():
return str(current_dir)
current_dir = current_dir.parent
return os.getcwd()
def _is_url(path: str) -> bool:
"""检查是否为 URL"""
return path and path.startswith(('http://', 'https://'))
def _download_config(url: str, cache_dir: str) -> str:
"""下载配置文件到缓存"""
import hashlib
url_hash = hashlib.md5(url.encode()).hexdigest()
cache_path = os.path.join(cache_dir, f"{url_hash}.yaml")
try:
print(f"正在下载配置文件: {url}")
response = requests.get(url, timeout=30)
response.raise_for_status()
with open(cache_path, 'w', encoding='utf-8') as f:
f.write(response.text)
print(f"配置文件已更新并缓存到: {cache_path}")
return cache_path
except Exception as e:
print(f"下载配置文件失败: {e}")
if os.path.exists(cache_path):
print(f"网络连接失败,使用缓存的配置文件: {cache_path}")
return cache_path
else:
print(f"无可用缓存文件,下载失败: {e}")
raise
def _get_config_files(config_file: Optional[str] = None) -> list:
"""获取配置文件列表(供 Dynaconf ``settings_files`` 使用)。
Dynaconf 按列表顺序依次加载,**后加载的文件会覆盖先加载的同名字段**。
因此列表顺序为「基底 → 越来越高优先级」,而不是把「最重要」的文件放在最前。
合并结果上的优先级(后者覆盖前者):
项目根目录 ``config.yaml`` < ``conf/config.yaml`` < 参数 ``config_file`` < 环境变量 ``CONFIG_FILE``
"""
project_root = _find_project_root()
cache_dir = os.path.join(tempfile.gettempdir(), 'myboot_config_cache')
os.makedirs(cache_dir, exist_ok=True)
config_files = []
added_paths = set() # 用于去重
# 与 Dynaconf 加载顺序一致:靠前的先加载(作默认),靠后的覆盖同名键
config_paths = [
# 1. 项目根目录(最先加载)
os.path.join(project_root, 'config.yaml'),
os.path.join(project_root, 'config.yml'),
# 2. conf 目录
os.path.join(project_root, 'conf', 'config.yaml'),
os.path.join(project_root, 'conf', 'config.yml'),
# 3. 调用方显式传入
config_file,
# 4. CONFIG_FILE 环境变量(最后加载,覆盖上述来源中的同名字段)
os.getenv('CONFIG_FILE'),
]
for config_path in config_paths:
if not config_path:
continue
# 处理 URL 配置
if _is_url(config_path):
downloaded_path = _download_config(config_path, cache_dir)
if downloaded_path and downloaded_path not in added_paths:
config_files.append(downloaded_path)
added_paths.add(downloaded_path)
# 处理文件路径
elif os.path.exists(config_path) and config_path not in added_paths:
config_files.append(config_path)
added_paths.add(config_path)
return config_files
def create_settings(config_file: Optional[str] = None) -> Dynaconf:
"""创建 Dynaconf 设置实例"""
config_files = _get_config_files(config_file)
# 创建 Dynaconf 配置
settings = Dynaconf(
# 配置文件列表
settings_files=config_files,
# 环境变量前缀(禁用前缀)
envvar_prefix=False,
# 环境变量分隔符
envvar_separator="__",
# 是否自动转换环境变量类型
env_parse_values=True,
# 是否忽略空值
ignore_unknown_envvars=True,
# 是否合并环境变量
merge_enabled=True,
# 自动加载项目根目录 .env(不覆盖已存在的真实环境变量,
# 与容器部署习惯一致:真实 env > .env > 配置文件)
load_dotenv=True,
dotenv_path=os.path.join(_find_project_root(), ".env"),
dotenv_override=False,
# 默认值:以大写 kwargs 传入(Dynaconf 将任意大写 kwarg 注册为默认配置项)。
# 注意不能用 default_settings= —— Dynaconf 没有该参数,
# 它会被当成一个名为 DEFAULT_SETTINGS 的普通配置项,默认值整体失效
APP={
"name": "MyBoot App",
"version": "0.1.0"
},
SERVER={
"host": "0.0.0.0",
"port": 8000,
"reload": True,
"workers": 1,
"keep_alive_timeout": 5,
"graceful_timeout": 30,
"response_format": {
"enabled": True,
"exclude_paths": ["/docs"]
}
},
LOGGING={
"level": "INFO"
},
METRICS={
"enabled": False,
"path": "/metrics",
"multiproc_dir": None,
"http_metrics": True
},
SCHEDULER={
"enabled": True,
"timezone": "UTC",
"max_workers": 10
}
)
return settings
# 全局配置实例
_settings: Optional[Dynaconf] = None
def get_settings(config_file: Optional[str] = None) -> Dynaconf:
"""获取 Dynaconf 设置实例"""
global _settings
if _settings is None:
_settings = create_settings(config_file)
return _settings
# 为了向后兼容,保留一些便捷函数
def get_config(key: str, default: Any = None) -> Any:
"""获取配置值的便捷函数"""
return get_settings().get(key, default)
def get_config_str(key: str, default: str = "") -> str:
"""获取字符串配置值的便捷函数"""
value = get_config(key, default)
return str(value) if value is not None else default
def get_config_int(key: str, default: int = 0) -> int:
"""获取整数配置值的便捷函数"""
value = get_config(key, default)
try:
return int(value)
except (ValueError, TypeError):
return default
def get_config_bool(key: str, default: bool = False) -> bool:
"""获取布尔配置值的便捷函数"""
value = get_config(key, default)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', '1', 'yes', 'on')
return bool(value)
def reload_config() -> None:
"""重新加载配置"""
global _settings
_settings = None