from pydantic import Field, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): app_name: str = Field(..., validation_alias="APP_NAME") app_env: str = Field(..., validation_alias="APP_ENV") app_port: int = Field(..., validation_alias="BACKEND_PORT") api_v1_prefix: str = Field(..., validation_alias="API_V1_PREFIX") auth_header_name: str = Field(..., validation_alias="AUTH_HEADER_NAME") api_keys_admin: str = Field(..., validation_alias="API_KEYS_ADMIN") api_keys_operator: str = Field(..., validation_alias="API_KEYS_OPERATOR") api_keys_viewer: str = Field(..., validation_alias="API_KEYS_VIEWER") postgres_host: str = Field(..., validation_alias="POSTGRES_HOST") postgres_port: int = Field(..., validation_alias="POSTGRES_PORT") postgres_db: str = Field(..., validation_alias="POSTGRES_DB") postgres_user: str = Field(..., validation_alias="POSTGRES_USER") postgres_password: str = Field(..., validation_alias="POSTGRES_PASSWORD") database_url_raw: str | None = Field(default=None, validation_alias="DATABASE_URL") svg_max_file_size_bytes: int = Field(10 * 1024 * 1024, validation_alias="SVG_MAX_FILE_SIZE_BYTES") svg_max_elements: int = Field(25000, validation_alias="SVG_MAX_ELEMENTS") svg_allow_internal_use_references_only: bool = Field(True, validation_alias="SVG_ALLOW_INTERNAL_USE_REFERENCES_ONLY") svg_forbid_foreign_object_v1: bool = Field(True, validation_alias="SVG_FORBID_FOREIGN_OBJECT_V1") svg_forbid_style_v1: bool = Field(False, validation_alias="SVG_FORBID_STYLE_V1") svg_forbid_image_v1: bool = Field(True, validation_alias="SVG_FORBID_IMAGE_V1") svg_display_enabled: bool = True svg_display_mode: str = "passthrough" svg_display_hide_small_text: bool = False svg_display_min_text_font_size: float = 8.0 svg_display_hide_technical_text: bool = False svg_display_remove_hidden_elements: bool = True svg_display_force_viewbox: bool = True svg_display_technical_text_patterns: str = "debug,tech,helper,tmp,service" storage_root_dir: str = Field(..., validation_alias="STORAGE_ROOT") publish_preview_retention_per_variant: int = 2 publish_require_full_pricing_coverage: bool = False model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) @model_validator(mode="after") def validate_database_config(self) -> "Settings": assembled_database_url = ( f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}" f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" ) if self.database_url_raw and self.database_url_raw != assembled_database_url: raise ValueError("DATABASE_URL must match POSTGRES_HOST/PORT/DB/USER/PASSWORD") return self @property def admin_keys(self) -> set[str]: return {item.strip() for item in self.api_keys_admin.split(",") if item.strip()} @property def operator_keys(self) -> set[str]: return {item.strip() for item in self.api_keys_operator.split(",") if item.strip()} @property def viewer_keys(self) -> set[str]: return {item.strip() for item in self.api_keys_viewer.split(",") if item.strip()} @property def database_url(self) -> str: if self.database_url_raw: return self.database_url_raw return ( f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}" f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" ) @property def storage_original_dir(self) -> str: return f"{self.storage_root_dir}/original" @property def storage_sanitized_dir(self) -> str: return f"{self.storage_root_dir}/sanitized" @property def storage_normalized_dir(self) -> str: return f"{self.storage_root_dir}/normalized" @property def storage_display_dir(self) -> str: return f"{self.storage_root_dir}/display" @property def storage_preview_dir(self) -> str: return f"{self.storage_root_dir}/preview" settings = Settings()