"""Runtime settings, read from environment variables (and an optional .env file).""" from __future__ import annotations import os from dataclasses import dataclass from pathlib import Path from dotenv import load_dotenv PROJECT_ROOT = Path(__file__).resolve().parent.parent DEFAULT_BASE_URL = "https://api.devupai.com/v1" DEFAULT_MODEL = "deepseek-ai/DeepSeek-V4.1-Flash-1017" class ConfigError(RuntimeError): """Raised when a required setting is missing or invalid.""" def _int_env(name: str, default: int, minimum: int = 1) -> int: raw = os.getenv(name) if raw is None or raw.strip() == "": return default try: value = int(raw) except ValueError as exc: raise ConfigError(f"{name} must be an integer, got {raw!r}") from exc if value < minimum: raise ConfigError(f"{name} must be >= {minimum}, got {value}") return value @dataclass(frozen=True) class Settings: devup_api_key: str devup_base_url: str model: str telegram_bot_token: str | None admin_chat_id: int | None db_path: Path catalog_path: Path history_messages: int rate_limit_per_minute: int max_input_chars: int max_tool_rounds: int max_output_tokens: int request_timeout_s: int @classmethod def from_env(cls, *, require_api_key: bool = True, require_telegram: bool = False) -> Settings: load_dotenv(PROJECT_ROOT / ".env") api_key = os.getenv("DEVUP_API_KEY", "").strip() if require_api_key and not api_key: raise ConfigError("DEVUP_API_KEY is not set. Copy .env.example to .env and add your key.") token = os.getenv("TELEGRAM_BOT_TOKEN", "").strip() or None if require_telegram and not token: raise ConfigError("TELEGRAM_BOT_TOKEN is not set. Create a bot with @BotFather and add its token to .env.") admin_raw = os.getenv("ADMIN_CHAT_ID", "").strip() try: admin_chat_id = int(admin_raw) if admin_raw else None except ValueError as exc: raise ConfigError(f"ADMIN_CHAT_ID must be a numeric Telegram chat id, got {admin_raw!r}") from exc return cls( devup_api_key=api_key, devup_base_url=os.getenv("DEVUP_BASE_URL", DEFAULT_BASE_URL).strip() or DEFAULT_BASE_URL, model=os.getenv("DEVUP_MODEL", DEFAULT_MODEL).strip() or DEFAULT_MODEL, telegram_bot_token=token, admin_chat_id=admin_chat_id, db_path=Path(os.getenv("STORE_DB_PATH", str(PROJECT_ROOT / "data" / "store.db"))), catalog_path=Path(os.getenv("STORE_CATALOG_PATH", str(PROJECT_ROOT / "data" / "demo_store.json"))), history_messages=_int_env("HISTORY_MESSAGES", 12), rate_limit_per_minute=_int_env("RATE_LIMIT_PER_MINUTE", 8), max_input_chars=_int_env("MAX_INPUT_CHARS", 1000), max_tool_rounds=_int_env("MAX_TOOL_ROUNDS", 4), max_output_tokens=_int_env("MAX_OUTPUT_TOKENS", 8192), request_timeout_s=_int_env("REQUEST_TIMEOUT_S", 60), )