548 lines
26 KiB
Python
548 lines
26 KiB
Python
from urllib.parse import urlsplit
|
|
|
|
from pydantic import AliasChoices, Field, field_validator, model_validator
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(
|
|
env_file=".env",
|
|
env_file_encoding="utf-8",
|
|
extra="ignore",
|
|
populate_by_name=True,
|
|
)
|
|
|
|
app_env: str = Field(default="development", validation_alias="GEOINTEL_ENV")
|
|
app_version: str = Field(
|
|
default="1.0.0",
|
|
validation_alias="GEOINTEL_APP_VERSION",
|
|
)
|
|
build_sha: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_SHA")
|
|
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
|
|
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
|
|
auth_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AUTH_ENABLED")
|
|
auth_username: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_USERNAME")
|
|
auth_password_hash: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_PASSWORD_HASH")
|
|
auth_session_secret: str | None = Field(default=None, validation_alias="GEOINTEL_AUTH_SESSION_SECRET")
|
|
authentik_issuer: str | None = Field(default=None, validation_alias="GEOINTEL_AUTHENTIK_ISSUER")
|
|
authentik_client_id: str | None = Field(default=None, validation_alias="GEOINTEL_AUTHENTIK_CLIENT_ID")
|
|
authentik_client_secret: str | None = Field(default=None, validation_alias="GEOINTEL_AUTHENTIK_CLIENT_SECRET")
|
|
authentik_allowed_email: str | None = Field(default=None, validation_alias="GEOINTEL_AUTHENTIK_ALLOWED_EMAIL")
|
|
public_base_url: str = Field(
|
|
default="http://localhost:1202",
|
|
validation_alias="GEOINTEL_PUBLIC_BASE_URL",
|
|
)
|
|
auth_session_ttl_seconds: int = Field(
|
|
default=43_200,
|
|
ge=900,
|
|
le=604_800,
|
|
validation_alias="GEOINTEL_AUTH_SESSION_TTL_SECONDS",
|
|
)
|
|
guest_access_enabled: bool = Field(
|
|
default=True,
|
|
validation_alias="GEOINTEL_GUEST_ACCESS_ENABLED",
|
|
)
|
|
guest_display_name: str = Field(
|
|
default="Gast",
|
|
min_length=1,
|
|
max_length=64,
|
|
validation_alias="GEOINTEL_GUEST_DISPLAY_NAME",
|
|
)
|
|
guest_session_ttl_seconds: int = Field(
|
|
default=7_200,
|
|
ge=900,
|
|
le=86_400,
|
|
validation_alias="GEOINTEL_GUEST_SESSION_TTL_SECONDS",
|
|
)
|
|
database_url: str = Field(
|
|
default="postgresql+psycopg://geointel:geointel@localhost:5432/geointel?connect_timeout=1",
|
|
validation_alias="DATABASE_URL",
|
|
)
|
|
storage_root: str = Field(default="./storage", validation_alias="STORAGE_ROOT")
|
|
# Analysis consumes only artifacts under storage_root. Provisioning
|
|
# workflows that stage tiles elsewhere before ingest can opt out.
|
|
allow_external_artifact_paths: bool = Field(
|
|
default=False, validation_alias="GEOINTEL_ALLOW_EXTERNAL_ARTIFACT_PATHS"
|
|
)
|
|
max_upload_mb: int = Field(
|
|
default=500,
|
|
ge=1,
|
|
le=2_048,
|
|
validation_alias=AliasChoices("GEOINTEL_MAX_UPLOAD_MB", "MAX_UPLOAD_MB"),
|
|
)
|
|
max_in_memory_vector_mb: int = Field(
|
|
default=64,
|
|
ge=1,
|
|
le=256,
|
|
validation_alias="GEOINTEL_MAX_IN_MEMORY_VECTOR_MB",
|
|
)
|
|
orthophoto_enabled: bool = Field(default=True, validation_alias="ORTHOPHOTO_ENABLED")
|
|
orthophoto_wms_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms",
|
|
validation_alias="ORTHOPHOTO_WMS_URL",
|
|
)
|
|
orthophoto_wms_layer: str = Field(default="Ortho", validation_alias="ORTHOPHOTO_WMS_LAYER")
|
|
spw_orthophoto_wms_url: str = Field(
|
|
default="https://geoservices.wallonie.be/arcgis/services/IMAGERIE/ORTHO_LAST/MapServer/WMSServer",
|
|
validation_alias="SPW_ORTHOPHOTO_WMS_URL",
|
|
)
|
|
brussels_orthophoto_wms_url: str = Field(
|
|
default="https://geoservices-grid.irisnet.be/geoserver/urbisgrid/ows",
|
|
validation_alias="BRUSSELS_ORTHOPHOTO_WMS_URL",
|
|
)
|
|
orthophoto_resolution_m: float = Field(default=1.0, gt=0, validation_alias="ORTHOPHOTO_RESOLUTION_M")
|
|
orthophoto_min_side_m: float = Field(default=128.0, gt=0, validation_alias="ORTHOPHOTO_MIN_SIDE_M")
|
|
orthophoto_max_side_m: float = Field(default=1024.0, gt=0, validation_alias="ORTHOPHOTO_MAX_SIDE_M")
|
|
orthophoto_timeout_seconds: int = Field(default=120, ge=1, validation_alias="ORTHOPHOTO_TIMEOUT_SECONDS")
|
|
orthophoto_max_response_mb: int = Field(default=32, ge=1, validation_alias="ORTHOPHOTO_MAX_RESPONSE_MB")
|
|
orthophoto_cache_ttl_hours: int = Field(default=24, ge=0, validation_alias="ORTHOPHOTO_CACHE_TTL_HOURS")
|
|
source_catalog_probe_enabled: bool = Field(default=True, validation_alias="SOURCE_CATALOG_PROBE_ENABLED")
|
|
source_catalog_grb_wfs_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/GRB/wfs",
|
|
validation_alias="SOURCE_CATALOG_GRB_WFS_URL",
|
|
)
|
|
source_catalog_alz_release_url: str = Field(
|
|
default="https://landbouwcijfers.vlaanderen.be/open-geodata-landbouwgebruikspercelen",
|
|
validation_alias="SOURCE_CATALOG_ALZ_RELEASE_URL",
|
|
)
|
|
source_catalog_statbel_dcat_url: str = Field(
|
|
default="https://doc.statbel.be/publications/DCAT/DCAT_opendata_datasets.ttl",
|
|
validation_alias="SOURCE_CATALOG_STATBEL_DCAT_URL",
|
|
)
|
|
source_catalog_statbel_max_response_mb: int = Field(
|
|
default=5,
|
|
ge=1,
|
|
le=10,
|
|
validation_alias="SOURCE_CATALOG_STATBEL_MAX_RESPONSE_MB",
|
|
)
|
|
source_catalog_probe_timeout_seconds: int = Field(
|
|
default=10,
|
|
ge=1,
|
|
le=60,
|
|
validation_alias="SOURCE_CATALOG_PROBE_TIMEOUT_SECONDS",
|
|
)
|
|
source_catalog_probe_max_response_mb: int = Field(
|
|
default=2,
|
|
ge=1,
|
|
le=10,
|
|
validation_alias="SOURCE_CATALOG_PROBE_MAX_RESPONSE_MB",
|
|
)
|
|
source_catalog_probe_cache_ttl_seconds: int = Field(
|
|
default=900,
|
|
ge=0,
|
|
le=86_400,
|
|
validation_alias="SOURCE_CATALOG_PROBE_CACHE_TTL_SECONDS",
|
|
)
|
|
grb_enabled: bool = Field(default=True, validation_alias="GRB_ENABLED")
|
|
grb_ogc_api_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/GRB/ogc/features/v1",
|
|
validation_alias="GRB_OGC_API_URL",
|
|
)
|
|
grb_min_side_m: float = Field(default=10.0, gt=0, validation_alias="GRB_MIN_SIDE_M")
|
|
grb_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="GRB_MAX_SIDE_M")
|
|
grb_page_size: int = Field(default=1000, ge=1, le=1000, validation_alias="GRB_PAGE_SIZE")
|
|
grb_max_pages: int = Field(default=200, ge=1, le=1000, validation_alias="GRB_MAX_PAGES")
|
|
grb_max_features: int = Field(default=150_000, ge=1, validation_alias="GRB_MAX_FEATURES")
|
|
grb_timeout_seconds: int = Field(default=180, ge=1, le=600, validation_alias="GRB_TIMEOUT_SECONDS")
|
|
grb_max_response_mb: int = Field(default=20, ge=1, le=100, validation_alias="GRB_MAX_RESPONSE_MB")
|
|
grb_max_total_response_mb: int = Field(
|
|
default=256,
|
|
ge=1,
|
|
le=2048,
|
|
validation_alias="GRB_MAX_TOTAL_RESPONSE_MB",
|
|
)
|
|
grb_cache_ttl_hours: int = Field(default=24, ge=0, le=8760, validation_alias="GRB_CACHE_TTL_HOURS")
|
|
official_vector_enabled: bool = Field(default=True, validation_alias="OFFICIAL_VECTOR_ENABLED")
|
|
bwk_wfs_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/BWK/wfs",
|
|
validation_alias="BWK_WFS_URL",
|
|
)
|
|
dov_soil_wfs_url: str = Field(
|
|
default="https://www.dov.vlaanderen.be/geoserver/wfs",
|
|
validation_alias="DOV_SOIL_WFS_URL",
|
|
)
|
|
official_vector_min_side_m: float = Field(
|
|
default=10.0,
|
|
gt=0,
|
|
validation_alias="OFFICIAL_VECTOR_MIN_SIDE_M",
|
|
)
|
|
official_vector_max_side_m: float = Field(
|
|
default=20_000.0,
|
|
gt=0,
|
|
validation_alias="OFFICIAL_VECTOR_MAX_SIDE_M",
|
|
)
|
|
official_vector_page_size: int = Field(
|
|
default=1000,
|
|
ge=1,
|
|
le=2000,
|
|
validation_alias="OFFICIAL_VECTOR_PAGE_SIZE",
|
|
)
|
|
official_vector_max_pages: int = Field(
|
|
default=200,
|
|
ge=1,
|
|
le=1000,
|
|
validation_alias="OFFICIAL_VECTOR_MAX_PAGES",
|
|
)
|
|
official_vector_max_features: int = Field(
|
|
default=100_000,
|
|
ge=1,
|
|
validation_alias="OFFICIAL_VECTOR_MAX_FEATURES",
|
|
)
|
|
official_vector_timeout_seconds: int = Field(
|
|
default=180,
|
|
ge=1,
|
|
le=600,
|
|
validation_alias="OFFICIAL_VECTOR_TIMEOUT_SECONDS",
|
|
)
|
|
official_vector_max_response_mb: int = Field(
|
|
default=20,
|
|
ge=1,
|
|
le=100,
|
|
validation_alias="OFFICIAL_VECTOR_MAX_RESPONSE_MB",
|
|
)
|
|
official_vector_max_total_response_mb: int = Field(
|
|
default=256,
|
|
ge=1,
|
|
le=2048,
|
|
validation_alias="OFFICIAL_VECTOR_MAX_TOTAL_RESPONSE_MB",
|
|
)
|
|
official_vector_cache_ttl_hours: int = Field(
|
|
default=24,
|
|
ge=0,
|
|
le=8760,
|
|
validation_alias="OFFICIAL_VECTOR_CACHE_TTL_HOURS",
|
|
)
|
|
spw_picc_enabled: bool = Field(default=True, validation_alias="SPW_PICC_ENABLED")
|
|
spw_picc_mapserver_url: str = Field(
|
|
default=(
|
|
"https://geoservices.wallonie.be/arcgis/rest/services/"
|
|
"TOPOGRAPHIE/PICC_VDIFF/MapServer"
|
|
),
|
|
validation_alias="SPW_PICC_MAPSERVER_URL",
|
|
)
|
|
spw_flood_hazard_enabled: bool = Field(default=True, validation_alias="SPW_FLOOD_HAZARD_ENABLED")
|
|
spw_flood_hazard_mapserver_url: str = Field(
|
|
default=(
|
|
"https://geoservices.wallonie.be/arcgis/rest/services/"
|
|
"EAU/ALEA_INOND/MapServer"
|
|
),
|
|
validation_alias="SPW_FLOOD_HAZARD_MAPSERVER_URL",
|
|
)
|
|
urbis_enabled: bool = Field(default=True, validation_alias="URBIS_ENABLED")
|
|
urbis_wfs_url: str = Field(
|
|
default="https://geoservices-vector.irisnet.be/geoserver/urbisvector/ows",
|
|
validation_alias="URBIS_WFS_URL",
|
|
)
|
|
dhmv_enabled: bool = Field(default=True, validation_alias="DHMV_ENABLED")
|
|
dhmv_wcs_url: str = Field(
|
|
default="https://geo.api.vlaanderen.be/DHMV/wcs",
|
|
validation_alias="DHMV_WCS_URL",
|
|
)
|
|
dhmv_resolution_m: float = Field(default=5.0, ge=1.0, le=10.0, validation_alias="DHMV_RESOLUTION_M")
|
|
dhmv_min_side_m: float = Field(default=10.0, gt=0, validation_alias="DHMV_MIN_SIDE_M")
|
|
dhmv_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="DHMV_MAX_SIDE_M")
|
|
dhmv_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="DHMV_MAX_PIXELS")
|
|
dhmv_timeout_seconds: int = Field(default=300, ge=1, validation_alias="DHMV_TIMEOUT_SECONDS")
|
|
dhmv_max_response_mb: int = Field(default=160, ge=1, validation_alias="DHMV_MAX_RESPONSE_MB")
|
|
flood_hazard_enabled: bool = Field(default=True, validation_alias="FLOOD_HAZARD_ENABLED")
|
|
flood_hazard_wcs_url: str = Field(
|
|
default="https://geoservice.waterinfo.be/OGRK/wcs",
|
|
validation_alias="FLOOD_HAZARD_WCS_URL",
|
|
)
|
|
flood_hazard_resolution_m: float = Field(default=5.0, ge=2.0, le=20.0, validation_alias="FLOOD_HAZARD_RESOLUTION_M")
|
|
flood_hazard_min_side_m: float = Field(default=10.0, gt=0, validation_alias="FLOOD_HAZARD_MIN_SIDE_M")
|
|
flood_hazard_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="FLOOD_HAZARD_MAX_SIDE_M")
|
|
flood_hazard_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="FLOOD_HAZARD_MAX_PIXELS")
|
|
flood_hazard_timeout_seconds: int = Field(default=300, ge=1, validation_alias="FLOOD_HAZARD_TIMEOUT_SECONDS")
|
|
flood_hazard_max_response_mb: int = Field(default=160, ge=1, validation_alias="FLOOD_HAZARD_MAX_RESPONSE_MB")
|
|
bathymetry_profiles_enabled: bool = Field(default=True, validation_alias="BATHYMETRY_PROFILES_ENABLED")
|
|
bathymetry_profiles_layer_url: str = Field(
|
|
default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/0",
|
|
validation_alias="BATHYMETRY_PROFILES_LAYER_URL",
|
|
)
|
|
bathymetry_watercourse_layer_url: str = Field(
|
|
default="https://vha.waterinfo.be/arcgis/rest/services/digitale_atlas/MapServer/1",
|
|
validation_alias="BATHYMETRY_WATERCOURSE_LAYER_URL",
|
|
)
|
|
bathymetry_profiles_page_size: int = Field(
|
|
default=1000,
|
|
ge=1,
|
|
le=2000,
|
|
validation_alias="BATHYMETRY_PROFILES_PAGE_SIZE",
|
|
)
|
|
bathymetry_profiles_max_features: int = Field(
|
|
default=50_000,
|
|
ge=1,
|
|
le=250_000,
|
|
validation_alias="BATHYMETRY_PROFILES_MAX_FEATURES",
|
|
)
|
|
bathymetry_profiles_max_pages: int = Field(
|
|
default=200,
|
|
ge=1,
|
|
le=5_000,
|
|
validation_alias="BATHYMETRY_PROFILES_MAX_PAGES",
|
|
)
|
|
bathymetry_profiles_timeout_seconds: int = Field(
|
|
default=120,
|
|
ge=1,
|
|
le=600,
|
|
validation_alias="BATHYMETRY_PROFILES_TIMEOUT_SECONDS",
|
|
)
|
|
bathymetry_profiles_max_response_mb: int = Field(
|
|
default=32,
|
|
ge=1,
|
|
le=256,
|
|
validation_alias="BATHYMETRY_PROFILES_MAX_RESPONSE_MB",
|
|
)
|
|
bathymetry_raster_max_pixels: int = Field(
|
|
default=30_000_000,
|
|
ge=1,
|
|
validation_alias="BATHYMETRY_RASTER_MAX_PIXELS",
|
|
)
|
|
mdk_bathymetry_probe_enabled: bool = Field(default=True, validation_alias="MDK_BATHYMETRY_PROBE_ENABLED")
|
|
mdk_bathymetry_wcs_url: str = Field(
|
|
default="https://bathy.agentschapmdk.be/spatialfusionserver/services/ows/wcs/EL_wcs",
|
|
validation_alias="MDK_BATHYMETRY_WCS_URL",
|
|
)
|
|
mdk_bathymetry_probe_timeout_seconds: int = Field(
|
|
default=20,
|
|
ge=1,
|
|
le=120,
|
|
validation_alias="MDK_BATHYMETRY_PROBE_TIMEOUT_SECONDS",
|
|
)
|
|
mdk_bathymetry_probe_max_response_mb: int = Field(
|
|
default=4,
|
|
ge=1,
|
|
le=16,
|
|
validation_alias="MDK_BATHYMETRY_PROBE_MAX_RESPONSE_MB",
|
|
)
|
|
thematic_raster_enabled: bool = Field(default=True, validation_alias="THEMATIC_RASTER_ENABLED")
|
|
thematic_raster_wcs_url: str = Field(
|
|
default="https://www.mercator.vlaanderen.be/raadpleegdienstenmercatorpubliek/wcs",
|
|
validation_alias="THEMATIC_RASTER_WCS_URL",
|
|
)
|
|
mdk_bathymetry_acquisition_enabled: bool = Field(
|
|
default=False,
|
|
validation_alias="MDK_BATHYMETRY_ACQUISITION_ENABLED",
|
|
)
|
|
mdk_bathymetry_coverage_id: str | None = Field(default=None, validation_alias="MDK_BATHYMETRY_COVERAGE_ID")
|
|
mdk_bathymetry_request_crs: str = Field(default="EPSG:4326", validation_alias="MDK_BATHYMETRY_REQUEST_CRS")
|
|
mdk_bathymetry_max_bbox_deg2: float = Field(
|
|
default=0.25,
|
|
gt=0,
|
|
validation_alias="MDK_BATHYMETRY_MAX_BBOX_DEG2",
|
|
)
|
|
mdk_bathymetry_acquisition_timeout_seconds: int = Field(
|
|
default=120,
|
|
ge=1,
|
|
validation_alias="MDK_BATHYMETRY_ACQUISITION_TIMEOUT_SECONDS",
|
|
)
|
|
mdk_bathymetry_acquisition_max_response_mb: int = Field(
|
|
default=160,
|
|
ge=1,
|
|
validation_alias="MDK_BATHYMETRY_ACQUISITION_MAX_RESPONSE_MB",
|
|
)
|
|
thematic_raster_min_side_m: float = Field(default=100.0, gt=0, validation_alias="THEMATIC_RASTER_MIN_SIDE_M")
|
|
thematic_raster_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="THEMATIC_RASTER_MAX_SIDE_M")
|
|
thematic_raster_max_pixels: int = Field(default=30_000_000, ge=1, validation_alias="THEMATIC_RASTER_MAX_PIXELS")
|
|
thematic_raster_timeout_seconds: int = Field(default=300, ge=1, validation_alias="THEMATIC_RASTER_TIMEOUT_SECONDS")
|
|
thematic_raster_max_response_mb: int = Field(default=160, ge=1, validation_alias="THEMATIC_RASTER_MAX_RESPONSE_MB")
|
|
walous_enabled: bool = Field(default=True, validation_alias="WALOUS_ENABLED")
|
|
walous_source_dir: str = Field(
|
|
default="/app/storage/source-cache/walous",
|
|
validation_alias="WALOUS_SOURCE_DIR",
|
|
)
|
|
walous_analysis_resolution_m: float = Field(
|
|
default=10.0,
|
|
ge=1.0,
|
|
le=100.0,
|
|
validation_alias="WALOUS_ANALYSIS_RESOLUTION_M",
|
|
)
|
|
walous_max_side_m: float = Field(default=60_000.0, gt=0, validation_alias="WALOUS_MAX_SIDE_M")
|
|
walous_max_pixels: int = Field(default=36_000_000, ge=1, validation_alias="WALOUS_MAX_PIXELS")
|
|
spw_terrain_enabled: bool = Field(default=True, validation_alias="SPW_TERRAIN_ENABLED")
|
|
spw_terrain_source_dir: str = Field(
|
|
default="/app/storage/source-cache/spw-terrain",
|
|
validation_alias="SPW_TERRAIN_SOURCE_DIR",
|
|
)
|
|
spw_terrain_analysis_resolution_m: float = Field(
|
|
default=5.0,
|
|
ge=1.0,
|
|
le=10.0,
|
|
validation_alias="SPW_TERRAIN_ANALYSIS_RESOLUTION_M",
|
|
)
|
|
spw_terrain_max_side_m: float = Field(default=20_000.0, gt=0, validation_alias="SPW_TERRAIN_MAX_SIDE_M")
|
|
spw_terrain_max_pixels: int = Field(default=12_000_000, ge=1, validation_alias="SPW_TERRAIN_MAX_PIXELS")
|
|
redis_url: str | None = Field(default=None, validation_alias="REDIS_URL")
|
|
log_level: str = Field(default="INFO", validation_alias="GEOINTEL_LOG_LEVEL")
|
|
sql_log_level: str = Field(default="WARNING", validation_alias="GEOINTEL_SQL_LOG_LEVEL")
|
|
reconcile_interrupted_runs_on_startup: bool = Field(
|
|
default=False,
|
|
validation_alias="GEOINTEL_RECONCILE_INTERRUPTED_RUNS_ON_STARTUP",
|
|
)
|
|
aoi_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_AOI_WORKER_ENABLED")
|
|
aoi_worker_poll_seconds: float = Field(default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_AOI_WORKER_POLL_SECONDS")
|
|
# Executes queued detection.run / segmentation.run jobs so tiled GPU
|
|
# inference never blocks an HTTP request.
|
|
analysis_worker_enabled: bool = Field(default=False, validation_alias="GEOINTEL_ANALYSIS_WORKER_ENABLED")
|
|
analysis_worker_poll_seconds: float = Field(
|
|
default=2.0, ge=0.5, le=60.0, validation_alias="GEOINTEL_ANALYSIS_WORKER_POLL_SECONDS"
|
|
)
|
|
database_statement_timeout_ms: int = Field(default=5_000, validation_alias="DATABASE_STATEMENT_TIMEOUT_MS")
|
|
yolo_enabled: bool = Field(default=False, validation_alias="YOLO_ENABLED")
|
|
yolo_models_dir: str = Field(default="/app/models", validation_alias="YOLO_MODELS_DIR")
|
|
yolo_model_path: str | None = Field(default=None, validation_alias="YOLO_MODEL_PATH")
|
|
yolo_model_id: str = Field(default="yolo-configured", validation_alias="YOLO_MODEL_ID")
|
|
yolo_model_display_name: str = Field(default="Configured YOLO detector", validation_alias="YOLO_MODEL_DISPLAY_NAME")
|
|
yolo_model_version: str | None = Field(default=None, validation_alias="YOLO_MODEL_VERSION")
|
|
yolo_model_classes: str = Field(default="building", validation_alias="YOLO_MODEL_CLASSES")
|
|
yolo_enforce_validation_scope: bool = Field(default=False, validation_alias="YOLO_ENFORCE_VALIDATION_SCOPE")
|
|
yolo_validation_scope_manifest_path: str | None = Field(
|
|
default=None,
|
|
validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_PATH",
|
|
)
|
|
yolo_validation_scope_manifest_sha256: str | None = Field(
|
|
default=None,
|
|
validation_alias="YOLO_VALIDATION_SCOPE_MANIFEST_SHA256",
|
|
)
|
|
# Deprecated compatibility field. Mutable Area names are never an
|
|
# inference authorization boundary; deployments must use the immutable
|
|
# checksum-bound scope manifest above.
|
|
yolo_validated_area_names: str = Field(default="Mol,Kempen", validation_alias="YOLO_VALIDATED_AREA_NAMES")
|
|
yolo_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE")
|
|
yolo_require_cuda: bool = Field(default=False, validation_alias="YOLO_REQUIRE_CUDA")
|
|
yolo_image_size: int = Field(default=640, validation_alias="YOLO_IMAGE_SIZE")
|
|
yolo_max_tiles: int = Field(default=100, validation_alias="YOLO_MAX_TILES")
|
|
yolo_max_detections: int = Field(default=1000, validation_alias="YOLO_MAX_DETECTIONS")
|
|
yolo_duplicate_iou_threshold: float = Field(default=0.5, ge=0.0, le=1.0, validation_alias="YOLO_DUPLICATE_IOU_THRESHOLD")
|
|
yolo_suppress_tile_edge_detections: bool = Field(
|
|
default=True, validation_alias="YOLO_SUPPRESS_TILE_EDGE_DETECTIONS"
|
|
)
|
|
# Intersection over the smaller box. The candidate evaluation freezes this
|
|
# during calibration; serving a promoted model at a different value means
|
|
# the runtime suppresses detections the gate counted.
|
|
yolo_containment_nms_threshold: float = Field(
|
|
default=0.85, ge=0.0, le=1.0, validation_alias="YOLO_CONTAINMENT_NMS_THRESHOLD"
|
|
)
|
|
yolo_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE")
|
|
yolo_seg_enabled: bool = Field(default=False, validation_alias="YOLO_SEG_ENABLED")
|
|
yolo_seg_model_path: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_PATH")
|
|
yolo_seg_model_id: str = Field(default="yolo-seg-configured", validation_alias="YOLO_SEG_MODEL_ID")
|
|
yolo_seg_model_display_name: str = Field(
|
|
default="Configured YOLO segmentation",
|
|
validation_alias="YOLO_SEG_MODEL_DISPLAY_NAME",
|
|
)
|
|
yolo_seg_model_version: str | None = Field(default=None, validation_alias="YOLO_SEG_MODEL_VERSION")
|
|
sam_enabled: bool = Field(default=False, validation_alias="SAM_ENABLED")
|
|
sam_model_path: str | None = Field(default=None, validation_alias="SAM_MODEL_PATH")
|
|
sam_model_id: str = Field(default="sam-configured", validation_alias="SAM_MODEL_ID")
|
|
sam_model_display_name: str = Field(
|
|
default="Configured SAM segmentation",
|
|
validation_alias="SAM_MODEL_DISPLAY_NAME",
|
|
)
|
|
sam_model_version: str | None = Field(default=None, validation_alias="SAM_MODEL_VERSION")
|
|
segmentation_max_masks_per_tile: int = Field(default=300, ge=1, validation_alias="SEGMENTATION_MAX_MASKS_PER_TILE")
|
|
# Masks and boxes overlap differently, so segmentation carries its own
|
|
# containment value rather than borrowing the detector's.
|
|
segmentation_containment_nms_threshold: float = Field(
|
|
default=0.85,
|
|
ge=0.0,
|
|
le=1.0,
|
|
validation_alias="SEGMENTATION_CONTAINMENT_NMS_THRESHOLD",
|
|
)
|
|
segmentation_duplicate_iou_threshold: float = Field(
|
|
default=0.5,
|
|
ge=0.0,
|
|
le=1.0,
|
|
validation_alias="SEGMENTATION_DUPLICATE_IOU_THRESHOLD",
|
|
)
|
|
ollama_enabled: bool = Field(default=False, validation_alias="OLLAMA_ENABLED")
|
|
ollama_base_url: str = Field(default="http://127.0.0.1:11434", validation_alias="OLLAMA_BASE_URL")
|
|
ollama_default_model: str = Field(default="qwen3.5:9b", validation_alias="OLLAMA_DEFAULT_MODEL")
|
|
ollama_timeout_seconds: int = Field(default=120, ge=5, le=600, validation_alias="OLLAMA_TIMEOUT_SECONDS")
|
|
ollama_max_output_tokens: int = Field(default=1_200, ge=100, le=4_000, validation_alias="OLLAMA_MAX_OUTPUT_TOKENS")
|
|
ollama_context_tokens: int = Field(default=16_384, ge=4_096, le=131_072, validation_alias="OLLAMA_CONTEXT_TOKENS")
|
|
cors_origins: list[str] | str = Field(
|
|
default=["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
validation_alias="CORS_ORIGINS",
|
|
)
|
|
|
|
@field_validator("cors_origins", mode="before")
|
|
@classmethod
|
|
def parse_cors_origins(cls, value: object) -> list[str]:
|
|
if isinstance(value, str):
|
|
return [item.strip() for item in value.split(",") if item.strip()]
|
|
if isinstance(value, list):
|
|
return value
|
|
if value is None:
|
|
return ["http://localhost:5173", "http://127.0.0.1:5173"]
|
|
return [str(value)]
|
|
|
|
@field_validator("ollama_base_url")
|
|
@classmethod
|
|
def validate_ollama_base_url(cls, value: str) -> str:
|
|
normalized = value.strip().rstrip("/")
|
|
if not normalized.startswith(("http://", "https://")):
|
|
raise ValueError("OLLAMA_BASE_URL must use http or https")
|
|
return normalized
|
|
|
|
@model_validator(mode="after")
|
|
def validate_operator_auth(self) -> "Settings":
|
|
self.guest_display_name = self.guest_display_name.strip()
|
|
if not self.guest_display_name:
|
|
raise ValueError("GEOINTEL_GUEST_DISPLAY_NAME must not be blank")
|
|
for field_name in (
|
|
"authentik_issuer",
|
|
"authentik_client_id",
|
|
"authentik_client_secret",
|
|
"authentik_allowed_email",
|
|
):
|
|
value = getattr(self, field_name)
|
|
setattr(self, field_name, value.strip() if value else None)
|
|
self.public_base_url = self.public_base_url.strip().rstrip("/")
|
|
authentik_values = (
|
|
self.authentik_issuer,
|
|
self.authentik_client_id,
|
|
self.authentik_client_secret,
|
|
self.authentik_allowed_email,
|
|
)
|
|
if any(authentik_values) and not all(authentik_values):
|
|
raise ValueError("All GEOINTEL_AUTHENTIK_* values must be configured together")
|
|
if all(authentik_values):
|
|
if not self.auth_enabled:
|
|
raise ValueError("GEOINTEL_AUTH_ENABLED must be true when Authentik is configured")
|
|
for label, value in (
|
|
("GEOINTEL_AUTHENTIK_ISSUER", self.authentik_issuer),
|
|
("GEOINTEL_PUBLIC_BASE_URL", self.public_base_url),
|
|
):
|
|
parsed = urlsplit(str(value))
|
|
if (
|
|
parsed.scheme != "https"
|
|
or not parsed.hostname
|
|
or parsed.username
|
|
or parsed.password
|
|
or parsed.query
|
|
or parsed.fragment
|
|
):
|
|
raise ValueError(f"{label} must be an absolute HTTPS URL without credentials, query or fragment")
|
|
public_url = urlsplit(self.public_base_url)
|
|
if public_url.path not in ("", "/"):
|
|
raise ValueError("GEOINTEL_PUBLIC_BASE_URL must not contain a path")
|
|
if "@" not in str(self.authentik_allowed_email) or any(
|
|
character.isspace() for character in str(self.authentik_allowed_email)
|
|
):
|
|
raise ValueError("GEOINTEL_AUTHENTIK_ALLOWED_EMAIL must be one valid e-mail address")
|
|
if not self.auth_enabled:
|
|
return self
|
|
if not (self.auth_username or "").strip():
|
|
raise ValueError("GEOINTEL_AUTH_USERNAME is required when authentication is enabled")
|
|
if not (self.auth_password_hash or "").startswith("pbkdf2_sha256$"):
|
|
raise ValueError("GEOINTEL_AUTH_PASSWORD_HASH must be a PBKDF2-SHA256 hash")
|
|
if len(self.auth_session_secret or "") < 32:
|
|
raise ValueError("GEOINTEL_AUTH_SESSION_SECRET must contain at least 32 characters")
|
|
return self
|
|
|
|
|
|
def get_settings() -> Settings:
|
|
return Settings()
|