87 lines
3.3 KiB
Python
87 lines
3.3 KiB
Python
"""Runtime configuration loaded from environment variables.
|
|
|
|
Anything that varies between dev / staging / prod (storage URLs, queue
|
|
endpoints, auth secrets) lands here. Phase 0 only exposes the bare minimum
|
|
needed for `/health` and the app factory.
|
|
"""
|
|
|
|
from functools import lru_cache
|
|
|
|
from pydantic import Field, model_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_prefix="PDFENGINE_",
|
|
env_file=".env",
|
|
extra="ignore",
|
|
)
|
|
|
|
environment: str = Field(default="dev", description="dev | staging | prod")
|
|
engine_available: bool = Field(
|
|
default=False,
|
|
description=(
|
|
"True once the pybind11 module in bindings/python/ is importable. "
|
|
"Phase 0 default is False — routes that need the engine return 501."
|
|
),
|
|
)
|
|
|
|
render_cache_enabled: bool = Field(default=True)
|
|
render_cache_max_entries: int = Field(default=256)
|
|
render_cache_max_bytes: int = Field(
|
|
default=536_870_912,
|
|
description="Max total bytes for the tile render cache. Set to 0 to disable byte cap.",
|
|
)
|
|
render_cache_max_entry_bytes: int = Field(
|
|
default=8_388_608,
|
|
description="Max bytes for a single cache entry. Oversized tiles are rendered but not cached.",
|
|
)
|
|
|
|
gotenberg_url: str = Field(
|
|
default="http://127.0.0.1:3000",
|
|
description="URL for the Gotenberg headless microservice for high-fidelity Office->PDF.",
|
|
)
|
|
|
|
# CORS is explicit by default. A comma-separated string keeps deployment
|
|
# configuration ergonomic across Docker/PowerShell/systemd, while the
|
|
# property below gives the middleware a normalized list.
|
|
cors_allowed_origins: str = Field(
|
|
default="http://localhost:5173,http://127.0.0.1:5173",
|
|
description="Comma-separated browser origins allowed to call the gateway.",
|
|
)
|
|
cors_allow_credentials: bool = Field(
|
|
default=False,
|
|
description="Allow browser credentials only with an explicit origin allowlist.",
|
|
)
|
|
convert_max_concurrent: int = Field(
|
|
default=4,
|
|
description="Max concurrent conversion jobs executed simultaneously to prevent memory exhaustion.",
|
|
)
|
|
convert_queue_timeout_seconds: float = Field(
|
|
default=60.0,
|
|
description="Max seconds a request will wait in the queue before timing out with 504.",
|
|
)
|
|
|
|
@property
|
|
def cors_origins(self) -> list[str]:
|
|
"""Return normalized CORS origins, preserving ``*`` when explicitly set."""
|
|
return [origin.strip() for origin in self.cors_allowed_origins.split(",") if origin.strip()]
|
|
|
|
@model_validator(mode="after")
|
|
def validate_cors(self) -> "Settings":
|
|
"""Keep wildcard origins out of credentialed and production deployments."""
|
|
if self.cors_allow_credentials and "*" in self.cors_origins:
|
|
raise ValueError(
|
|
"PDFENGINE_CORS_ALLOWED_ORIGINS cannot contain '*' when "
|
|
"PDFENGINE_CORS_ALLOW_CREDENTIALS is enabled"
|
|
)
|
|
if self.environment.strip().lower() == "prod" and "*" in self.cors_origins:
|
|
raise ValueError("PDFENGINE_CORS_ALLOWED_ORIGINS cannot contain '*' in production")
|
|
return self
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def get_settings() -> Settings:
|
|
return Settings()
|