"""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 # What `/admin/updates` compares against and the helper deploys. # # Deployment configuration and deliberately not instance settings: they # decide what code runs on this machine, and a value a web administrator # could edit would turn "you may deploy the channel" into "you may deploy # anything". `deploy/install.sh` writes both beside the rest. # # `stable` follows the newest release tag; `edge` follows the branch tip. # Stable is the default because a branch tip is not a release -- following # one means deploying whatever was pushed five minutes ago, which is right # for whoever is building this and wrong for whoever is running it. update_channel: Literal["stable", "edge"] = "stable" # Which branch is fetched, and which one `edge` follows. Stable needs it too: # a fetch has to name a branch, and tags come down with it. update_branch: str = "main" @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()