"""Application configuration, loaded from the environment and/or a .env file.""" from __future__ import annotations import secrets from functools import lru_cache from pathlib import Path from typing import Literal from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """Runtime configuration. Every variable is prefixed ``LEMBAS_``.""" model_config = SettingsConfigDict( env_prefix="LEMBAS_", env_file=".env", env_file_encoding="utf-8", extra="ignore", ) secret_key: str = Field(default="") # Set when no LEMBAS_SECRET_KEY was supplied and one had to be invented. # main.py warns about it at startup; see the validator below. secret_key_is_ephemeral: bool = Field(default=False, exclude=True) data_dir: Path = Path("./data") host: str = "127.0.0.1" port: int = 8080 reload: bool = False log_level: Literal["debug", "info", "warning", "error"] = "info" allow_signup: bool = True default_theme: Literal["moria", "shire"] = "moria" session_ttl: int = 60 * 60 * 24 * 30 request_timeout: float = 300.0 @model_validator(mode="after") def _generate_secret_if_absent(self) -> Settings: # A generated key lets `lembas serve` work with no configuration at all, # but it changes on every restart: sessions drop and stored API keys # become unreadable. Flagged so startup can warn. Never use in anger. if not self.secret_key: self.secret_key = secrets.token_urlsafe(48) self.secret_key_is_ephemeral = True return self @property def db_path(self) -> Path: return self.data_dir / "lembas.db" @property def uploads_dir(self) -> Path: return self.data_dir / "uploads" def ensure_dirs(self) -> None: self.data_dir.mkdir(parents=True, exist_ok=True) self.uploads_dir.mkdir(parents=True, exist_ok=True) @lru_cache def get_settings() -> Settings: """Cached singleton so config is parsed once per process.""" return Settings() settings = get_settings()