An ArcGIS layer without supportsPagination accepts resultOffset and ignores it, answering every page with the first one. The VHA profile reader advanced its offset by the page length and stopped at the announced count, so for a count that is a multiple of the page size it collected N copies of page one — and its completeness check, len(features) == candidate_count, passed. Four announced records became four stored records, two of them duplicates, filed under an official provenance. That is the substitution bounded acquisition exists to prevent, arriving through the front door. The reader now refuses a record it already collected. It fails rather than silently dropping the duplicate: a provider that cannot page is a provider whose count proves nothing, so a smaller-but-clean result would still be unverifiable. Its watercourse-name loop was worse — a bare `while True` that ended only when the provider stopped setting exceededTransferLimit, with names deduplicated by code so a stuck provider produced no visible change while the requests continued. It now refuses a repeated page body, and both loops have the page budget the sibling readers already had. Those siblings turned out to be fine. GRB and official vector already refuse a repeated page URL, bound the page count, and deduplicate on feature identity — but none of it had a test, so none of it was known to work. Exercised now, including the case where distinct URLs defeat the loop check and the budget is the only backstop. A duplicate across two genuinely different pages is kept once rather than failing, because a cursor over a changing table produces that legitimately. Also: _bash_path fell back to the raw path whenever wslpath failed, except on timeout, which propagated and reddened the suite when starting WSL took more than ten seconds under load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
486 lines
23 KiB
Python
486 lines
23 KiB
Python
from pydantic import 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")
|
|
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, validation_alias="MAX_UPLOAD_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")
|
|
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()
|