Publish LumaOps source
This commit is contained in:
commit
7f1c0e5f71
2363 files changed
+501543
No files matched your search
@@ -0,0 +1,117 @@
|
||||
"""Application configuration with deployment-safe validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Runtime configuration sourced from environment variables."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
case_sensitive=False,
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
app_name: str = "LumaOps"
|
||||
app_version: str = "0.1.0"
|
||||
app_host: str = "0.0.0.0"
|
||||
app_port: int = Field(default=8080, ge=1, le=65535)
|
||||
app_base_url: str = ""
|
||||
environment: Literal["development", "test", "production"] = Field(
|
||||
default="production", alias="LUMAOPS_ENV"
|
||||
)
|
||||
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
|
||||
timezone: str = Field(default="Europe/Brussels", alias="TZ")
|
||||
|
||||
openrgb_host: str = "127.0.0.1"
|
||||
openrgb_port: int = Field(default=6742, ge=1, le=65535)
|
||||
openrgb_config_dir: Path = Path("/config/openrgb")
|
||||
openrgb_connect_timeout: float = Field(default=2.0, gt=0, le=30)
|
||||
openrgb_command_timeout: float = Field(default=5.0, gt=0, le=60)
|
||||
openrgb_max_packet_size: int = Field(default=16 * 1024 * 1024, ge=1024, le=64 * 1024 * 1024)
|
||||
|
||||
config_dir: Path = Path("/config/lumaops")
|
||||
data_dir: Path = Path("/data")
|
||||
logs_dir: Path = Path("/logs")
|
||||
static_dir: Path = Path("/opt/lumaops/static")
|
||||
database_url: str = "sqlite:////data/lumaops.db"
|
||||
secret_key: str | None = Field(default=None, alias="LUMAOPS_SECRET_KEY")
|
||||
|
||||
enable_network_discovery: bool = True
|
||||
enable_home_assistant: bool = False
|
||||
enable_wled: bool = False
|
||||
connector_mode: Literal["openrgb", "mock"] = "openrgb"
|
||||
|
||||
auth_enabled: bool = True
|
||||
admin_token: str | None = Field(default=None, alias="LUMAOPS_ADMIN_TOKEN")
|
||||
external_access: bool = False
|
||||
trusted_proxies: str = ""
|
||||
cors_origins: str = ""
|
||||
secure_cookies: bool = True
|
||||
|
||||
command_rate_per_second: float = Field(default=10.0, gt=0, le=60)
|
||||
realtime_rate_per_second: float = Field(default=30.0, gt=0, le=120)
|
||||
command_timeout_seconds: float = Field(default=10.0, gt=0, le=120)
|
||||
health_retention_days: int = Field(default=30, ge=1, le=3650)
|
||||
|
||||
@field_validator("openrgb_host")
|
||||
@classmethod
|
||||
def validate_openrgb_host(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("OPENRGB_HOST mag niet leeg zijn")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_security_invariants(self) -> Settings:
|
||||
if self.environment == "production" and self.connector_mode == "mock":
|
||||
raise ValueError("MockOpenRGBAdapter is verboden in productie")
|
||||
if self.external_access and not self.auth_enabled:
|
||||
raise ValueError("AUTH_ENABLED is verplicht wanneer EXTERNAL_ACCESS=true")
|
||||
if self.environment == "production" and self.app_host not in {
|
||||
"127.0.0.1",
|
||||
"::1",
|
||||
"localhost",
|
||||
} and not self.auth_enabled:
|
||||
raise ValueError("AUTH_ENABLED is verplicht bij een niet-lokale productiebind")
|
||||
if self.auth_enabled:
|
||||
token = (self.admin_token or "").strip()
|
||||
placeholders = {
|
||||
"replace-with-a-long-random-token",
|
||||
"change-me",
|
||||
"changeme",
|
||||
}
|
||||
if len(token) < 32 or token.lower() in placeholders:
|
||||
raise ValueError(
|
||||
"LUMAOPS_ADMIN_TOKEN moet minstens 32 tekens lang en niet voorspelbaar zijn"
|
||||
)
|
||||
return self
|
||||
|
||||
@property
|
||||
def database_path(self) -> Path:
|
||||
prefix = "sqlite:///"
|
||||
if not self.database_url.startswith(prefix):
|
||||
raise ValueError("LumaOps MVP ondersteunt uitsluitend sqlite:/// DATABASE_URL waarden")
|
||||
raw = self.database_url[len(prefix) :]
|
||||
return Path(raw if raw else "lumaops.db").expanduser().resolve()
|
||||
|
||||
@property
|
||||
def trusted_proxy_list(self) -> tuple[str, ...]:
|
||||
return tuple(part.strip() for part in self.trusted_proxies.split(",") if part.strip())
|
||||
|
||||
@property
|
||||
def cors_origin_list(self) -> tuple[str, ...]:
|
||||
return tuple(part.strip() for part in self.cors_origins.split(",") if part.strip())
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
Reference in new issue
Block a user