from __future__ import annotations from functools import lru_cache from ipaddress import IPv4Network, IPv6Network, ip_network from pathlib import Path from typing import Literal from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict PROJECT_ROOT = Path(__file__).resolve().parents[2] class AppSettings(BaseSettings): model_config = SettingsConfigDict( env_file=PROJECT_ROOT / ".env", env_prefix="ROTATOR_", extra="ignore", ) app_name: str = "FluxIP" environment: Literal["development", "test", "production"] = "development" host: str = "127.0.0.1" port: int = Field(default=8787, ge=1, le=65535) database_path: Path = PROJECT_ROOT / "data" / "fluxip.db" master_key_file: Path = PROJECT_ROOT / "data" / "master.key" master_key: str | None = None bootstrap_token: SecretStr | None = None cookie_secure: bool = False session_days: int = Field(default=7, ge=1, le=90) allowed_origins: str = "http://localhost:8787,http://127.0.0.1:8787" trusted_hosts: str = "localhost,127.0.0.1" trusted_proxies: str = "127.0.0.1,::1" scheduler_enabled: bool = True scheduler_tick_seconds: int = Field(default=10, ge=2, le=60) log_level: str = "INFO" external_timeout_seconds: int = Field(default=20, ge=5, le=60) @field_validator("database_path", "master_key_file", mode="before") @classmethod def resolve_path(cls, value: str | Path) -> Path: path = Path(value) return path if path.is_absolute() else PROJECT_ROOT / path @field_validator("bootstrap_token", mode="before") @classmethod def normalize_optional_secret(cls, value: object) -> object: return None if value is None or not str(value).strip() else value @field_validator("trusted_proxies") @classmethod def validate_trusted_proxies(cls, value: str) -> str: for item in value.split(","): if item.strip(): ip_network(item.strip(), strict=False) return value @property def origins(self) -> list[str]: return [value.strip() for value in self.allowed_origins.split(",") if value.strip()] @property def hosts(self) -> list[str]: return [value.strip() for value in self.trusted_hosts.split(",") if value.strip()] @property def proxy_networks(self) -> tuple[IPv4Network | IPv6Network, ...]: return tuple( ip_network(value.strip(), strict=False) for value in self.trusted_proxies.split(",") if value.strip() ) @lru_cache def get_settings() -> AppSettings: return AppSettings()