"""Application settings loaded from environment.""" from __future__ import annotations import os from functools import lru_cache from pathlib import Path from typing import Any from pydantic import Field, ValidationError from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Runtime configuration for the Telegram bot.""" bot_token: str = Field(alias="BOT_TOKEN", min_length=1) designapi_api_key: str = Field(alias="DESIGNAPI_API_KEY", min_length=1) designapi_base_url: str = Field(alias="DESIGNAPI_BASE_URL", default="https://api.designapi.ink", min_length=1) designapi_model: str = Field(alias="DESIGNAPI_MODEL", default="gpt-5.5", min_length=1) llm_max_tokens: int = Field(alias="LLM_MAX_TOKENS", default=600, ge=64, le=4096) db_path: str = Field(alias="DB_PATH", default="/workspace/.services/bot/data.db", min_length=1) health_host: str = Field(alias="HEALTH_HOST", default="0.0.0.0", min_length=1) health_port: int = Field(alias="HEALTH_PORT", default=8080, ge=1, le=65535) model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", populate_by_name=True, ) def _load_runtime_fallbacks() -> None: """Populate env from platform-compatible fallbacks without logging secrets. Older attempts in this shared workspace left BOT_TOKEN in bot_token.txt and exposed an OpenAI-compatible key as ANTHROPIC_API_KEY. Settings still prefer the canonical variables, but these fallbacks make restarts deterministic. """ if not os.getenv("BOT_TOKEN"): token_file = Path("/workspace/bot_token.txt") if token_file.exists(): token = token_file.read_text(encoding="utf-8").strip() if token: os.environ["BOT_TOKEN"] = token if not os.getenv("DESIGNAPI_API_KEY") and os.getenv("ANTHROPIC_API_KEY"): os.environ["DESIGNAPI_API_KEY"] = os.environ["ANTHROPIC_API_KEY"] if not os.getenv("DESIGNAPI_BASE_URL") and os.getenv("ANTHROPIC_BASE_URL"): base_url = os.environ["ANTHROPIC_BASE_URL"].strip().rstrip("/") if base_url.endswith("/v1"): base_url = base_url[:-3] if base_url: os.environ["DESIGNAPI_BASE_URL"] = base_url @lru_cache(maxsize=1) def get_settings() -> Settings: """Return cached settings or raise a readable validation error.""" _load_runtime_fallbacks() return Settings() # type: ignore[call-arg] def format_config_error(exc: ValidationError) -> str: """Make missing env diagnostics concise and operator-friendly.""" missing: list[str] = [] for err in exc.errors(): loc: tuple[Any, ...] = err.get("loc", ()) if err.get("type") == "missing" and loc: missing.append(str(loc[0])) if missing: return "Missing required environment variables: " + ", ".join(sorted(missing)) return str(exc)