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
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:
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user