Imago/app/schemas/__init__.py

230 lines
7.0 KiB
Python

"""
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