feat: stage1 — domain schemas, SSE events, pydantic-settings config

This commit is contained in:
Yuzviak
2026-03-22 22:18:50 +02:00
parent 6ba883f4d6
commit 445f3bd099
7 changed files with 625 additions and 4 deletions
+39
View File
@@ -0,0 +1,39 @@
"""Domain models — GPS, Camera, Geometry."""
from __future__ import annotations
from typing import Optional
from pydantic import BaseModel, Field
class GPSPoint(BaseModel):
"""WGS-84 coordinate."""
lat: float = Field(..., ge=-90, le=90, description="Latitude")
lon: float = Field(..., ge=-180, le=180, description="Longitude")
class CameraParameters(BaseModel):
"""Intrinsic camera parameters."""
focal_length: float = Field(..., gt=0, description="Focal length in mm")
sensor_width: float = Field(..., gt=0, description="Sensor width in mm")
sensor_height: float = Field(..., gt=0, description="Sensor height in mm")
resolution_width: int = Field(..., gt=0, description="Image width in pixels")
resolution_height: int = Field(..., gt=0, description="Image height in pixels")
principal_point: tuple[float, float] | None = None
distortion_coefficients: list[float] | None = None
class Polygon(BaseModel):
"""Bounding box defined by NW and SE corners."""
north_west: GPSPoint
south_east: GPSPoint
class Geofences(BaseModel):
"""Collection of geofence polygons."""
polygons: list[Polygon] = Field(default_factory=list)
+66
View File
@@ -0,0 +1,66 @@
"""SSE event schemas."""
from __future__ import annotations
from datetime import datetime
from enum import Enum
from typing import Any
from pydantic import BaseModel
from gps_denied.schemas import GPSPoint
class SSEEventType(str, Enum):
"""Server-Sent Event types emitted during flight processing."""
FRAME_PROCESSED = "frame_processed"
FRAME_REFINED = "frame_refined"
SEARCH_EXPANDED = "search_expanded"
USER_INPUT_NEEDED = "user_input_needed"
PROCESSING_BLOCKED = "processing_blocked"
FLIGHT_COMPLETED = "flight_completed"
class FrameProcessedEvent(BaseModel):
"""Payload for frame_processed / frame_refined events."""
frame_id: int
gps: GPSPoint
altitude: float | None = None
confidence: float
heading: float | None = None
timestamp: datetime
class SearchExpandedEvent(BaseModel):
"""Payload for search_expanded event."""
frame_id: int
grid_size: int
message: str | None = None
class UserInputNeededEvent(BaseModel):
"""Payload for user_input_needed event."""
frame_id: int
reason: str
consecutive_failures: int = 0
class FlightCompletedEvent(BaseModel):
"""Payload for flight_completed event."""
frames_total: int
frames_processed: int
duration_seconds: float | None = None
class SSEMessage(BaseModel):
"""Generic SSE message wrapper."""
event: SSEEventType
data: dict[str, Any]
id: str | None = None
retry: int | None = None
+162
View File
@@ -0,0 +1,162 @@
"""Schemas for Flight API requests and responses."""
from __future__ import annotations
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, Field
from gps_denied.schemas import CameraParameters, Geofences, GPSPoint
# ---------------------------------------------------------------------------
# Waypoint
# ---------------------------------------------------------------------------
class Waypoint(BaseModel):
"""Single waypoint with optional refinement flag."""
id: str
lat: float = Field(..., ge=-90, le=90)
lon: float = Field(..., ge=-180, le=180)
altitude: float | None = None
confidence: float = Field(..., ge=0, le=1)
timestamp: datetime
refined: bool = False
# ---------------------------------------------------------------------------
# Flight — Requests
# ---------------------------------------------------------------------------
class FlightCreateRequest(BaseModel):
"""POST /flights body."""
name: str = Field(..., min_length=1, max_length=255)
description: str = ""
start_gps: GPSPoint
rough_waypoints: list[GPSPoint] = Field(default_factory=list)
geofences: Geofences = Field(default_factory=Geofences)
camera_params: CameraParameters
altitude: float = Field(..., gt=0)
class UserFixRequest(BaseModel):
"""POST /flights/{flightId}/user-fix body."""
frame_id: int = Field(..., ge=0)
uav_pixel: tuple[float, float]
satellite_gps: GPSPoint
class ObjectToGPSRequest(BaseModel):
"""POST /flights/{flightId}/frames/{frameId}/object-to-gps body."""
pixel_x: float = Field(..., ge=0)
pixel_y: float = Field(..., ge=0)
# ---------------------------------------------------------------------------
# Flight — Responses
# ---------------------------------------------------------------------------
class FlightResponse(BaseModel):
"""Response after flight creation."""
flight_id: str
status: str
message: str | None = None
created_at: datetime
class FlightDetailResponse(BaseModel):
"""GET /flights/{flightId} response."""
flight_id: str
name: str
description: str
start_gps: GPSPoint
waypoints: list[Waypoint] = Field(default_factory=list)
geofences: Geofences
camera_params: CameraParameters
altitude: float
status: str
frames_processed: int = 0
frames_total: int = 0
created_at: datetime
updated_at: datetime
class FlightStatusResponse(BaseModel):
"""GET /flights/{flightId}/status response."""
status: str
frames_processed: int = 0
frames_total: int = 0
current_frame: int | None = None
current_heading: float | None = None
blocked: bool = False
search_grid_size: int | None = None
message: str | None = None
created_at: datetime
updated_at: datetime
class DeleteResponse(BaseModel):
deleted: bool
flight_id: str
class UpdateResponse(BaseModel):
updated: bool
waypoint_id: str
class BatchUpdateResponse(BaseModel):
success: bool
updated_count: int
failed_ids: list[str] = Field(default_factory=list)
errors: dict[str, str] | None = None
# ---------------------------------------------------------------------------
# Image Batch
# ---------------------------------------------------------------------------
class BatchMetadata(BaseModel):
"""Metadata accompanying an image batch upload."""
start_sequence: int = Field(..., ge=0)
end_sequence: int = Field(..., ge=0)
batch_number: int = Field(..., ge=0)
class BatchResponse(BaseModel):
"""Response after batch upload."""
accepted: bool
sequences: list[int] = Field(default_factory=list)
next_expected: int = 0
message: str | None = None
# ---------------------------------------------------------------------------
# User Fix
# ---------------------------------------------------------------------------
class UserFixResponse(BaseModel):
accepted: bool
processing_resumed: bool = False
message: str | None = None
# ---------------------------------------------------------------------------
# Object → GPS
# ---------------------------------------------------------------------------
class ObjectGPSResponse(BaseModel):
gps: GPSPoint
accuracy_meters: float
frame_id: int
pixel: tuple[float, float]