Add comprehensive test suite for image processing and related services
CI / Lint & Format (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled

- 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.
This commit is contained in:
2026-02-24 11:22:10 -05:00
commit cc99fea20a
80 changed files with 9582 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
"""
Modèle SQLAlchemy — Image et métadonnées associées
"""
import enum
from datetime import datetime, timezone
from sqlalchemy import (
Column, Integer, String, Text, DateTime,
JSON, Float, Enum as SAEnum, BigInteger, Boolean, ForeignKey
)
from sqlalchemy.orm import relationship
from app.database import Base
class ProcessingStatus(str, enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
DONE = "done"
ERROR = "error"
class Image(Base):
__tablename__ = "images"
# ── Identité ──────────────────────────────────────────────
id = Column(Integer, primary_key=True, index=True)
uuid = Column(String(36), unique=True, index=True, nullable=False)
# ── Client (multi-tenant) ─────────────────────────────────
client_id = Column(String(36), ForeignKey("api_clients.id"), nullable=False, index=True)
client = relationship("APIClient", back_populates="images")
# ── Fichier ───────────────────────────────────────────────
original_name = Column(String(512), nullable=False)
filename = Column(String(512), nullable=False) # nom sur disque (uuid-based)
file_path = Column(String(1024), nullable=False)
thumbnail_path = Column(String(1024))
mime_type = Column(String(128))
file_size = Column(BigInteger) # bytes
width = Column(Integer)
height = Column(Integer)
uploaded_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
# ── Statut du pipeline AI ─────────────────────────────────
processing_status = Column(
SAEnum(ProcessingStatus),
default=ProcessingStatus.PENDING,
nullable=False,
index=True
)
processing_error = Column(Text)
processing_started_at = Column(DateTime)
processing_done_at = Column(DateTime)
# ── Métadonnées EXIF ──────────────────────────────────────
exif_raw = Column(JSON) # dict complet brut
exif_make = Column(String(256)) # Appareil — fabricant
exif_model = Column(String(256)) # Appareil — modèle
exif_lens = Column(String(256))
exif_taken_at = Column(DateTime) # DateTimeOriginal EXIF
exif_gps_lat = Column(Float)
exif_gps_lon = Column(Float)
exif_altitude = Column(Float)
exif_iso = Column(Integer)
exif_aperture = Column(String(32)) # ex: "f/2.8"
exif_shutter = Column(String(32)) # ex: "1/250"
exif_focal = Column(String(32)) # ex: "50mm"
exif_flash = Column(Boolean)
exif_orientation = Column(Integer)
exif_software = Column(String(256))
# ── OCR ───────────────────────────────────────────────────
ocr_text = Column(Text)
ocr_language = Column(String(64))
ocr_confidence = Column(Float) # 0.0 – 1.0
ocr_has_text = Column(Boolean, default=False)
# ── AI Vision ─────────────────────────────────────────────
ai_description = Column(Text)
ai_tags = Column(JSON) # ["nature", "paysage", ...]
ai_confidence = Column(Float) # score de confiance global
ai_model_used = Column(String(128))
ai_processed_at = Column(DateTime)
ai_prompt_tokens = Column(Integer)
ai_output_tokens = Column(Integer)
def __repr__(self):
return f"<Image id={self.id} name={self.original_name} status={self.processing_status}>"
@property
def has_gps(self) -> bool:
return self.exif_gps_lat is not None and self.exif_gps_lon is not None
@property
def dimensions(self) -> str | None:
if self.width and self.height:
return f"{self.width}x{self.height}"
return None