- Implement tests for database generator to ensure proper session handling. - Create tests for EXIF extraction and conversion functions. - Add tests for image-related endpoints, ensuring proper data retrieval and isolation between clients. - Develop tests for OCR functionality, including language detection and text extraction. - Introduce tests for the image processing pipeline, covering success and failure scenarios. - Validate rate limiting functionality and ensure independent counters for different clients. - Implement scraper tests to verify HTML content fetching and error handling. - Add unit tests for various services, including storage and filename generation. - Establish worker entry point for ARQ to handle background image processing tasks.
229 lines
7.0 KiB
Python
229 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):
|
|
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
|