""" Schémas Pydantic — validation et sérialisation des réponses API """ from datetime import datetime from typing import Any, List, Optional from pydantic import BaseModel, ConfigDict from app.models.image import ProcessingStatus # ───────────────────────────────────────────────────────────── # Sous-schémas imbriqués # ───────────────────────────────────────────────────────────── class ExifGPS(BaseModel): latitude: Optional[float] = None longitude: Optional[float] = None altitude: Optional[float] = None has_gps: bool = False class ExifCamera(BaseModel): make: Optional[str] = None model: Optional[str] = None lens: Optional[str] = None iso: Optional[int] = None aperture: Optional[str] = None shutter_speed: Optional[str] = None focal_length: Optional[str] = None flash: Optional[bool] = None orientation: Optional[int] = None software: Optional[str] = None taken_at: Optional[datetime] = None class ExifData(BaseModel): camera: ExifCamera gps: ExifGPS raw: Optional[dict[str, Any]] = None class OcrData(BaseModel): text: Optional[str] = None language: Optional[str] = None confidence: Optional[float] = None has_text: bool = False class AiData(BaseModel): model_config = ConfigDict(protected_namespaces=()) description: Optional[str] = None tags: Optional[List[str]] = None confidence: Optional[float] = None model_used: Optional[str] = None processed_at: Optional[datetime] = None prompt_tokens: Optional[int] = None output_tokens: Optional[int] = None class ProcessingInfo(BaseModel): status: ProcessingStatus error: Optional[str] = None started_at: Optional[datetime] = None done_at: Optional[datetime] = None # ───────────────────────────────────────────────────────────── # Réponses principales # ───────────────────────────────────────────────────────────── class ImageBase(BaseModel): model_config = ConfigDict(from_attributes=True) id: int uuid: str original_name: str mime_type: Optional[str] = None file_size: Optional[int] = None width: Optional[int] = None height: Optional[int] = None uploaded_at: Optional[datetime] = None processing_status: ProcessingStatus class ImageSummary(ImageBase): """Version allégée pour les listes.""" ai_tags: Optional[List[str]] = None ai_description: Optional[str] = None thumbnail_path: Optional[str] = None class ImageDetail(ImageBase): """Version complète avec toutes les données collectées.""" exif: ExifData ocr: OcrData ai: AiData processing: ProcessingInfo @classmethod def from_orm_full(cls, img) -> "ImageDetail": return cls( id=img.id, uuid=img.uuid, original_name=img.original_name, mime_type=img.mime_type, file_size=img.file_size, width=img.width, height=img.height, uploaded_at=img.uploaded_at, processing_status=img.processing_status, thumbnail_path=img.thumbnail_path, exif=ExifData( camera=ExifCamera( make=img.exif_make, model=img.exif_model, lens=img.exif_lens, iso=img.exif_iso, aperture=img.exif_aperture, shutter_speed=img.exif_shutter, focal_length=img.exif_focal, flash=img.exif_flash, orientation=img.exif_orientation, software=img.exif_software, taken_at=img.exif_taken_at, ), gps=ExifGPS( latitude=img.exif_gps_lat, longitude=img.exif_gps_lon, altitude=img.exif_altitude, has_gps=img.has_gps, ), raw=img.exif_raw, ), ocr=OcrData( text=img.ocr_text, language=img.ocr_language, confidence=img.ocr_confidence, has_text=img.ocr_has_text or False, ), ai=AiData( description=img.ai_description, tags=img.ai_tags, confidence=img.ai_confidence, model_used=img.ai_model_used, processed_at=img.ai_processed_at, prompt_tokens=img.ai_prompt_tokens, output_tokens=img.ai_output_tokens, ), processing=ProcessingInfo( status=img.processing_status, error=img.processing_error, started_at=img.processing_started_at, done_at=img.processing_done_at, ), ) class UploadResponse(BaseModel): id: int uuid: str original_name: str status: ProcessingStatus message: str = "Image uploadée — traitement AI en cours" class StatusResponse(BaseModel): id: int uuid: str status: ProcessingStatus error: Optional[str] = None started_at: Optional[datetime] = None done_at: Optional[datetime] = None class PaginatedImages(BaseModel): total: int page: int page_size: int pages: int items: List[ImageSummary] # Quota tracking storage_used_mb: Optional[float] = None storage_quota_mb: Optional[int] = None quota_pct: Optional[float] = None class DeleteResponse(BaseModel): deleted_id: int message: str = "Image supprimée avec succès" class TagsResponse(BaseModel): tags: List[str] total: int class ReprocessResponse(BaseModel): id: int message: str = "Traitement AI relancé" # ───────────────────────────────────────────────────────────── # AI — Endpoints externes (résumé URL, rédaction) # ───────────────────────────────────────────────────────────── class SummarizeRequest(BaseModel): url: str language: str = "français" class SummarizeResponse(BaseModel): url: str title: Optional[str] = None summary: str tags: List[str] model: str class DraftTaskRequest(BaseModel): description: str context: Optional[str] = None language: str = "français" class DraftTaskResponse(BaseModel): title: str description: str steps: List[str] estimated_time: Optional[str] = None priority: Optional[str] = None