from __future__ import annotations import os from dataclasses import dataclass from functools import lru_cache @dataclass(frozen=True) class Settings: app_base_url: str | None auth_secret_key: str | None auth_cookie_secure: bool cors_allow_origins: tuple[str, ...] forgejo_base_url: str forgejo_token: str | None forgejo_oauth_client_id: str | None forgejo_oauth_client_secret: str | None forgejo_oauth_scopes: tuple[str, ...] forgejo_general_discussion_repo: str | None forgejo_webhook_secret: str | None forgejo_repo_scan_limit: int forgejo_recent_issue_limit: int forgejo_cache_ttl_seconds: float forgejo_request_timeout_seconds: float calendar_feed_urls: tuple[str, ...] calendar_event_limit: int def _normalize_base_url(raw_value: str | None) -> str: value = (raw_value or "").strip() if not value: return "https://aksal.cloud" if value.startswith(("http://", "https://")): return value.rstrip("/") return f"https://{value.rstrip('/')}" def _parse_calendar_feed_urls(raw_value: str | None) -> tuple[str, ...]: return _parse_csv_values(raw_value) def _parse_csv_values(raw_value: str | None) -> tuple[str, ...]: value = (raw_value or "").strip() if not value: return () return tuple(entry.strip() for entry in value.replace("\n", ",").split(",") if entry.strip()) def _parse_scopes(raw_value: str | None) -> tuple[str, ...]: value = (raw_value or "").strip() if not value: return ("openid", "profile") return tuple(scope for scope in value.replace(",", " ").split() if scope) def _parse_bool(raw_value: str | None, *, default: bool = False) -> bool: value = (raw_value or "").strip().lower() if not value: return default return value in {"1", "true", "yes", "on"} def _cors_allow_origins(app_base_url: str | None, raw_value: str | None) -> tuple[str, ...]: configured_origins = _parse_csv_values(raw_value) if configured_origins: return configured_origins if app_base_url: return (app_base_url,) return ("*",) @lru_cache(maxsize=1) def get_settings() -> Settings: app_base_url = ( _normalize_base_url(os.getenv("APP_BASE_URL")) if os.getenv("APP_BASE_URL") else None ) return Settings( app_base_url=app_base_url, auth_secret_key=os.getenv("AUTH_SECRET_KEY") or None, auth_cookie_secure=_parse_bool(os.getenv("AUTH_COOKIE_SECURE")), cors_allow_origins=_cors_allow_origins(app_base_url, os.getenv("CORS_ALLOW_ORIGINS")), forgejo_base_url=_normalize_base_url(os.getenv("FORGEJO_BASE_URL")), forgejo_token=os.getenv("FORGEJO_TOKEN") or None, forgejo_oauth_client_id=os.getenv("FORGEJO_OAUTH_CLIENT_ID") or None, forgejo_oauth_client_secret=os.getenv("FORGEJO_OAUTH_CLIENT_SECRET") or None, forgejo_oauth_scopes=_parse_scopes(os.getenv("FORGEJO_OAUTH_SCOPES")), forgejo_general_discussion_repo=os.getenv("FORGEJO_GENERAL_DISCUSSION_REPO") or None, forgejo_webhook_secret=os.getenv("FORGEJO_WEBHOOK_SECRET") or None, forgejo_repo_scan_limit=int(os.getenv("FORGEJO_REPO_SCAN_LIMIT", "30")), forgejo_recent_issue_limit=int(os.getenv("FORGEJO_RECENT_ISSUE_LIMIT", "50")), forgejo_cache_ttl_seconds=float(os.getenv("FORGEJO_CACHE_TTL_SECONDS", "60.0")), forgejo_request_timeout_seconds=float( os.getenv("FORGEJO_REQUEST_TIMEOUT_SECONDS", "10.0"), ), calendar_feed_urls=_parse_calendar_feed_urls(os.getenv("CALENDAR_FEED_URLS")), calendar_event_limit=int(os.getenv("CALENDAR_EVENT_LIMIT", "3")), )