Files
life-echo/api/app/core/config.py
Sully 53e0065e3e refactor(api): TOML 配置 SSOT、统一错误契约、Auth/事务加固与可观测性 (#33)
配置 SSOT(TOML + .env)
统一错误契约
Auth 与事务边界
Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client
可观测性(OpenTelemetry + LGTM)
2026-05-22 13:44:50 +08:00

138 lines
4.2 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.
"""
统一配置:密钥与连接串经 .env / Settings其余非密钥项见 config/*.tomlAppConfig
本地开发时由 api/development.sh 在启动前将 .env.development 同步为 .env。
"""
from __future__ import annotations
from pydantic import AliasChoices, Field, model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.core.app_config_models import DeployConfig
from app.core.redis_urls import resolve_redis_urls
_DEV_SECRET_KEY = "dev-only-secret-key-do-not-use-in-production"
_DEPLOY_FIELD_NAMES = frozenset(DeployConfig.model_fields.keys())
class Settings(BaseSettings):
"""Secrets and bootstrap only — non-secret deploy/product config lives in TOML."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
database_url: str = "postgresql://postgres:postgres@localhost:48291/life_echo"
redis_url: str = "redis://localhost:48307/0"
redis_password: str = ""
celery_redis_url: str = ""
app_environment: str = Field(
default="development",
validation_alias=AliasChoices("APP_ENV", "APP_ENVIRONMENT"),
)
secret_key: str = _DEV_SECRET_KEY
deepseek_api_key: str = ""
zhipu_api_key: str = ""
tencent_secret_id: str = ""
tencent_secret_key: str = ""
wechat_pay_api_v3_key: str = ""
wechat_pay_private_key: str = ""
wechat_pay_platform_public_key: str = ""
alipay_private_key: str = ""
alipay_public_key: str = ""
liblib_access_key: str = ""
liblib_secret_key: str = ""
internal_eval_api_key: str = ""
@model_validator(mode="after")
def _validate_secret_key(self) -> "Settings":
env = (self.app_environment or "").strip().lower()
if env in ("production", "staging") and (
not self.secret_key or self.secret_key == _DEV_SECRET_KEY
):
raise ValueError(
"SECRET_KEY must be set to a strong random value in production/staging"
)
return self
@property
def is_production(self) -> bool:
return (self.app_environment or "").strip().lower() == "production"
@property
def enable_test_subscription(self) -> bool:
return not self.is_production
@property
def enable_test_plan(self) -> bool:
return not self.is_production
@property
def redis_url_resolved(self) -> str:
business, _ = resolve_redis_urls(
self.redis_url,
redis_password=self.redis_password or None,
celery_redis_url_override=None,
)
return business
@property
def celery_redis_url_resolved(self) -> str:
override = (self.celery_redis_url or "").strip() or None
_, celery = resolve_redis_urls(
self.redis_url,
redis_password=self.redis_password or None,
celery_redis_url_override=override,
)
return celery
class SettingsFacade:
"""Backward-compatible facade: secrets from Settings, deploy flags from TOML."""
__slots__ = ("_secrets",)
def __init__(self, secrets: Settings) -> None:
object.__setattr__(self, "_secrets", secrets)
@property
def _deploy(self) -> DeployConfig:
from app.core.app_config import get_app_config
return get_app_config().deploy
def __getattr__(self, name: str):
secrets = object.__getattribute__(self, "_secrets")
if hasattr(secrets, name):
return getattr(secrets, name)
if name in _DEPLOY_FIELD_NAMES:
return getattr(self._deploy, name)
raise AttributeError(f"Settings has no attribute {name!r}")
def __setattr__(self, name: str, value) -> None:
if name == "_secrets":
object.__setattr__(self, name, value)
return
secrets = object.__getattribute__(self, "_secrets")
if hasattr(type(secrets), name) and name not in _DEPLOY_FIELD_NAMES:
setattr(secrets, name, value)
return
if name in _DEPLOY_FIELD_NAMES:
setattr(self._deploy, name, value)
return
setattr(secrets, name, value)
settings = SettingsFacade(Settings())