配置 SSOT(TOML + .env) 统一错误契约 Auth 与事务边界 Redis / Celery 可靠性:业务 Redis(DB/0)与 Celery broker/backend(DB/1)显式拆分;连接池、sync client 可观测性(OpenTelemetry + LGTM)
138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
"""
|
||
统一配置:密钥与连接串经 .env / Settings;其余非密钥项见 config/*.toml(AppConfig)。
|
||
|
||
本地开发时由 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())
|