from pydantic import Field, field_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-rc.1", 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") 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") 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") 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", ) 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_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", ) 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", ) 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") 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", ) 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_device: str = Field(default="cpu", validation_alias="YOLO_DEVICE") 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_batch_size: int = Field(default=1, validation_alias="YOLO_BATCH_SIZE") 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 def get_settings() -> Settings: return Settings()