- 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.
736 lines
29 KiB
Markdown
736 lines
29 KiB
Markdown
# Rapport d'amélioration — Imago
|
||
|
||
**Architecture d'un hub centralisé multi-clients**
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Version** | 1.0.0 |
|
||
| **Date** | Février 2026 |
|
||
| **Statut** | Proposition — v1 Initiale |
|
||
| **Contexte** | Backend FastAPI — Hub image multi-clients |
|
||
|
||
---
|
||
|
||
## Table des matières
|
||
|
||
1. [Résumé exécutif](#1-résumé-exécutif)
|
||
2. [Axe 1 — Authentification et autorisation](#2-axe-1--authentification-et-autorisation)
|
||
3. [Axe 2 — Architecture multi-clients (tenants)](#3-axe-2--architecture-multi-clients-tenants)
|
||
4. [Axe 3 — Pipeline asynchrone robuste](#4-axe-3--pipeline-asynchrone-robuste)
|
||
5. [Axe 4 — Stockage et distribution des fichiers](#5-axe-4--stockage-et-distribution-des-fichiers)
|
||
6. [Axe 5 — WebSockets et notifications en temps réel](#6-axe-5--websockets-et-notifications-en-temps-réel)
|
||
7. [Axe 6 — Observabilité et monitoring](#7-axe-6--observabilité-et-monitoring)
|
||
8. [Axe 7 — Versioning de l'API et SDK clients](#8-axe-7--versioning-de-lapi-et-sdk-clients)
|
||
9. [Axe 8 — Tests, qualité de code et CI/CD](#9-axe-8--tests-qualité-de-code-et-cicd)
|
||
10. [Feuille de route proposée](#10-feuille-de-route-proposée)
|
||
11. [Stack technique finale recommandée](#11-stack-technique-finale-recommandée)
|
||
12. [Conclusion](#12-conclusion)
|
||
|
||
---
|
||
|
||
## 1. Résumé exécutif
|
||
|
||
Le backend Imago a été conçu initialement comme un complément à l'interface Shaarli pour gérer les images, l'OCR et les analyses AI. L'objectif a évolué : ce backend doit devenir un **hub centralisé** capable de servir plusieurs applications clientes simultanément.
|
||
|
||
Ce rapport identifie **8 axes d'amélioration** couvrant la sécurité, la scalabilité, la qualité du code et l'expérience développeur. Chaque axe est accompagné de recommandations concrètes, d'exemples de code et d'une estimation d'effort.
|
||
|
||
> **Contexte actuel**
|
||
> - Clients identifiés : Interface Shaarli (client 1), futures applications tierces
|
||
> - Technologies actuelles : FastAPI, SQLAlchemy async, Pillow, Tesseract, Anthropic Claude
|
||
> - Points critiques immédiats : absence d'authentification, pas de gestion multi-clients, pipeline non persistant
|
||
> - Horizon de développement proposé : 3 phases sur 3 mois
|
||
|
||
### 1.1 Synthèse des axes d'amélioration
|
||
|
||
| Axe d'amélioration | Impact principal | Priorité | Effort |
|
||
|---|---|:---:|:---:|
|
||
| Authentification & Autorisation | Sécurité multi-clients | 🔴 Critique | 3–5 j |
|
||
| Gestion multi-clients (tenants) | Isolation des données | 🔴 Critique | 5–7 j |
|
||
| Pipeline asynchrone robuste | Fiabilité du traitement AI | 🟠 Haute | 4–5 j |
|
||
| Stockage & CDN | Performance, scalabilité | 🟠 Haute | 3–4 j |
|
||
| WebSockets & temps réel | Expérience client | 🟡 Moyenne | 2–3 j |
|
||
| Observabilité & Monitoring | Production-ready | 🟡 Moyenne | 3–4 j |
|
||
| API versioning & SDK clients | Contrat API stable | 🟡 Moyenne | 2–3 j |
|
||
| Tests & CI/CD | Qualité et maintenabilité | 🟠 Haute | 4–5 j |
|
||
|
||
---
|
||
|
||
## 2. Axe 1 — Authentification et autorisation
|
||
|
||
### 2.1 Problématique
|
||
|
||
Dans l'état actuel, le backend est entièrement public. N'importe quelle application ou personne ayant accès au réseau peut uploader, lire ou supprimer des images. Cela est incompatible avec un hub servant plusieurs clients distincts.
|
||
|
||
> ⚠️ **Risques actuels**
|
||
> - Accès non contrôlé à l'ensemble des images et métadonnées
|
||
> - Consommation illimitée de tokens AI (coût financier direct)
|
||
> - Suppression accidentelle ou malveillante de données
|
||
|
||
### 2.2 Solution recommandée — API Keys + JWT
|
||
|
||
Pour un hub multi-clients, la combinaison la plus pragmatique est : des **API Keys** pour l'authentification machine-to-machine (clients automatiques, scripts), et des **tokens JWT** pour les sessions utilisateurs interactives.
|
||
|
||
| Mécanisme | Cas d'usage | Avantages |
|
||
|---|---|---|
|
||
| API Key | Shaarli, applications serveur, scripts CLI | Simple, stateless, facile à révoquer par client |
|
||
| JWT (Bearer) | Interface web, utilisateur connecté | Expiration automatique, payload riche |
|
||
| OAuth2 (futur) | Intégration systèmes tiers | Standard industriel, écosystème large |
|
||
|
||
### 2.3 Implémentation
|
||
|
||
Ajouter un modèle `APIClient` en base de données reliant chaque client à ses permissions et sa clé. Injecter une dépendance FastAPI `verify_api_key` qui valide la clé sur chaque requête. Définir des scopes granulaires : `images:read`, `images:write`, `images:delete`, `ai:use`.
|
||
|
||
```python
|
||
# app/models/client.py
|
||
class APIClient(Base):
|
||
__tablename__ = "api_clients"
|
||
|
||
id = Column(UUID, primary_key=True, default=uuid4)
|
||
name = Column(String, nullable=False) # "Shaarli", "App Mobile"
|
||
api_key_hash = Column(String, nullable=False) # SHA-256, jamais en clair
|
||
scopes = Column(JSON, default=list) # ["images:read", "ai:use"]
|
||
is_active = Column(Boolean, default=True)
|
||
created_at = Column(DateTime, default=datetime.utcnow)
|
||
```
|
||
|
||
```python
|
||
# app/dependencies/auth.py
|
||
async def verify_api_key(
|
||
authorization: str = Header(...),
|
||
db: AsyncSession = Depends(get_db)
|
||
) -> APIClient:
|
||
key = authorization.removeprefix("Bearer ").strip()
|
||
key_hash = hashlib.sha256(key.encode()).hexdigest()
|
||
|
||
result = await db.execute(
|
||
select(APIClient).where(APIClient.api_key_hash == key_hash)
|
||
)
|
||
client = result.scalar_one_or_none()
|
||
|
||
if not client or not client.is_active:
|
||
raise HTTPException(status_code=401, detail="Clé API invalide")
|
||
|
||
return client
|
||
```
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Librairies : `python-jose` (JWT), `passlib` (hachage), `itsdangerous` (tokens signés)
|
||
> - Stockage des clés : hachées en base (sha256), **jamais en clair**
|
||
> - Rotation des clés : endpoint `POST /auth/rotate-key` avec période de transition
|
||
> - Audit log : chaque requête authentifiée loggée avec `client_id`, `action`, `timestamp`
|
||
|
||
---
|
||
|
||
## 3. Axe 2 — Architecture multi-clients (tenants)
|
||
|
||
### 3.1 Vision cible
|
||
|
||
Le hub doit servir plusieurs applications clientes avec une **isolation complète des données**. Shaarli est le client 1, mais d'autres applications peuvent s'inscrire et utiliser l'API de manière indépendante. Chaque client possède son propre espace de stockage, ses propres images et ses propres quotas.
|
||
|
||
| Concept | Description |
|
||
|---|---|
|
||
| **Client (Tenant)** | Une application consommatrice de l'API. Identifiée par son API Key. Ex : Shaarli, App Mobile, Script de backup. |
|
||
| **Espace de stockage** | Répertoire dédié : `uploads/{client_id}/`. Les images d'un client ne sont jamais accessibles par un autre. |
|
||
| **Quota** | Limite configurable : nb d'images, espace disque total, tokens AI consommés par mois. |
|
||
| **Plan** | Niveaux de service : `free` (limite stricte), `standard`, `premium` (OCR + AI complets). |
|
||
|
||
### 3.2 Modifications de la base de données
|
||
|
||
Ajouter une table `clients` avec les champs essentiels. Lier chaque image à son client via une clé étrangère `client_id`. Tous les endpoints filtrent automatiquement par `client_id` injecté depuis l'authentification.
|
||
|
||
| Champ | Type | Description |
|
||
|---|---|---|
|
||
| `id` | UUID | Identifiant unique du client |
|
||
| `name` | String | Nom de l'application cliente |
|
||
| `api_key_hash` | String | Hash SHA-256 de la clé (jamais en clair) |
|
||
| `scopes` | JSON | Permissions accordées : `["images:read", "ai:use", ...]` |
|
||
| `quota_images` | Integer | Nombre maximum d'images stockées |
|
||
| `quota_storage_mb` | Integer | Espace disque alloué en mégaoctets |
|
||
| `quota_ai_tokens` | Integer | Tokens AI mensuels autorisés |
|
||
| `is_active` | Boolean | Désactiver un client sans supprimer ses données |
|
||
| `created_at` | DateTime | Date d'inscription |
|
||
|
||
### 3.3 Injection automatique du client dans les endpoints
|
||
|
||
```python
|
||
# Avant (aucun contrôle)
|
||
@router.get("/images")
|
||
async def list_images(db: AsyncSession = Depends(get_db)):
|
||
return await db.execute(select(Image))
|
||
|
||
# Après (filtrage automatique par client)
|
||
@router.get("/images")
|
||
async def list_images(
|
||
db: AsyncSession = Depends(get_db),
|
||
client: APIClient = Depends(verify_api_key), # ← injecté
|
||
):
|
||
query = select(Image).where(Image.client_id == client.id) # ← filtré
|
||
return await db.execute(query)
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Axe 3 — Pipeline asynchrone robuste
|
||
|
||
### 4.1 Limites de l'implémentation actuelle
|
||
|
||
Le pipeline actuel utilise les `BackgroundTasks` de FastAPI. Cette approche est fonctionnelle pour un usage léger mais présente des limites importantes pour un hub en production.
|
||
|
||
> ⚠️ **Problèmes identifiés**
|
||
> - **Perte de tâches** : si le serveur redémarre, toutes les tâches en attente sont perdues (pas de persistance)
|
||
> - **Pas de limite de concurrence** : risque de saturer l'API Anthropic et de dépasser les quotas
|
||
> - **Pas de retry automatique** : un timeout API ou une erreur réseau = tâche perdue définitivement
|
||
> - **Pas de priorité** : un client `premium` attend autant qu'un client `free`
|
||
|
||
### 4.2 Solution — File de messages avec ARQ ou Celery
|
||
|
||
| Solution | Avantages | Inconvénients |
|
||
|---|---|---|
|
||
| **ARQ + Redis** | 100% async, léger, parfait pour FastAPI, retry natif | Nécessite Redis (service supplémentaire) |
|
||
| **Celery + Redis** | Très mature, monitoring Flower intégré, priorités | Plus lourd, mélange sync/async parfois délicat |
|
||
| **Dramatiq** | Simple, middleware extensible | Communauté plus petite qu'Celery |
|
||
|
||
**Recommandation : ARQ avec Redis** pour rester dans un écosystème async pur. Redis sert à la fois de broker de tâches et de cache pour les résultats AI.
|
||
|
||
```python
|
||
# app/workers/image_worker.py
|
||
import arq
|
||
|
||
async def process_image_task(ctx, image_id: int):
|
||
"""Tâche ARQ — remplace le BackgroundTask actuel."""
|
||
db = ctx["db"]
|
||
await process_image_pipeline(image_id, db)
|
||
|
||
class WorkerSettings:
|
||
functions = [process_image_task]
|
||
redis_settings = arq.connections.RedisSettings()
|
||
max_jobs = 10 # concurrence max
|
||
job_timeout = 180 # timeout par tâche (secondes)
|
||
retry_jobs = True
|
||
max_tries = 3 # retry automatique × 3
|
||
```
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Retry configurable : 3 tentatives avec backoff exponentiel (1s, 4s, 16s)
|
||
> - Timeout par tâche : 120 secondes pour l'appel Vision AI, 30s pour l'OCR
|
||
> - Files distinctes : `queue:premium` (priorité haute) et `queue:standard`
|
||
> - Dead-letter queue : tâches échouées après 3 tentatives → alerte + log persistant
|
||
|
||
---
|
||
|
||
## 5. Axe 4 — Stockage et distribution des fichiers
|
||
|
||
### 5.1 Limites du stockage local
|
||
|
||
Le stockage sur disque local fonctionne pour un serveur unique mais devient problématique dès que l'on veut scaler horizontalement ou séparer le serveur applicatif du stockage.
|
||
|
||
> ⚠️ **Problèmes identifiés**
|
||
> - Le disque du serveur est partagé par tous les clients sans isolation
|
||
> - Pas de réplication — une panne disque = perte de toutes les images
|
||
> - Les URLs statiques exposent le chemin réel du fichier sur le serveur
|
||
> - Impossible de scaler sur plusieurs instances (fichiers non partagés)
|
||
|
||
### 5.2 Abstraction du stockage
|
||
|
||
Introduire une interface `StorageBackend` abstraite permettant de basculer entre stockage local et S3 sans modifier le reste du code.
|
||
|
||
```python
|
||
# app/services/storage_backend.py
|
||
from abc import ABC, abstractmethod
|
||
|
||
class StorageBackend(ABC):
|
||
@abstractmethod
|
||
async def save(self, file: bytes, path: str) -> str: ...
|
||
|
||
@abstractmethod
|
||
async def delete(self, path: str) -> None: ...
|
||
|
||
@abstractmethod
|
||
async def get_url(self, path: str, expires_in: int = 900) -> str: ...
|
||
|
||
|
||
class LocalStorage(StorageBackend):
|
||
"""Backend local — développement et serveur unique."""
|
||
async def save(self, file: bytes, path: str) -> str:
|
||
full_path = Path(settings.UPLOAD_DIR) / path
|
||
async with aiofiles.open(full_path, "wb") as f:
|
||
await f.write(file)
|
||
return str(full_path)
|
||
|
||
async def get_url(self, path: str, expires_in: int = 900) -> str:
|
||
# Token HMAC signé avec expiration
|
||
token = signing.dumps({"path": path, "exp": time() + expires_in})
|
||
return f"/files/{token}"
|
||
|
||
|
||
class S3Storage(StorageBackend):
|
||
"""Backend S3/MinIO/R2 — production."""
|
||
async def get_url(self, path: str, expires_in: int = 900) -> str:
|
||
# URL pré-signée AWS native
|
||
return await self.client.generate_presigned_url(
|
||
"get_object", Params={"Bucket": self.bucket, "Key": path},
|
||
ExpiresIn=expires_in
|
||
)
|
||
```
|
||
|
||
| Backend | Usage recommandé | Librairie | Coût |
|
||
|---|---|---|---|
|
||
| Local FileSystem | Développement, serveur unique | `aiofiles` (déjà présent) | Gratuit |
|
||
| MinIO (self-hosted) | Production on-premise | `aioboto3` | Infrastructure |
|
||
| AWS S3 | Production cloud | `aioboto3` | ~0.023$/GB/mois |
|
||
| Cloudflare R2 | Production cloud économique | `aioboto3` (compatible S3) | 0$ egress |
|
||
|
||
### 5.3 URLs signées et sécurité d'accès
|
||
|
||
Remplacer les routes `/static/` par des **URLs signées à durée limitée**. Le client reçoit une URL temporaire valide X minutes.
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Endpoint : `GET /images/{id}/download-url` → retourne une URL signée valide 15 minutes
|
||
> - Paramètre optionnel : `expires_in` (en secondes, max configurable par plan)
|
||
> - Pour S3/R2 : URL pré-signée native. Pour stockage local : token HMAC signé par le serveur
|
||
> - Thumbnail : même mécanisme via `GET /images/{id}/thumbnail-url`
|
||
|
||
---
|
||
|
||
## 6. Axe 5 — WebSockets et notifications en temps réel
|
||
|
||
### 6.1 Problématique du polling
|
||
|
||
Actuellement, les clients doivent interroger `GET /images/{id}/status` régulièrement pour connaître l'avancement du pipeline. Cette approche génère du trafic inutile et ajoute de la latence perceptible.
|
||
|
||
### 6.2 Solution — WebSocket par session d'upload
|
||
|
||
Proposer un endpoint WebSocket que le client connecte après l'upload. Le serveur **pousse** des événements au fur et à mesure de l'avancement du pipeline.
|
||
|
||
**Endpoint :** `WS /ws/pipeline/{image_id}?token=<api_key>`
|
||
|
||
| Événement WebSocket | Payload |
|
||
|---|---|
|
||
| `pipeline.started` | `{ "image_id": 42, "steps": ["exif", "ocr", "ai"] }` |
|
||
| `step.completed` | `{ "step": "exif", "duration_ms": 45, "data": { "camera": "Canon EOS R5" } }` |
|
||
| `step.completed` | `{ "step": "ocr", "duration_ms": 820, "data": { "has_text": true, "preview": "Café de Flore..." } }` |
|
||
| `step.completed` | `{ "step": "ai", "duration_ms": 3200, "data": { "tags": ["café", "paris"], "confidence": 0.97 } }` |
|
||
| `pipeline.done` | `{ "image_id": 42, "total_duration_ms": 4100, "status": "done" }` |
|
||
| `pipeline.error` | `{ "step": "ai", "error": "API timeout", "retry": 1 }` |
|
||
|
||
```python
|
||
# app/routers/websocket.py
|
||
@router.websocket("/ws/pipeline/{image_id}")
|
||
async def pipeline_ws(
|
||
websocket: WebSocket,
|
||
image_id: int,
|
||
token: str = Query(...),
|
||
):
|
||
client = await verify_api_key_ws(token) # auth via query param
|
||
await websocket.accept()
|
||
|
||
# S'abonner aux événements Redis publiés par le pipeline
|
||
async with redis.subscribe(f"pipeline:{image_id}") as channel:
|
||
async for message in channel:
|
||
await websocket.send_json(message)
|
||
if message.get("event") in ("pipeline.done", "pipeline.error"):
|
||
break
|
||
```
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Fallback automatique : si WebSocket non disponible, le client retombe sur le polling REST
|
||
> - FastAPI supporte nativement les WebSockets sans dépendance supplémentaire
|
||
> - Gestion de la déconnexion : événements bufferisés 60s si le client se reconnecte
|
||
> - Redis Pub/Sub : le pipeline publie ses événements, le WebSocket handler s'y abonne
|
||
|
||
---
|
||
|
||
## 7. Axe 6 — Observabilité et monitoring
|
||
|
||
### 7.1 Logs structurés
|
||
|
||
Remplacer les `print()` du code actuel par des **logs structurés en JSON**. Chaque log doit contenir au minimum : `timestamp`, `level`, `client_id`, `image_id`, `action`, `duration_ms`.
|
||
|
||
```python
|
||
# app/logging.py
|
||
import structlog
|
||
|
||
log = structlog.get_logger()
|
||
|
||
# Exemple d'usage dans le pipeline
|
||
log.info(
|
||
"pipeline.step.completed",
|
||
image_id=image.id,
|
||
client_id=client.id,
|
||
step="ocr",
|
||
duration_ms=820,
|
||
has_text=True,
|
||
)
|
||
|
||
# Sortie JSON en production :
|
||
# {"event": "pipeline.step.completed", "image_id": 42, "client_id": "abc",
|
||
# "step": "ocr", "duration_ms": 820, "has_text": true, "timestamp": "2026-02-23T..."}
|
||
```
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Librairie recommandée : `structlog` (compatible FastAPI, format JSON natif)
|
||
> - Middleware de logs : enregistrer chaque requête HTTP avec méthode, path, status, duration, client_id
|
||
> - Niveaux : `DEBUG` (dev), `INFO` (prod normal), `WARNING` (anomalie récupérable), `ERROR` (action requise)
|
||
> - Exportation vers Loki, Datadog, CloudWatch selon infrastructure cible
|
||
|
||
### 7.2 Métriques applicatives
|
||
|
||
Exposer des métriques **Prometheus** sur `/metrics` pour un monitoring en temps réel.
|
||
|
||
| Métrique | Type | Utilité |
|
||
|---|---|---|
|
||
| `hub_images_uploaded_total` | Counter | Volume d'uploads par client |
|
||
| `hub_pipeline_duration_seconds` | Histogram | Temps de traitement p50/p95/p99 |
|
||
| `hub_ai_tokens_consumed_total` | Counter | Suivi des coûts AI par client |
|
||
| `hub_pipeline_errors_total` | Counter | Taux d'erreur par étape |
|
||
| `hub_storage_used_bytes` | Gauge | Espace disque par client |
|
||
| `hub_active_websockets` | Gauge | Connexions WebSocket actives |
|
||
|
||
```python
|
||
# main.py
|
||
from prometheus_fastapi_instrumentator import Instrumentator
|
||
|
||
Instrumentator().instrument(app).expose(app, endpoint="/metrics")
|
||
```
|
||
|
||
### 7.3 Health checks détaillés
|
||
|
||
Améliorer `/health/detailed` pour qu'il vérifie activement chaque composant.
|
||
|
||
```python
|
||
@app.get("/health/detailed")
|
||
async def health_detailed():
|
||
checks = {}
|
||
|
||
# Base de données
|
||
try:
|
||
await db.execute(text("SELECT 1"))
|
||
checks["database"] = {"status": "ok", "latency_ms": 12}
|
||
except Exception as e:
|
||
checks["database"] = {"status": "error", "detail": str(e)}
|
||
|
||
# Tesseract
|
||
checks["tesseract"] = _check_tesseract()
|
||
|
||
# API Anthropic
|
||
checks["anthropic"] = await _check_anthropic_api()
|
||
|
||
# Espace disque
|
||
usage = shutil.disk_usage(settings.UPLOAD_DIR)
|
||
checks["disk"] = {
|
||
"status": "ok" if usage.percent < 85 else "warning",
|
||
"used_pct": round(usage.percent, 1)
|
||
}
|
||
|
||
# File ARQ
|
||
checks["queue"] = await _check_arq_queue()
|
||
|
||
overall = "healthy" if all(c["status"] == "ok" for c in checks.values()) else "degraded"
|
||
return {"status": overall, "checks": checks}
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Axe 7 — Versioning de l'API et SDK clients
|
||
|
||
### 8.1 Versioning des endpoints
|
||
|
||
Un hub servant plusieurs clients ne peut pas modifier ses endpoints sans risquer de casser les intégrations existantes.
|
||
|
||
| Stratégie | Exemple | Recommandation |
|
||
|---|---|---|
|
||
| **URL path** | `/v1/images/upload`, `/v2/images/upload` | ✅ Simple, visible, **recommandé** |
|
||
| Header | `API-Version: 2024-01-15` | Flexible mais moins visible |
|
||
| Query param | `?version=1` | ❌ À éviter, pollue les URLs |
|
||
|
||
Adopter `/api/v1/` comme préfixe dès maintenant. Définir une **politique de dépréciation** : une version est supportée au minimum 12 mois après publication de la suivante.
|
||
|
||
```python
|
||
# main.py
|
||
from fastapi import APIRouter
|
||
|
||
v1 = APIRouter(prefix="/api/v1")
|
||
v1.include_router(images_router)
|
||
v1.include_router(ai_router)
|
||
|
||
app.include_router(v1)
|
||
|
||
# Middleware de dépréciation
|
||
@app.middleware("http")
|
||
async def deprecation_header(request: Request, call_next):
|
||
response = await call_next(request)
|
||
if request.url.path.startswith("/api/v1/"):
|
||
# Ajouter quand v2 sera disponible
|
||
# response.headers["Deprecation"] = "true"
|
||
# response.headers["Sunset"] = "2027-02-01"
|
||
pass
|
||
return response
|
||
```
|
||
|
||
### 8.2 SDK Python officiel
|
||
|
||
Générer un SDK Python à partir des schémas OpenAPI du backend.
|
||
|
||
```bash
|
||
# Génération automatique depuis /openapi.json
|
||
openapi-generator-cli generate \
|
||
-i http://localhost:8000/openapi.json \
|
||
-g python \
|
||
-o ./sdk \
|
||
--package-name imago_client
|
||
```
|
||
|
||
```python
|
||
# Exemple d'utilisation du SDK par les clients
|
||
from imago_client import HubClient
|
||
|
||
client = HubClient(api_key="sk-...", base_url="https://hub.example.com")
|
||
|
||
# Upload avec gestion automatique du pipeline
|
||
result = await client.images.upload("photo.jpg")
|
||
await result.wait_until_done() # WebSocket ou polling selon dispo
|
||
|
||
print(result.ai.description)
|
||
print(result.ai.tags)
|
||
print(result.exif.camera.model)
|
||
```
|
||
|
||
> 💡 **Points d'implémentation**
|
||
> - Package installable : `pip install imago-client`
|
||
> - Fonctionnalités : upload, polling, WebSocket, gestion des erreurs, retry automatique
|
||
> - Publication : PyPI (public) ou Gitea/GitHub Packages (privé)
|
||
|
||
---
|
||
|
||
## 9. Axe 8 — Tests, qualité de code et CI/CD
|
||
|
||
### 9.1 Stratégie de tests
|
||
|
||
| Niveau | Ce qu'on teste | Couverture cible | Outils |
|
||
|---|---|:---:|---|
|
||
| **Unitaires** | Services isolés (EXIF, OCR, storage) | 90%+ | `pytest`, `unittest.mock` |
|
||
| **Intégration** | Endpoints FastAPI + BDD de test | 80%+ | `httpx.AsyncClient`, SQLite in-memory |
|
||
| **End-to-End** | Pipeline complet upload → AI | Scénarios clés | `pytest-asyncio`, mocks API Anthropic |
|
||
| **Performance** | Charge concurrente, temps de réponse | Benchmarks | `locust`, `k6` |
|
||
|
||
```python
|
||
# tests/test_api_integration.py
|
||
import pytest
|
||
from httpx import AsyncClient
|
||
from app.main import app
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_upload_and_pipeline():
|
||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||
# Upload avec API Key de test
|
||
headers = {"Authorization": "Bearer test-key-123"}
|
||
with open("tests/fixtures/photo.jpg", "rb") as f:
|
||
response = await client.post(
|
||
"/api/v1/images/upload",
|
||
files={"file": f},
|
||
headers=headers
|
||
)
|
||
|
||
assert response.status_code == 201
|
||
data = response.json()
|
||
assert data["status"] == "pending"
|
||
|
||
# Vérifier le statut après pipeline (mocké)
|
||
status = await client.get(f"/api/v1/images/{data['id']}/status", headers=headers)
|
||
assert status.json()["status"] in ("processing", "done")
|
||
```
|
||
|
||
### 9.2 Qualité de code
|
||
|
||
| Outil | Rôle | Configuration |
|
||
|---|---|---|
|
||
| `ruff` | Linter ultra-rapide (remplace flake8 + isort) | `ruff check . --fix` |
|
||
| `black` | Formatage automatique | `black . --line-length 100` |
|
||
| `mypy` | Vérification des types statiques | `mypy app/ --strict` |
|
||
| `bandit` | Analyse de sécurité du code Python | `bandit -r app/` |
|
||
| `pre-commit` | Exécute tous les checks avant chaque commit | `.pre-commit-config.yaml` |
|
||
|
||
```yaml
|
||
# .pre-commit-config.yaml
|
||
repos:
|
||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||
rev: v0.3.0
|
||
hooks:
|
||
- id: ruff
|
||
args: [--fix]
|
||
- repo: https://github.com/psf/black
|
||
rev: 24.2.0
|
||
hooks:
|
||
- id: black
|
||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||
rev: v1.8.0
|
||
hooks:
|
||
- id: mypy
|
||
args: [--strict]
|
||
```
|
||
|
||
### 9.3 Pipeline CI/CD
|
||
|
||
```yaml
|
||
# .github/workflows/ci.yml
|
||
name: CI
|
||
|
||
on: [push, pull_request]
|
||
|
||
jobs:
|
||
quality:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- uses: actions/setup-python@v5
|
||
with: { python-version: "3.12" }
|
||
- run: pip install -r requirements-dev.txt
|
||
- run: ruff check .
|
||
- run: black --check .
|
||
- run: mypy app/ --strict
|
||
- run: bandit -r app/ -ll
|
||
|
||
tests:
|
||
runs-on: ubuntu-latest
|
||
services:
|
||
redis:
|
||
image: redis:7
|
||
ports: ["6379:6379"]
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- run: pip install -r requirements.txt -r requirements-dev.txt
|
||
- run: pytest tests/ -v --cov=app --cov-report=xml
|
||
- uses: codecov/codecov-action@v4
|
||
|
||
docker:
|
||
needs: [quality, tests]
|
||
if: github.ref == 'refs/heads/main'
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
- run: docker build -t imago:latest .
|
||
- run: docker push registry.example.com/imago:latest
|
||
```
|
||
|
||
> 💡 **Étapes du pipeline**
|
||
> - **Sur chaque Pull Request** : ruff + mypy + bandit + tests unitaires + tests d'intégration
|
||
> - **Sur merge main** : build image Docker + push registry + déploiement staging automatique
|
||
> - **Sur tag release** : déploiement production avec approbation manuelle
|
||
> - **Rapports** : couverture de tests publiée sur chaque PR, alertes Slack si échec
|
||
|
||
---
|
||
|
||
## 10. Feuille de route proposée
|
||
|
||
Les 8 axes ont été organisés en trois phases. Chaque phase est indépendante et livre de la valeur. Les phases ne se bloquent pas mutuellement, certains travaux peuvent commencer en parallèle.
|
||
|
||
### Phase 1 — Fondations sécurité (Semaines 1–3)
|
||
|
||
| # | Livrable | Axe | Effort | Priorité |
|
||
|:---:|---|:---:|:---:|:---:|
|
||
| 1.1 | Authentification API Keys + JWT avec scopes | Axe 1 | 3–5 j | 🔴 Critique |
|
||
| 1.2 | Modèle clients + isolation des données | Axe 2 | 5–7 j | 🔴 Critique |
|
||
| 1.3 | Rate limiting par client et par endpoint | Axe 1 | 1–2 j | 🟠 Haute |
|
||
| 1.4 | Tests d'intégration auth + multi-tenants | Axe 8 | 2–3 j | 🟠 Haute |
|
||
|
||
**Résultat de la phase 1 :** hub sécurisé, données isolées par client, déployable en production.
|
||
|
||
### Phase 2 — Robustesse et scalabilité (Semaines 4–6)
|
||
|
||
| # | Livrable | Axe | Effort | Priorité |
|
||
|:---:|---|:---:|:---:|:---:|
|
||
| 2.1 | Migration BackgroundTasks → ARQ + Redis | Axe 3 | 4–5 j | 🟠 Haute |
|
||
| 2.2 | Abstraction StorageBackend + support MinIO/S3 | Axe 4 | 3–4 j | 🟠 Haute |
|
||
| 2.3 | URLs signées pour accès aux fichiers | Axe 4 | 1–2 j | 🟠 Haute |
|
||
| 2.4 | Logs structurés (structlog) + métriques Prometheus | Axe 6 | 3–4 j | 🟡 Moyenne |
|
||
| 2.5 | CI/CD complet avec GitHub Actions | Axe 8 | 2–3 j | 🟠 Haute |
|
||
|
||
**Résultat de la phase 2 :** pipeline robuste avec retry, stockage abstrait, observabilité opérationnelle.
|
||
|
||
### Phase 3 — Expérience développeur (Semaines 7–10)
|
||
|
||
| # | Livrable | Axe | Effort | Priorité |
|
||
|:---:|---|:---:|:---:|:---:|
|
||
| 3.1 | WebSocket pipeline temps réel | Axe 5 | 2–3 j | 🟡 Moyenne |
|
||
| 3.2 | API versioning `/api/v1/` + politique de dépréciation | Axe 7 | 1–2 j | 🟡 Moyenne |
|
||
| 3.3 | SDK Python généré + publié sur PyPI | Axe 7 | 3–4 j | 🟡 Moyenne |
|
||
| 3.4 | Dashboard admin (quotas, métriques, clients) | Axe 6 | 4–5 j | 🟢 Faible |
|
||
| 3.5 | Intégration Shaarli complète + documentation | Axe 7 | 2–3 j | 🟠 Haute |
|
||
|
||
**Résultat de la phase 3 :** hub avec SDK, WebSockets et intégration Shaarli finalisée.
|
||
|
||
---
|
||
|
||
## 11. Stack technique finale recommandée
|
||
|
||
La stack suivante est une **évolution directe** de la stack actuelle. Chaque ajout répond à un besoin identifié dans ce rapport. Rien n'est ajouté par effet de mode.
|
||
|
||
| Couche | Librairie | Justification |
|
||
|---|---|---|
|
||
| **Web Framework** | FastAPI + Uvicorn | Déjà en place. Performance async excellente. |
|
||
| **Base de données** | SQLAlchemy async + Alembic | Déjà en place. Migrations propres. |
|
||
| **File de tâches** | ARQ + Redis | Pipeline robuste, retry, persistance, priorités. |
|
||
| **Cache** | Redis (partagé avec ARQ) | Cache résultats AI, sessions WebSocket. |
|
||
| **Authentification** | python-jose + passlib | JWT + hachage des API Keys. |
|
||
| **Stockage fichiers** | Local → MinIO → S3/R2 | Interface abstraite. Migration transparente. |
|
||
| **Traitement images** | Pillow + piexif | Déjà en place. |
|
||
| **OCR** | pytesseract + Tesseract | Déjà en place. |
|
||
| **Vision AI** | Anthropic Claude (httpx) | Déjà en place. Abstraction multi-provider future. |
|
||
| **Logs** | structlog | JSON structuré, compatible Loki/Datadog. |
|
||
| **Métriques** | prometheus-fastapi-instrumentator | Exposition automatique des métriques HTTP. |
|
||
| **Qualité code** | ruff + black + mypy + bandit | Lint, format, types, sécurité. |
|
||
| **Tests** | pytest-asyncio + httpx | Tests unitaires et d'intégration async. |
|
||
| **CI/CD** | GitHub Actions / Gitea CI | Automatisation complète du cycle de vie. |
|
||
|
||
### requirements.txt mis à jour
|
||
|
||
```
|
||
# Existant
|
||
fastapi==0.115.0
|
||
uvicorn[standard]==0.30.6
|
||
sqlalchemy[asyncio]==2.0.35
|
||
alembic==1.13.3
|
||
pydantic-settings==2.5.2
|
||
pillow==10.4.0
|
||
piexif==1.1.3
|
||
pytesseract==0.3.13
|
||
anthropic==0.34.2
|
||
httpx==0.27.2
|
||
beautifulsoup4==4.12.3
|
||
aiofiles==24.1.0
|
||
|
||
# Nouveaux — Phase 1
|
||
python-jose[cryptography]==3.3.0
|
||
passlib[bcrypt]==1.7.4
|
||
slowapi==0.1.9 # rate limiting
|
||
|
||
# Nouveaux — Phase 2
|
||
arq==0.25.0 # file de tâches async
|
||
redis==5.0.8 # client Redis
|
||
aioboto3==13.0.0 # S3/MinIO async
|
||
structlog==24.4.0 # logs structurés
|
||
prometheus-fastapi-instrumentator==0.14.0
|
||
|
||
# Nouveaux — Phase 3
|
||
websockets==13.0 # WebSocket (déjà dans FastAPI)
|
||
```
|
||
|
||
---
|
||
|
||
## 12. Conclusion
|
||
|
||
Le backend Imago dispose d'une **base technique solide**. L'architecture FastAPI + SQLAlchemy async est bien choisie et évolutive. Les services EXIF, OCR et Vision AI sont fonctionnels et bien découpés.
|
||
|
||
La **priorité absolue** est la sécurisation avant tout passage en production multi-clients. L'authentification et l'isolation des données par client (Phase 1) sont des prérequis non négociables. Ces deux axes représentent environ 10 jours de développement.
|
||
|
||
Une fois la sécurité en place, la Phase 2 transforme le backend en un service de production véritable avec un pipeline robuste, un stockage abstrait et un monitoring opérationnel. La Phase 3 améliore l'expérience des équipes qui intègrent le hub.
|
||
|
||
> ✅ **Résumé des horizons**
|
||
> - **10 jours** : Phase 1 complète — hub sécurisé et multi-clients opérationnel
|
||
> - **20 jours** : Phase 2 complète — hub production-ready, robuste et observable
|
||
> - **30 jours** : Phase 3 complète — hub avec SDK, WebSockets et intégration Shaarli finalisée
|
||
|
||
---
|
||
|
||
*Imago — Rapport d'amélioration v1.0.0 — Février 2026*
|