Files
life-echo/api/app/core/config.py

139 lines
4.2 KiB
Python
Raw Normal View History

"""
统一配置密钥与连接串经 .env / Settings其余非密钥项见 config/*.tomlAppConfig
2026-03-23 13:21:07 +08:00
本地开发时由 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 = ""
tencent_app_id: 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())