"""Typed configuration from the environment / `.env` — one settings class per subsystem (ARCHITECTURE §2.11).""" from pathlib import Path from typing import Annotated from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict _ENV = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") class TelegramSettings(BaseSettings): model_config = SettingsConfigDict(**_ENV, env_prefix="TELEGRAM_") bot_token: SecretStr class AccessSettings(BaseSettings): model_config = _ENV # Bootstrap admins (SPEC A-8). Comma-separated in the env: ADMIN_IDS=1,2 admin_ids: Annotated[frozenset[int], NoDecode] = frozenset() @field_validator("admin_ids", mode="before") @classmethod def _split_ids(cls, value: object) -> object: if isinstance(value, str): return frozenset(int(part) for part in value.replace(" ", "").split(",") if part) return value class SourceCraftSettings(BaseSettings): model_config = SettingsConfigDict(**_ENV, env_prefix="SOURCECRAFT_") token: SecretStr = SecretStr("") api_url: str = "https://api.sourcecraft.tech" uploads_repo: str = "rogachevegorushka/tgit-uploads" # Web link to a CI run. The SPA answers 200 on any path, so the format is unverified — adjust if it 404s in UI. run_url_template: str = "https://sourcecraft.dev/{repo}/cicd/runs/{run}" timeout: float = Field(default=30.0, gt=0) max_attempts: int = Field(default=5, ge=1) class RuntimeSettings(BaseSettings): model_config = _ENV log_level: str = "INFO" # Non-secret configs (texts, thresholds, prompts). Relative to the working dir: /srv/tgit in Docker. config_dir: Path = Path("config") # The heartbeat file is touched while the event loop is alive; the Docker healthcheck reads its age. heartbeat_file: Path = Path("/tmp/tgit-heartbeat") heartbeat_interval: float = Field(default=10.0, gt=0) heartbeat_max_age: float = Field(default=30.0, gt=0) class Settings(BaseSettings): model_config = _ENV telegram: TelegramSettings = Field(default_factory=TelegramSettings) access: AccessSettings = Field(default_factory=AccessSettings) sourcecraft: SourceCraftSettings = Field(default_factory=SourceCraftSettings) runtime: RuntimeSettings = Field(default_factory=RuntimeSettings)