Prepare GeoIntel for public release
Managed validation / Managed repository validation (pull_request) Successful in 1m46s
GeoIntel release gates / Compile, test, contracts and builds (pull_request) Successful in 1m51s
GeoIntel release gates / Python and npm vulnerability policy (pull_request) Successful in 20s
GeoIntel release gates / Production AI image, SBOM and container scan (pull_request) Successful in 15m3s
GeoIntel release gates / Deploy exact gated revision to Unraid (pull_request) Skipped

This commit is contained in:
Jens
2026-08-31 21:33:10 +02:00
parent dcd67b11d1
commit 2cdf9c99c6
195 changed files with 595 additions and 207595 deletions
+45 -2
View File
@@ -8,17 +8,19 @@ import secrets
import threading
import time
from collections import deque
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Literal, cast
from uuid import UUID
from app.core.config import Settings
from app.core.public_demo import PUBLIC_DEMO_PROJECT_ID
@dataclass(frozen=True)
class AuthPrincipal:
username: str
expires_at: int
session_id: str = field(default_factory=lambda: secrets.token_urlsafe(12))
role: Literal["operator", "guest"] = "operator"
project_id: UUID | None = None
@@ -30,6 +32,10 @@ class AuthService:
FAILURE_WINDOW_SECONDS = 300
_failures: dict[str, deque[float]] = {}
_failure_lock = threading.Lock()
_guest_requests: dict[str, deque[float]] = {}
_guest_request_lock = threading.Lock()
_active_guest_compute = 0
_guest_compute_lock = threading.Lock()
@staticmethod
def _b64_encode(value: bytes) -> str:
@@ -159,8 +165,9 @@ class AuthService:
issued_at = int(payload.get("iat") or 0)
version = int(payload.get("v") or 0)
role_value = str(payload.get("role") or "operator")
session_id = str(payload.get("jti") or "")
current = int(time.time() if now is None else now)
if version not in {1, 2} or role_value not in {"operator", "guest"}:
if version not in {1, 2} or role_value not in {"operator", "guest"} or not session_id:
return None
role = cast(Literal["operator", "guest"], role_value)
if issued_at <= 0 or issued_at > current + 60 or expires_at <= current:
@@ -178,11 +185,14 @@ class AuthService:
if not raw_project_id:
return None
project_id = UUID(str(raw_project_id))
if project_id != PUBLIC_DEMO_PROJECT_ID:
return None
if expires_at - issued_at > max_ttl:
return None
return AuthPrincipal(
username=username,
expires_at=expires_at,
session_id=session_id,
role=role,
project_id=project_id,
)
@@ -215,3 +225,36 @@ class AuthService:
def clear_failures(cls, key: str) -> None:
with cls._failure_lock:
cls._failures.pop(key, None)
@classmethod
def consume_guest_request(
cls,
key: str,
*,
max_requests: int,
window_seconds: int = 60,
now: float | None = None,
) -> int:
"""Record a guest action and return Retry-After seconds when limited."""
current = time.monotonic() if now is None else now
with cls._guest_request_lock:
attempts = cls._guest_requests.setdefault(key, deque())
while attempts and current - attempts[0] >= window_seconds:
attempts.popleft()
if len(attempts) >= max_requests:
return max(1, int(window_seconds - (current - attempts[0])))
attempts.append(current)
return 0
@classmethod
def try_acquire_guest_compute(cls, *, max_concurrency: int) -> bool:
with cls._guest_compute_lock:
if cls._active_guest_compute >= max_concurrency:
return False
cls._active_guest_compute += 1
return True
@classmethod
def release_guest_compute(cls) -> None:
with cls._guest_compute_lock:
cls._active_guest_compute = max(0, cls._active_guest_compute - 1)
+23 -14
View File
@@ -10,6 +10,12 @@ from uuid import UUID, uuid4
from geoalchemy2.shape import from_shape
from sqlalchemy.orm import Session
from app.core.errors import AppError
from app.core.public_demo import (
PUBLIC_DEMO_PROJECT_ID,
PUBLIC_DEMO_PROJECT_MARKER,
PUBLIC_DEMO_PROJECT_NAME,
)
from app.models import Area, Dataset, DatasetVersion, Metric, Project, QualityCheck
from app.schemas.demo import DemoWorkflowResponse
from app.services.derived_dataset_governance_service import DerivedDatasetGovernanceService
@@ -23,7 +29,8 @@ from app.utils.geometry import area_m2, geometry_bbox_polygon, normalize_to_mult
class DemoWorkflowService:
PROJECT_NAME = "GeoIntel Demo - Building QA"
PROJECT_ID = PUBLIC_DEMO_PROJECT_ID
PROJECT_NAME = PUBLIC_DEMO_PROJECT_NAME
AREA_NAME = "Demo AOI - Geel buildings"
REFERENCE_FILENAME = "demo_reference_buildings.geojson"
CANDIDATE_FILENAME = "demo_predicted_buildings.geojson"
@@ -91,17 +98,19 @@ class DemoWorkflowService:
@staticmethod
def _find_existing_project(db: Session) -> Project | None:
projects = (
db.query(Project)
.filter(Project.name == DemoWorkflowService.PROJECT_NAME)
.filter(Project.status != "deleted")
.order_by(Project.created_at.asc())
.all()
)
for project in projects:
if DemoWorkflowService._has_complete_demo_state(db, project.id):
return project
return projects[0] if projects else None
project = db.get(Project, DemoWorkflowService.PROJECT_ID)
if project is None:
return None
if (
project.name != DemoWorkflowService.PROJECT_NAME
or project.description != PUBLIC_DEMO_PROJECT_MARKER
):
raise AppError(
code="PUBLIC_DEMO_IDENTITY_CONFLICT",
message="The reserved public-demo project identity is already in use.",
status_code=409,
)
return project
@staticmethod
def _activate_explicit_demo_project(db: Session, project: Project | None) -> Project | None:
@@ -487,9 +496,9 @@ class DemoWorkflowService:
created = True
else:
project = Project(
id=uuid4(),
id=DemoWorkflowService.PROJECT_ID,
name=DemoWorkflowService.PROJECT_NAME,
description="Offline fixture workflow: reference buildings, predicted buildings and persisted QA metrics.",
description=PUBLIC_DEMO_PROJECT_MARKER,
region="Kempen",
status="active",
)
+13
View File
@@ -6,6 +6,7 @@ from typing import Literal
from sqlalchemy.orm import Session
from app.core.errors import AppError
from app.core.public_demo import is_public_demo_project
from app.models import Project
from app.schemas.project import ProjectCreate, ProjectRead, ProjectUpdate
@@ -50,6 +51,12 @@ class ProjectService:
@staticmethod
def update_project(db: Session, project_id: uuid.UUID, payload: ProjectUpdate) -> ProjectRead | None:
if is_public_demo_project(project_id):
raise AppError(
code="PUBLIC_DEMO_IMMUTABLE",
message="The public demo project identity cannot be edited.",
status_code=409,
)
project = db.get(Project, project_id)
if not project or project.status == "deleted":
return None
@@ -71,6 +78,12 @@ class ProjectService:
@staticmethod
def delete_project(db: Session, project_id: uuid.UUID) -> bool:
if is_public_demo_project(project_id):
raise AppError(
code="PUBLIC_DEMO_IMMUTABLE",
message="The public demo project identity cannot be deleted.",
status_code=409,
)
project = db.get(Project, project_id)
if not project or project.status == "deleted":
return False
@@ -466,7 +466,11 @@ class RasterOperationsService:
height = int(source.height)
preview_width, preview_height = RasterOperationsService._preview_dimensions(width, height)
if not preview_path.exists():
data = source.read(1)
data = source.read(
1,
out_shape=(preview_height, preview_width),
resampling=rasterio.enums.Resampling.nearest,
)
try:
preview_width, preview_height = RasterOperationsService._write_preview_image(
data=data,
+49 -3
View File
@@ -2,9 +2,26 @@ from __future__ import annotations
from pathlib import Path
from app.core.config import get_settings
from app.core.errors import AppError
_DTYPE_BYTES = {
"uint8": 1,
"int8": 1,
"uint16": 2,
"int16": 2,
"uint32": 4,
"int32": 4,
"float32": 4,
"uint64": 8,
"int64": 8,
"float64": 8,
"complex64": 8,
"complex128": 16,
}
def _import_rasterio():
import importlib
@@ -26,6 +43,33 @@ def extract_raster_metadata(path: str) -> dict:
dataset_path = Path(path)
try:
with rasterio.open(dataset_path) as dataset:
settings = get_settings()
width = int(dataset.width)
height = int(dataset.height)
band_count = int(dataset.count)
pixel_count = width * height
dtype_bytes = max((_DTYPE_BYTES.get(str(dtype), 16) for dtype in dataset.dtypes), default=16)
decoded_bytes = pixel_count * band_count * dtype_bytes
if (
width <= 0
or height <= 0
or band_count <= 0
or pixel_count > settings.max_raster_pixels
or band_count > settings.max_raster_bands
or decoded_bytes > settings.max_decoded_raster_mb * 1024 * 1024
):
raise AppError(
code="RASTER_RESOURCE_LIMIT_EXCEEDED",
message="Raster dimensions or decoded size exceed the configured processing budget.",
details={
"width": width,
"height": height,
"band_count": band_count,
"pixel_count": pixel_count,
"estimated_decoded_bytes": decoded_bytes,
},
status_code=413,
)
nodata = dataset.nodata
if isinstance(nodata, (list, tuple)):
nodata_value = [None if value is None else float(value) for value in nodata]
@@ -35,9 +79,9 @@ def extract_raster_metadata(path: str) -> dict:
transform = dataset.transform.to_gdal() if hasattr(dataset, "transform") else None
return {
"driver": dataset.driver,
"width": int(dataset.width),
"height": int(dataset.height),
"band_count": int(dataset.count),
"width": width,
"height": height,
"band_count": band_count,
"crs": str(dataset.crs) if dataset.crs else None,
"bounds": list(dataset.bounds),
"resolution": list(dataset.res),
@@ -45,6 +89,8 @@ def extract_raster_metadata(path: str) -> dict:
"nodata": nodata_value,
"transform": list(transform) if transform is not None else None,
}
except AppError:
raise
except Exception as exc:
if isinstance(exc, errors.RasterioIOError):
raise AppError(code="INVALID_RASTER", message="Uploaded raster file is invalid", status_code=400) from exc
@@ -23,6 +23,8 @@ class YoloPreflightService:
assume_dependencies: bool = False,
check_model_load: bool = False,
model_asset_id: str | None = None,
db: Any | None = None,
allow_offline_model_load: bool = False,
) -> dict[str, Any]:
resolved_settings = settings or get_settings()
selected_asset = None
@@ -126,6 +128,21 @@ class YoloPreflightService:
if check_model_load:
try:
if db is not None:
RuntimeModelProvenanceService.validate_for_production_runtime(
db=db,
model_path=model_path,
model_id=resolved_settings.yolo_model_id,
task_type="object_detection",
expected_model_version=resolved_settings.yolo_model_version,
allowed_frameworks=("ultralytics/pytorch", "ultralytics", "pytorch"),
)
elif not allow_offline_model_load:
raise AppError(
code="MODEL_PROVENANCE_DATABASE_REQUIRED",
message="Model compatibility loading requires production provenance or an explicit offline operator command.",
status_code=409,
)
yolo_adapter_class(resolved_settings).load_model(model_path)
except AppError as exc:
result["status"] = "model_load_failed"