from __future__ import annotations from functools import lru_cache from urllib.parse import quote_plus from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", ) app_name: str = "otel-bi-backend" app_env: str = "dev" log_level: str = "INFO" api_host: str = "0.0.0.0" api_port: int = 8000 cors_origins: str = "http://localhost:5173" request_timeout_seconds: float = 20.0 mssql_host: str = "localhost" mssql_port: int = 1433 mssql_username: str = "sa" mssql_password: str = "Password!123" mssql_driver: str = "ODBC Driver 18 for SQL Server" mssql_trust_server_certificate: bool = False wwi_database: str = "WorldWideImporters" aw_database: str = "AdventureWorks2022DWH" wwi_connection_string: str | None = None aw_connection_string: str | None = None postgres_host: str = "localhost" postgres_port: int = 5432 postgres_database: str = "otel_bi_app" postgres_username: str = "otel_bi_app" postgres_password: str = "otel_bi_app" postgres_sslmode: str = "require" postgres_connection_string: str | None = None postgres_required: bool = True query_service_url: str = "http://localhost:8101" analytics_service_url: str = "http://localhost:8102" persistence_service_url: str = "http://localhost:8103" require_frontend_auth: bool = True frontend_jwt_issuer_url: str = "" frontend_jwt_audience: str = "" frontend_jwt_jwks_url: str | None = None frontend_jwt_algorithm: str = "RS256" frontend_required_scopes: str = "" frontend_clock_skew_seconds: int = Field(default=30, ge=0, le=300) internal_service_auth_enabled: bool = True internal_service_shared_secret: str = "change-me" internal_service_token_ttl_seconds: int = Field(default=120, ge=30, le=900) internal_service_token_audience: str = "bi-internal" internal_service_allowed_issuers: str = "api-gateway" internal_token_clock_skew_seconds: int = Field(default=15, ge=0, le=120) otel_service_name: str = "otel-bi-backend" otel_service_namespace: str = "final-thesis" otel_collector_endpoint: str = "http://localhost:4318" otel_export_timeout_ms: int = 10000 forecast_horizon_days: int = Field(default=30, ge=7, le=180) default_history_days: int = Field(default=365, ge=30, le=1460) ranking_default_top_n: int = Field(default=10, ge=3, le=100) storage_default_limit: int = Field(default=50, ge=10, le=500) @property def cors_origins_list(self) -> list[str]: return [ origin.strip() for origin in self.cors_origins.split(",") if origin.strip() ] @property def frontend_required_scopes_list(self) -> list[str]: return [ scope.strip() for scope in self.frontend_required_scopes.split(" ") if scope.strip() ] @property def internal_service_allowed_issuers_list(self) -> list[str]: return [ issuer.strip() for issuer in self.internal_service_allowed_issuers.split(",") if issuer.strip() ] def _build_mssql_connection_url(self, database: str) -> str: driver = quote_plus(self.mssql_driver) user = quote_plus(self.mssql_username) password = quote_plus(self.mssql_password) trust_cert = "yes" if self.mssql_trust_server_certificate else "no" return ( f"mssql+pyodbc://{user}:{password}@{self.mssql_host}:{self.mssql_port}/{database}" f"?driver={driver}&TrustServerCertificate={trust_cert}&ApplicationIntent=ReadOnly" ) @property def wwi_connection_url(self) -> str: return self.wwi_connection_string or self._build_mssql_connection_url( self.wwi_database ) @property def aw_connection_url(self) -> str: return self.aw_connection_string or self._build_mssql_connection_url( self.aw_database ) @property def postgres_connection_url(self) -> str: if self.postgres_connection_string: return self.postgres_connection_string user = quote_plus(self.postgres_username) password = quote_plus(self.postgres_password) return ( f"postgresql+psycopg://{user}:{password}@{self.postgres_host}:{self.postgres_port}/" f"{self.postgres_database}?sslmode={self.postgres_sslmode}" ) @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()