"""Runtime settings, read once from the environment (and a local .env in dev).""" from __future__ import annotations from functools import lru_cache from pathlib import Path from pydantic import field_validator from pydantic_settings import BaseSettings, SettingsConfigDict _REPO_ROOT = Path(__file__).resolve().parents[3] """Repo root: backend/src/fintracker/config.py -> ../../.. — the dev `.env` lives there.""" class Settings(BaseSettings): model_config = SettingsConfigDict( # absolute first: recipes run from `backend/`, so a bare ".env" would never resolve. # A ".env" next to the process (docker image, other layouts) still wins if present. env_file=(_REPO_ROOT / ".env", ".env"), env_file_encoding="utf-8", extra="ignore", ) database_url: str = "postgresql+asyncpg://fintracker:fintracker@localhost:54329/fintracker" """SQLAlchemy async URL. Inside docker compose the host is `db`.""" jwt_secret: str = "dev-insecure-secret-change-me" access_token_ttl_seconds: int = 3600 refresh_token_ttl_seconds: int = 30 * 86400 login_rate_limit_per_minute: int = 5 cors_origins: str = "" """Comma-separated origins for the Flutter web build when it is NOT served by Caddy from the same origin (e.g. `flutter run -d chrome` during development).""" timezone: str = "Europe/Moscow" uploads_dir: Path = Path("/data/uploads") web_dir: Path | None = None """Optional Flutter web build to serve at `/` (dev convenience; Caddy does this in prod).""" log_level: str = "INFO" base_currency: str = "RUB" """Display/metric currency; native amounts are always kept alongside.""" # --- sources: secrets from .env; rotating refresh tokens live in source_credential --- zenmoney_token: str | None = None """Static access token (e.g. from zerro.app/token). Used when no OAuth client is configured.""" zenmoney_client_id: str | None = None zenmoney_client_secret: str | None = None zenmoney_refresh_token: str | None = None """Initial refresh token for OAuth rotation; later values are stored in source_credential.""" tinvest_token: str | None = None @field_validator( "web_dir", "zenmoney_token", "zenmoney_client_id", "zenmoney_client_secret", "zenmoney_refresh_token", "tinvest_token", mode="before", ) @classmethod def _blank_is_none(cls, v: object) -> object: """A blank line in `.env` (`WEB_DIR=`) means unset — not Path('.'), not an empty token.""" return None if isinstance(v, str) and not v.strip() else v @property def cors_origin_list(self) -> list[str]: return [o.strip() for o in self.cors_origins.split(",") if o.strip()] @property def is_dev_secret(self) -> bool: return self.jwt_secret.startswith("dev-insecure") @lru_cache def get_settings() -> Settings: return Settings()