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
+13
View File
@@ -269,6 +269,19 @@ def guest_login(
status_code=status.HTTP_403_FORBIDDEN,
)
client_host = _client_host(request)
retry_after = AuthService.consume_guest_request(
f"guest-login:{client_host}",
max_requests=settings.guest_login_requests_per_minute,
)
if retry_after:
raise AppError(
code="GUEST_LOGIN_RATE_LIMITED",
message="Too many guest sessions were requested. Try again later.",
details={"retry_after_seconds": retry_after},
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
)
demo = DemoWorkflowService.seed(db)
token = AuthService.create_session_token(
settings.guest_display_name,
+7
View File
@@ -10,6 +10,7 @@ from fastapi import UploadFile
from sqlalchemy.orm import Session
from app.core.config import get_settings
from app.core.errors import AppError
from app.core.public_demo import is_public_demo_project
from app.db.session import get_db
from app.models import Area, Project
from app.schemas import (
@@ -163,6 +164,12 @@ async def upload_dataset(
source_version: str | None = Form(None),
db: Session = Depends(get_db),
):
if is_public_demo_project(project_id):
raise AppError(
code="PUBLIC_DEMO_UPLOAD_FORBIDDEN",
message="Operator uploads are not accepted in the public demo project.",
status_code=403,
)
if area_id is not None:
area = db.get(Area, area_id)
if not area:
+2
View File
@@ -55,12 +55,14 @@ def get_yolo_preflight(
tile_manifest_path: str | None = None,
check_model_load: bool = False,
model_asset_id: str | None = None,
db: Session = Depends(get_db),
) -> dict:
return envelope(
YoloPreflightService.run(
tile_manifest_path=tile_manifest_path,
check_model_load=check_model_load,
model_asset_id=model_asset_id,
db=db,
)
)
+6 -14
View File
@@ -47,21 +47,18 @@ def _database_checks() -> dict[str, str]:
with get_engine().connect() as connection:
connection.execute(text("SELECT 1"))
checks["database"] = "ok"
postgis_version = connection.execute(
connection.execute(
text("SELECT PostGIS_Version()")
).scalar_one()
checks["postgis"] = f"ok:{postgis_version}"
checks["postgis"] = "ok"
database_head = connection.execute(
text("SELECT version_num FROM alembic_version")
).scalar_one()
expected_heads = _expected_migration_heads()
if len(expected_heads) == 1 and database_head == expected_heads[0]:
checks["migration"] = f"ok:{database_head}"
checks["migration"] = "ok"
else:
checks["migration"] = (
f"degraded:database={database_head};"
f"expected={','.join(expected_heads) or 'none'}"
)
checks["migration"] = "degraded"
except Exception:
return checks
return checks
@@ -94,9 +91,7 @@ def _readiness_payload() -> HealthResponse:
return HealthResponse(
status="ok" if ready else "degraded",
service="geointel-backend",
version=settings.app_version,
build_sha=settings.build_sha,
build_time=settings.build_time,
version="public",
database=checks["database"],
postgis=checks["postgis"],
migration=checks["migration"],
@@ -107,13 +102,10 @@ def _readiness_payload() -> HealthResponse:
@router.get("/health/live", response_model=HealthResponse)
def liveness() -> HealthResponse:
settings = get_settings()
return HealthResponse(
status="ok",
service="geointel-backend",
version=settings.app_version,
build_sha=settings.build_sha,
build_time=settings.build_time,
version="public",
)
+36
View File
@@ -55,6 +55,24 @@ class Settings(BaseSettings):
le=86_400,
validation_alias="GEOINTEL_GUEST_SESSION_TTL_SECONDS",
)
guest_login_requests_per_minute: int = Field(
default=3,
ge=1,
le=60,
validation_alias="GEOINTEL_GUEST_LOGIN_REQUESTS_PER_MINUTE",
)
guest_compute_requests_per_minute: int = Field(
default=4,
ge=1,
le=120,
validation_alias="GEOINTEL_GUEST_COMPUTE_REQUESTS_PER_MINUTE",
)
guest_compute_max_concurrency: int = Field(
default=2,
ge=1,
le=16,
validation_alias="GEOINTEL_GUEST_COMPUTE_MAX_CONCURRENCY",
)
database_url: str = Field(
default="postgresql+psycopg://geointel:geointel@localhost:5432/geointel?connect_timeout=1",
validation_alias="DATABASE_URL",
@@ -77,6 +95,24 @@ class Settings(BaseSettings):
le=256,
validation_alias="GEOINTEL_MAX_IN_MEMORY_VECTOR_MB",
)
max_raster_pixels: int = Field(
default=40_000_000,
ge=1,
le=500_000_000,
validation_alias="GEOINTEL_MAX_RASTER_PIXELS",
)
max_raster_bands: int = Field(
default=16,
ge=1,
le=256,
validation_alias="GEOINTEL_MAX_RASTER_BANDS",
)
max_decoded_raster_mb: int = Field(
default=1024,
ge=16,
le=8192,
validation_alias="GEOINTEL_MAX_DECODED_RASTER_MB",
)
orthophoto_enabled: bool = Field(default=True, validation_alias="ORTHOPHOTO_ENABLED")
orthophoto_wms_url: str = Field(
default="https://geo.api.vlaanderen.be/OMWRGBMRVL/wms",
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from uuid import UUID
# Stable server-owned identity: a public session must never attach itself to an
# operator project merely because the display names happen to match.
PUBLIC_DEMO_PROJECT_ID = UUID("6f7e6f12-9b62-4a3f-a5a0-4b3bb6b2c901")
PUBLIC_DEMO_PROJECT_NAME = "GeoIntel Demo - Building QA"
PUBLIC_DEMO_PROJECT_MARKER = "geointel:public-demo:v1"
def is_public_demo_project(project_id: UUID) -> bool:
return project_id == PUBLIC_DEMO_PROJECT_ID
+48
View File
@@ -127,6 +127,7 @@ def create_app() -> FastAPI:
token = set_request_id(request_id)
started_at = time.perf_counter()
raw_path = str(request.scope.get("path") or "")
guest_compute_acquired = False
try:
host = request.headers.get("host", "")
content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().lower()
@@ -234,6 +235,20 @@ def create_app() -> FastAPI:
)
is_read_request = request.method in {"GET", "HEAD", "OPTIONS"}
if is_read_request:
if (
normalized_path == f"{settings.api_prefix}/detection/yolo/preflight"
and request.query_params.get("check_model_load", "").lower() in {"1", "true", "yes", "on"}
):
response = JSONResponse(
status_code=403,
content=_to_error_payload(
"GUEST_MODEL_LOAD_FORBIDDEN",
"Model loading is available to authenticated operators only.",
request_id=request_id,
),
)
response.headers["x-request-id"] = request_id
return response
guest_scoped_analysis_read = (
query_project_id == str(principal.project_id)
and normalized_path.startswith(
@@ -325,6 +340,37 @@ def create_app() -> FastAPI:
)
response.headers["x-request-id"] = request_id
return response
retry_after = AuthService.consume_guest_request(
f"guest-compute:{principal.session_id}",
max_requests=settings.guest_compute_requests_per_minute,
)
if retry_after:
response = JSONResponse(
status_code=429,
content=_to_error_payload(
"GUEST_COMPUTE_RATE_LIMITED",
"The public demo compute budget is temporarily exhausted.",
request_id=request_id,
),
)
response.headers["retry-after"] = str(retry_after)
response.headers["x-request-id"] = request_id
return response
guest_compute_acquired = AuthService.try_acquire_guest_compute(
max_concurrency=settings.guest_compute_max_concurrency,
)
if not guest_compute_acquired:
response = JSONResponse(
status_code=429,
content=_to_error_payload(
"GUEST_COMPUTE_BUSY",
"The public demo is already processing its maximum number of jobs.",
request_id=request_id,
),
)
response.headers["retry-after"] = "10"
response.headers["x-request-id"] = request_id
return response
response = await call_next(request)
response.headers["x-request-id"] = request_id
logger.info(
@@ -337,6 +383,8 @@ def create_app() -> FastAPI:
)
return response
finally:
if guest_compute_acquired:
AuthService.release_guest_compute()
reset_request_id(token)
@app.exception_handler(AppError)
+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"
+59
View File
@@ -0,0 +1,59 @@
# Linux x86_64 / CPython 3.11 AI runtime lock.
#
# Resolved on Linux for the production CUDA 12.8 image. Every artifact is
# pinned by version and SHA-256 so installation fails closed on index drift.
certifi==2026.7.22 --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775
charset-normalizer==3.5.1 --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8
contourpy==1.3.3 --hash=sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db
cuda-bindings==12.9.7 --hash=sha256:c6496a88d84b1209d6651b0370c19c26319e157c22f6d018bf9a358cd8049041
cuda-pathfinder==1.8.0 --hash=sha256:c44e574dc997fae2814721d1ae97d0fd6db76db82decbe9b753bf75de53f515e
cuda-toolkit==12.8.1 --hash=sha256:adc7906af4ecbf9a352f9dca5734eceb21daec281ccfcf5675e1d2f724fc2cba
cycler==0.12.1 --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30
filelock==3.32.5 --hash=sha256:142cd9fa77a872c5e78c62329a0d15278fadc686eb89e760017968961a4fd6b2
fonttools==4.64.0 --hash=sha256:ff7aff4637fbf71394df139c63ccfe08a47aa4252d2f91224ddb3335c716c925
fsspec==2026.7.0 --hash=sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279
idna==3.19 --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
jinja2==3.1.6 --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
kiwisolver==1.5.1 --hash=sha256:95a02752aa032eef4aed01cda6d9b687c669bd0396bf4519eef8bba22a286720
markupsafe==3.0.3 --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf
matplotlib==3.11.1 --hash=sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83
mpmath==1.3.0 --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c
networkx==3.6.1 --hash=sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762
numpy==2.4.6 --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93
nvidia-cublas-cu12==12.8.4.1 --hash=sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142
nvidia-cuda-cupti-cu12==12.8.90 --hash=sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182
nvidia-cuda-nvrtc-cu12==12.8.93 --hash=sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994
nvidia-cuda-runtime-cu12==12.8.90 --hash=sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90
nvidia-cudnn-cu12==9.19.0.56 --hash=sha256:ac6ad90a075bb33a94f2b4cf4622eac13dd4dc65cf6dd9c7572a318516a36625
nvidia-cufft-cu12==11.3.3.83 --hash=sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74
nvidia-cufile-cu12==1.13.1.3 --hash=sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc
nvidia-curand-cu12==10.3.9.90 --hash=sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9
nvidia-cusolver-cu12==11.7.3.90 --hash=sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450
nvidia-cusparse-cu12==12.5.8.93 --hash=sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b
nvidia-cusparselt-cu12==0.7.1 --hash=sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623
nvidia-ml-py==13.610.43 --hash=sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8
nvidia-nccl-cu12==2.28.9 --hash=sha256:485776daa8447da5da39681af455aa3b2c2586ddcf4af8772495e7c532c7e5ab
nvidia-nvjitlink-cu12==12.8.93 --hash=sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88
nvidia-nvshmem-cu12==3.4.5 --hash=sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd
nvidia-nvtx-cu12==12.8.90 --hash=sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f
opencv-python==5.0.0.93 --hash=sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039
packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
pillow==12.3.0 --hash=sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd
polars==1.44.1 --hash=sha256:1fa62fc1c88fba77a68b28291b5aabdd69e5f38b34e59721a064ae3169b59bb5
polars-runtime-32==1.44.1 --hash=sha256:eea4283be8e60822d890dbda20588fe59b4172b508bd5ebf3471e531ca9f50d7
psutil==7.2.2 --hash=sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9
pyparsing==3.3.2 --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d
python-dateutil==2.9.0.post0 --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
pyyaml==6.0.3 --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d
requests==2.34.2 --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0
setuptools==81.0.0 --hash=sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6
six==1.17.0 --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
sympy==1.14.0 --hash=sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5
torch==2.11.0+cu128 --hash=sha256:c9a7ca4c74fae10a58e6175b4b2cea953f9322bb6562bbf339ad6a05f52190ad
torchvision==0.26.0+cu128 --hash=sha256:8f2629d056570c929b0a1d5473d9cb0320b90bda1764bda353553a72cc6b2069
triton==3.6.0 --hash=sha256:e021e87e8f266c6f87bf379b669c43c2800aac6247bdf5d518cb553e3c403c00
typing-extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8
ultralytics==8.4.99 --hash=sha256:477727b3f07de28f1f34888c7661b7cc3b6bf3e55b9675d7b3eadfa6f4747385
ultralytics-thop==2.1.6 --hash=sha256:23f7b8ad124fa3432c1a7de9279102c4fdda699216032a7dff49f87ec3d1a3af
urllib3==2.7.0 --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
wheel==0.47.0 --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced
+4
View File
@@ -0,0 +1,4 @@
# Build tooling used after the runtime dependency installation.
packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
setuptools==81.0.0 --hash=sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6
wheel==0.47.0 --hash=sha256:212281cab4dff978f6cedd499cd893e1f620791ca6ff7107cf270781e587eced
+1
View File
@@ -50,6 +50,7 @@ def main() -> int:
tile_manifest_path=args.tile_manifest_path,
assume_dependencies=args.assume_dependencies,
check_model_load=args.check_model_load,
allow_offline_model_load=True,
)
if args.json:
@@ -1182,7 +1182,7 @@ def test_one_workflow_is_byte_reproducible_complete_and_fail_closed(
)
assert input_manifest["product_baseline"]["validation_status"] == "not_evaluable"
assert input_manifest["product_baseline"]["artifacts"] == []
assert "docs/accuracy-program/status.json" not in {
assert "fixtures/accuracy/readiness/status.json" not in {
item["path"] for item in input_manifest["inputs"]
}
assert input_manifest["readiness_snapshot"]["source_paths"][
@@ -1199,11 +1199,11 @@ def test_one_workflow_is_byte_reproducible_complete_and_fail_closed(
def test_readiness_snapshot_ignores_phase4_bookkeeping_but_binds_active_model(
tmp_path: Path,
) -> None:
status_path = tmp_path / "docs/accuracy-program/status.json"
scan_path = tmp_path / "artifacts/evidence/accuracy/P3/full-scan-manifest.json"
leakage_path = tmp_path / "artifacts/evidence/accuracy/P3/leakage-report.json"
status_path = tmp_path / "fixtures/accuracy/readiness/status.json"
scan_path = tmp_path / "fixtures/accuracy/readiness/full-scan-manifest.json"
leakage_path = tmp_path / "fixtures/accuracy/readiness/leakage-report.json"
status_path.parent.mkdir(parents=True)
scan_path.parent.mkdir(parents=True)
scan_path.parent.mkdir(parents=True, exist_ok=True)
status = {
"generated_at": "2026-08-02T00:00:00+02:00",
"documents": ["old.md"],
+4 -3
View File
@@ -7,6 +7,7 @@ from uuid import UUID, uuid4
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.core.public_demo import PUBLIC_DEMO_PROJECT_ID
from app.db.session import get_db
from app.main import create_app
from app.schemas.demo import DemoWorkflowResponse
@@ -172,7 +173,7 @@ def test_operator_login_can_require_https(monkeypatch) -> None:
def test_guest_login_exposes_models_but_rejects_management_and_cross_project_requests(monkeypatch) -> None:
project_id = UUID("00000000-0000-0000-0000-000000000123")
project_id = PUBLIC_DEMO_PROJECT_ID
demo = DemoWorkflowResponse(
project_id=project_id,
area_id=UUID("00000000-0000-0000-0000-000000000124"),
@@ -252,7 +253,7 @@ def test_guest_login_exposes_models_but_rejects_management_and_cross_project_req
def test_guest_change_detection_binds_both_datasets_to_signed_demo_project(monkeypatch) -> None:
project_id = UUID("00000000-0000-0000-0000-000000000123")
project_id = PUBLIC_DEMO_PROJECT_ID
other_project_id = UUID("00000000-0000-0000-0000-000000000999")
source_dataset_id = UUID("00000000-0000-0000-0000-000000000125")
target_dataset_id = UUID("00000000-0000-0000-0000-000000000126")
@@ -342,7 +343,7 @@ def test_guest_change_detection_binds_both_datasets_to_signed_demo_project(monke
def test_guest_can_prepare_tiles_and_queue_project_scoped_detection(monkeypatch) -> None:
project_id = UUID("00000000-0000-0000-0000-000000000123")
project_id = PUBLIC_DEMO_PROJECT_ID
raster_dataset_id = UUID("00000000-0000-0000-0000-000000000127")
manifest_path = "/app/storage/tiles/demo/manifest.json"
demo = DemoWorkflowResponse(
+9 -5
View File
@@ -48,11 +48,14 @@ def test_all_in_one_dockerfile_can_opt_into_ai_dependencies_without_base_install
assert "ARG GEOINTEL_INSTALL_AI=false" in dockerfile
assert "COPY backend/pyproject.toml /app/" in dockerfile
assert "COPY backend/requirements-runtime.lock /app/" in dockerfile
assert "COPY backend/requirements-ai-linux.lock /app/" in dockerfile
assert "COPY backend/requirements-build-tools.lock /app/" in dockerfile
assert "COPY backend/pyproject.toml backend/README.md /app/" not in dockerfile
assert "GeoIntel backend package metadata" in dockerfile
assert "--require-hashes -r requirements-runtime.lock" in dockerfile
assert "ARG GEOINTEL_ULTRALYTICS_VERSION=" in dockerfile
assert '"ultralytics==$GEOINTEL_ULTRALYTICS_VERSION"' in dockerfile
assert "--require-hashes" in dockerfile
assert "-r requirements-ai-linux.lock" in dockerfile
assert "-r requirements-build-tools.lock" in dockerfile
assert "python scripts/gis_import_smoke.py" in dockerfile
assert "yolo_preflight.py" in dockerfile
assert "libxcb1" in dockerfile
@@ -472,9 +475,10 @@ def test_all_in_one_dockerfile_caches_dependencies_and_pins_driver_compatible_cu
assert metadata_copy_index < placeholder_readme_index < dependency_install_index < backend_copy_index < smoke_index
assert "GEOINTEL_TORCH_INDEX_URL=https://download.pytorch.org/whl/cu128" in dockerfile
assert "GEOINTEL_TORCH_VERSION=2.11.0" in dockerfile
assert "GEOINTEL_TORCHVISION_VERSION=0.26.0" in dockerfile
assert '--index-url "$GEOINTEL_TORCH_INDEX_URL"' in dockerfile
assert '--extra-index-url "$GEOINTEL_TORCH_INDEX_URL"' in dockerfile
ai_lock = (ROOT / "backend" / "requirements-ai-linux.lock").read_text(encoding="utf-8")
assert "torch==2.11.0+cu128 --hash=sha256:" in ai_lock
assert "torchvision==0.26.0+cu128 --hash=sha256:" in ai_lock
def test_unraid_deploy_passes_ai_build_arg_and_yolo_runtime_env() -> None:
+2 -1
View File
@@ -7,6 +7,7 @@ import pytest
from fastapi.testclient import TestClient
from app.core.config import get_settings
from app.core.public_demo import PUBLIC_DEMO_PROJECT_ID
from app.db.session import get_db
from app.main import create_app
from app.models import AnalysisRun, Dataset, Detection, Export, Job, Segmentation
@@ -21,7 +22,7 @@ from app.services.detection_service import DetectionService
from app.services.segmentation_service import SegmentationService
GUEST_PROJECT_ID = UUID("00000000-0000-0000-0000-000000000123")
GUEST_PROJECT_ID = PUBLIC_DEMO_PROJECT_ID
OTHER_PROJECT_ID = UUID("00000000-0000-0000-0000-000000000999")
DATASET_ID = UUID("00000000-0000-0000-0000-000000000201")
DETECTION_RUN_ID = UUID("00000000-0000-0000-0000-000000000202")
+7 -4
View File
@@ -175,11 +175,14 @@ def test_release_image_uses_locked_non_ai_dependencies_and_npm_ci() -> None:
assert "RUN npm ci" in dockerfile
assert "COPY backend/requirements-runtime.lock /app/" in dockerfile
assert "COPY backend/requirements-ai-linux.lock /app/" in dockerfile
assert "COPY backend/requirements-build-tools.lock /app/" in dockerfile
assert "pip install --no-cache-dir --require-hashes -r requirements-runtime.lock" in dockerfile
assert "ARG GEOINTEL_ULTRALYTICS_VERSION=8.4.99" in dockerfile
assert '"ultralytics==$GEOINTEL_ULTRALYTICS_VERSION"' in dockerfile
assert "ARG GEOINTEL_SETUPTOOLS_VERSION=81.0.0" in dockerfile
assert "ARG GEOINTEL_WHEEL_VERSION=0.47.0" in dockerfile
assert "-r requirements-ai-linux.lock" in dockerfile
assert "-r requirements-build-tools.lock" in dockerfile
assert "--require-hashes" in dockerfile
assert "ultralytics==8.4.99 --hash=sha256:" in read("backend/requirements-ai-linux.lock")
assert "torch==2.11.0+cu128 --hash=sha256:" in read("backend/requirements-ai-linux.lock")
assert "COPY deploy/unraid/gosu-setpriv /usr/local/bin/gosu" in dockerfile
assert "&& pip check" in dockerfile
@@ -21,5 +21,5 @@ def test_evidence_bundle_script_accepts_browser_calibration_summary_export() ->
assert "root_project_id = summary.get(\"project_id\")" in script
assert "quality_check_ids" in script
assert "Browser Detection Lab calibration summary" in readme
assert "bash scripts/export_detection_calibration_evidence.sh http://192.168.123.45:1202 ./detection-calibration-summary.json" in readme
assert "bash scripts/export_detection_calibration_evidence.sh http://192.0.2.10:1202 ./detection-calibration-summary.json" in readme
assert "[x] Allow the evidence bundle script to consume Detection Lab calibration summary exports" in todo
@@ -206,6 +206,7 @@ def test_yolo_preflight_can_explicitly_smoke_load_local_model(tmp_path: Path) ->
tile_manifest_path=str(manifest_path),
yolo_adapter_class=AvailableAdapter,
check_model_load=True,
allow_offline_model_load=True,
)
assert result["status"] == "ready"
@@ -231,6 +232,7 @@ def test_yolo_preflight_reports_explicit_model_load_failure(tmp_path: Path) -> N
tile_manifest_path=str(_manifest(tmp_path)),
yolo_adapter_class=FailingLoadAdapter,
check_model_load=True,
allow_offline_model_load=True,
)
assert result["status"] == "model_load_failed"
+5 -5
View File
@@ -74,13 +74,13 @@ def test_demo_workflow_service_supports_container_fixture_mount() -> None:
assert "_quality_check_matches_expected" in service
def test_demo_workflow_prefers_complete_existing_demo_project() -> None:
def test_demo_workflow_uses_reserved_server_owned_project_identity() -> None:
service = (DemoWorkflowService._repo_root() / "backend" / "app" / "services" / "demo_workflow_service.py").read_text(encoding="utf-8")
assert "_has_complete_demo_state" in service
assert "order_by(Project.created_at.asc())" in service
assert "if DemoWorkflowService._has_complete_demo_state(db, project.id):" in service
assert "return projects[0] if projects else None" in service
assert "db.get(Project, DemoWorkflowService.PROJECT_ID)" in service
assert "PUBLIC_DEMO_PROJECT_MARKER" in service
assert "PUBLIC_DEMO_IDENTITY_CONFLICT" in service
assert "id=DemoWorkflowService.PROJECT_ID" in service
def test_explicit_demo_seed_reactivates_an_archived_fixture_project() -> None:
@@ -100,7 +100,7 @@ def test_unraid_all_in_one_runtime_starts_embedded_postgis_backend_and_nginx() -
nginx_config = (ROOT / "deploy" / "unraid" / "nginx-all-in-one.conf").read_text(encoding="utf-8")
dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
assert "FROM postgres:16-bookworm AS runtime" in dockerfile
assert "FROM postgres:16-bookworm@sha256:" in dockerfile
assert "postgresql-16-postgis-3" in dockerfile
assert "postgresql-16-postgis-3-scripts" in dockerfile
assert "ln -sf /usr/bin/python3.11 /usr/local/bin/python3" in dockerfile
@@ -13,9 +13,9 @@ def test_export_handoff_cards_use_width_aware_grid() -> None:
assert "repeat(auto-fit, minmax(7.5rem, 1fr))" in css
def test_live_workspace_smoke_artifacts_are_documented() -> None:
execution_log = (ROOT / "docs" / "CODEX_EXECUTION_LOG.md").read_text(encoding="utf-8")
def test_public_readme_documents_desktop_and_mobile_workspace() -> None:
readme = (ROOT / "README.md").read_text(encoding="utf-8")
assert "Sprint 66 Live workspace smoke polish" in execution_log
assert "desktop and mobile screenshots" in execution_log
assert "no console warnings/errors" in execution_log
assert "geointel-workbench-wide.png" in readme
assert "geointel-workbench-mobile.png" in readme
assert "Mobiele werkruimte" in readme