from __future__ import annotations import functools from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Application settings loaded from environment variables. Required variables must be present at startup. Optional variables have sensible defaults. No .env files are used — all values come from os.environ. """ model_config = SettingsConfigDict( env_file="", env_file_encoding="utf-8", case_sensitive=True, extra="ignore", ) BOT_TOKEN: str | None = None OPENAI_API_KEY: str | None = None ADMIN_ID: str | None = None ADMIN_USERNAME: str = "" PORT: int = Field(default=7860, ge=1, le=65535) SUPPORT_API_KEY: str = "" YOOKASSA_SECRET_KEY: str = "" YOOKASSA_SHOP_ID: str = "" DATABASE_URL: str = "" TELEGRAM_API_SERVER: str = "https://api.telegram.org" WEBHOOK_URL: str = "" DEFAULT_PROVIDER: Literal["openai", "claude", "gemini", "glm", "qwen"] = "openai" LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO" LOG_FORMAT: Literal["json", "console"] = "json" @property def required_fields(self) -> dict[str, str | None]: """Return mapping of required field names to their current values.""" return { "BOT_TOKEN": self.BOT_TOKEN, "OPENAI_API_KEY": self.OPENAI_API_KEY, "ADMIN_ID": self.ADMIN_ID, } @functools.lru_cache(maxsize=1) def get_settings() -> Settings: """Return the cached Settings singleton. Lazy initialization — Settings() is created only on the first call, not at import time. This avoids import-time side effects and allows test fixtures to override environment variables before the first call. To reset the cache in tests, call: get_settings.cache_clear() """ return Settings()