from datetime import date, datetime from enum import Enum from typing import Any, Optional from pydantic import BaseModel, EmailStr, Field, validator class DroneRequestStatus(str, Enum): NEW = "NEW" APPROVED = "APPROVED" DENIED = "DENIED" CANCELED = "CANCELED" INFLIGHT = "INFLIGHT" COMPLETED = "COMPLETED" class DroneRequestBase(BaseModel): operator_name: str = Field(..., max_length=128) operator_id: Optional[str] = Field(None, max_length=64) flight_date: Optional[date] = None estimated_takeoff_time: Optional[str] = Field(None, max_length=8) estimated_completion_time: Optional[str] = Field(None, max_length=8) maximum_elevation_ft_amsl: int = Field(..., ge=0) location_description: Optional[str] = None location_latitude: float = Field(..., ge=-90, le=90) location_longitude: float = Field(..., ge=-180, le=180) location_inside_frz: Optional[bool] = None flyer_name: Optional[str] = Field(None, max_length=128) flyer_id: Optional[str] = Field(None, max_length=64) email: EmailStr phone: Optional[str] = Field(None, max_length=32) notes: Optional[str] = None estimated_takeoff_at: datetime estimated_completion_at: datetime prototype_overlay: Optional[dict[str, Any]] = None @validator("operator_name") def validate_operator_name(cls, value): value = value.strip() if not value: raise ValueError("Operator name is required") return value @validator("location_inside_frz", pre=True) def parse_inside_frz(cls, value): if isinstance(value, str): normalized = value.strip().lower() if normalized in {"yes", "true", "1", "y"}: return True if normalized in {"no", "false", "0", "n"}: return False return value class DroneRequestCreate(DroneRequestBase): pass class DroneRequestUpdate(BaseModel): operator_name: Optional[str] = Field(None, max_length=128) operator_id: Optional[str] = Field(None, max_length=64) flyer_name: Optional[str] = Field(None, max_length=128) flyer_id: Optional[str] = Field(None, max_length=64) email: Optional[EmailStr] = None phone: Optional[str] = Field(None, max_length=32) flight_date: Optional[date] = None estimated_takeoff_time: Optional[str] = Field(None, max_length=8) estimated_completion_time: Optional[str] = Field(None, max_length=8) estimated_takeoff_at: Optional[datetime] = None estimated_completion_at: Optional[datetime] = None maximum_elevation_ft_amsl: Optional[int] = Field(None, ge=0) location_description: Optional[str] = None location_latitude: Optional[float] = Field(None, ge=-90, le=90) location_longitude: Optional[float] = Field(None, ge=-180, le=180) location_inside_frz: Optional[bool] = None notes: Optional[str] = None prototype_overlay: Optional[dict[str, Any]] = None operator_comments: Optional[str] = None class DroneRequestStatusUpdate(BaseModel): status: DroneRequestStatus comment: Optional[str] = None class DroneRequestComment(BaseModel): comment: str = Field(..., min_length=1) email_applicant: bool = True class DroneRequest(DroneRequestBase): id: int reference_number: str status: DroneRequestStatus operator_comments: Optional[str] = None submitted_via: str submitted_ip: Optional[str] = None created_by: Optional[str] = None submitted_at: datetime updated_at: datetime status_changed_at: Optional[datetime] = None status_changed_by: Optional[str] = None class Config: from_attributes = True class DroneRequestPublicSubmission(DroneRequest): request_id: str secure_link: str