# Prompt Claude Code — Phase 3 : Expérience développeur
# Modèle : claude-opus-4-6
# Usage : claude --model claude-opus-4-6 -p "$(cat PROMPT_PHASE3_claude-opus.md)"
# ou coller directement dans une session Claude Code interactive
---
Tu es un ingénieur backend senior Python spécialisé en conception d'APIs publiques, expérience développeur (DX) et intégration de systèmes. Tu travailles sur le projet Imago pour livrer sa phase finale : rendre ce hub aussi simple et agréable à intégrer que possible pour ses clients.
Les Phases 1 (sécurité, multi-tenants, rate limiting) et Phase 2 (ARQ + Redis, StorageBackend abstrait, structlog, Prometheus, CI/CD) sont entièrement terminées et validées. Tu prends le relais pour la dernière phase : WebSockets temps réel, versioning de l'API, SDK Python officiel, dashboard admin et intégration complète avec Shaarli.
## État du projet après Phase 1 et Phase 2
### Stack complète en place
```
# Phase 0 — Base
fastapi==0.115.0 # framework web async
uvicorn[standard]==0.30.6 # serveur ASGI
sqlalchemy[asyncio]==2.0.35 # ORM async
alembic==1.13.3 # migrations
pydantic-settings==2.5.2 # config depuis .env
pillow==10.4.0 # traitement images + thumbnails
piexif==1.1.3 # extraction EXIF avancée
pytesseract==0.3.13 # OCR Tesseract
anthropic==0.34.2 / httpx==0.27.2 # Vision AI Claude
beautifulsoup4==4.12.3 # scraping web
aiofiles==24.1.0 # I/O fichiers async
# Phase 1 — Sécurité
python-jose[cryptography]==3.3.0 # JWT
passlib[bcrypt]==1.7.4 # hachage API Keys
slowapi==0.1.9 # rate limiting par client/plan
# Phase 2 — Robustesse
arq==0.25.0 # file de tâches async persistante
redis==5.0.8 # client Redis (Pub/Sub + ARQ)
aioboto3==13.0.0 # stockage S3/MinIO/R2 async
structlog==24.4.0 # logs JSON structurés
prometheus-fastapi-instrumentator==0.14.0 # métriques Prometheus
```
### Structure complète du projet (après Phase 2)
```
imago/
├── app/
│ ├── main.py # App FastAPI, lifespan, middlewares, /metrics, /health
│ ├── config.py # Settings complets (auth, redis, S3, logging, quotas)
│ ├── database.py # Engine SQLAlchemy async + session factory
│ ├── logging_config.py # Configuration structlog JSON
│ ├── metrics.py # Compteurs/Histogrammes Prometheus custom
│ ├── models/
│ │ ├── image.py # Image (client_id FK, EXIF, OCR, AI, statut pipeline)
│ │ └── client.py # APIClient (id, name, hash, scopes, plan, quotas, storage_used)
│ ├── schemas/
│ │ └── __init__.py # Schémas Pydantic complets avec quotas
│ ├── dependencies/
│ │ └── auth.py # verify_api_key, require_scope, get_current_client
│ ├── middleware/
│ │ ├── rate_limit.py # Rate limiting par plan (slowapi)
│ │ └── logging_middleware.py # Logs HTTP structurés par requête
│ ├── routers/
│ │ ├── auth.py # CRUD clients, rotation de clé (/auth/*)
│ │ ├── images.py # Endpoints images filtrés par client (/images/*)
│ │ ├── ai.py # Endpoints AI (/ai/summarize, /ai/draft-task)
│ │ └── files.py # Serveur fichiers signés (/files/signed/{token})
│ ├── services/
│ │ ├── storage_backend.py # ABC StorageBackend + LocalStorage + S3Storage
│ │ ├── storage.py # Orchestration upload/delete avec quota check
│ │ ├── exif_service.py # Extraction EXIF (piexif)
│ │ ├── ocr_service.py # OCR Tesseract
│ │ ├── ai_vision.py # Vision AI + summarize_url + draft_task
│ │ ├── scraper.py # Scraping web BeautifulSoup
│ │ └── pipeline.py # Orchestration EXIF→OCR→AI + publication Redis Pub/Sub
│ └── workers/
│ ├── image_worker.py # Worker ARQ (process_image_task, WorkerSettings)
│ └── redis_client.py # Pool Redis async partagé
├── tests/
│ ├── conftest.py # Fixtures : test_db, client_a/b, auth_headers, redis_mock
│ ├── test_services.py # Tests unitaires services (EXIF, OCR, storage)
│ ├── test_auth.py # Tests authentification et scopes
│ ├── test_isolation.py # Tests isolation multi-tenants
│ ├── test_rate_limit.py # Tests rate limiting par plan
│ ├── test_pipeline_arq.py # Tests worker ARQ (enqueue, retry, dead-letter)
│ ├── test_storage.py # Tests StorageBackend + quota + URLs signées
│ └── test_observability.py # Tests /metrics et /health/detailed
├── alembic/
│ └── versions/ # Migrations : images, api_clients, storage_used
├── worker.py # Point d'entrée : python worker.py
├── run.py # Point d'entrée serveur : python run.py
├── Makefile # make dev | test | ci | lint | docker-up
├── pyproject.toml # Config ruff, black, mypy, pytest, coverage
├── requirements.txt # Dépendances production
├── requirements-dev.txt # Dépendances dev (pytest, ruff, mypy, bandit...)
├── .pre-commit-config.yaml # Hooks pre-commit
├── .github/workflows/ci.yml # Pipeline CI/CD (quality + tests + docker + deploy)
├── Dockerfile
└── docker-compose.yml # backend + worker + redis + minio
```
### Ce qui manque encore (cible de la Phase 3)
**WebSockets absents** : le pipeline publie déjà sur Redis Pub/Sub (Phase 2), mais aucun endpoint WebSocket n'expose ces événements aux clients. Ils doivent encore poller `GET /images/{id}/status`.
**API non versionnée** : tous les endpoints sont sous `/images/`, `/ai/`, `/auth/`. Si l'on change un contrat, tous les clients cassent. Aucune politique de dépréciation.
**Pas de SDK** : chaque client doit réimplémenter l'upload, le polling, la gestion des erreurs, le retry. Shaarli devra faire de même.
**Dashboard admin absent** : aucune interface pour superviser les clients, leurs quotas et la santé du hub. Les opérations admin passent par des appels curl directs.
**Intégration Shaarli non finalisée** : Shaarli est le premier client officiel mais l'intégration n'est pas documentée, pas testée de bout en bout, et aucun exemple de code n'existe.
## Mission : implémenter la Phase 3 — Expérience développeur
Tu dois réaliser les 5 livrables suivants dans l'ordre indiqué. Chaque livrable doit être **complet, testé et fonctionnel** avant de passer au suivant.
---
### Livrable 3.1 — WebSockets pipeline temps réel (priorité MOYENNE)
**Objectif** : exposer les événements Redis Pub/Sub aux clients via WebSocket, éliminant complètement le besoin de polling.
**Aucune nouvelle dépendance** : FastAPI supporte nativement les WebSockets via `websockets` (déjà inclus dans `uvicorn[standard]`).
**Ce qui doit être créé ou modifié** :
1. `app/routers/websocket.py` — Nouveau router WebSocket :
**Endpoint principal** :
```
WS /ws/pipeline/{image_id}?token=
```
- Authentification via query param `token` (pas de header Authorization sur WebSocket)
- Valider que l'image appartient au client authentifié avant d'accepter la connexion
- S'abonner au channel Redis `pipeline:{image_id}`
- Pusher chaque message JSON reçu vers le client WebSocket
- Fermer proprement après réception de `pipeline.done` ou `pipeline.error`
- Gérer la déconnexion du client : si le client se déconnecte avant la fin, se désabonner proprement de Redis
**Endpoint de liste des pipelines actifs** (admin) :
```
WS /ws/admin/monitor?token=
```
- Nécessite le scope `admin`
- Pusher un événement à chaque fois qu'un pipeline démarre ou se termine sur n'importe quelle image
**Buffer de reconnexion** :
- Stocker les 10 derniers événements d'un pipeline dans Redis (clé `pipeline:buffer:{image_id}`, TTL 60s)
- Si un client se connecte après que le pipeline a démarré, envoyer d'abord les événements bufferisés puis continuer en live
**Format exact des messages** (doit correspondre à ce que publie `pipeline.py`) :
```json
{ "event": "pipeline.started", "image_id": 42, "steps": ["exif", "ocr", "ai"], "timestamp": "..." }
{ "event": "step.completed", "image_id": 42, "step": "exif", "duration_ms": 45, "data": { "camera": "Canon EOS R5", "has_gps": true } }
{ "event": "step.completed", "image_id": 42, "step": "ocr", "duration_ms": 820, "data": { "has_text": true, "preview": "Café de..." } }
{ "event": "step.completed", "image_id": 42, "step": "ai", "duration_ms": 3200, "data": { "tags": ["café", "paris"], "confidence": 0.97 } }
{ "event": "pipeline.done", "image_id": 42, "total_duration_ms": 4065, "status": "done" }
{ "event": "pipeline.error", "image_id": 42, "step": "ai", "error": "API timeout", "retry_attempt": 1 }
```
**Gestion des erreurs** :
- Si l'image est déjà en statut `done` quand le client se connecte → envoyer immédiatement un message `pipeline.done` synthétique et fermer
- Si l'image est en statut `error` → envoyer `pipeline.error` synthétique et fermer
- Si l'image n'appartient pas au client → fermer avec code WebSocket 4003 (Forbidden)
2. `app/metrics.py` — Ajouter la gauge `hub_active_websockets` :
- Incrémenter à l'ouverture d'une connexion WebSocket
- Décrémenter à la fermeture (succès ou erreur)
3. `app/main.py` — Monter le router WebSocket :
```python
from app.routers.websocket import router as ws_router
app.include_router(ws_router) # pas de préfixe /api/v1 — WS garde son propre préfixe /ws
```
4. `tests/test_websocket.py` — Tests complets :
- ✅ Connexion sans token → refus immédiat (code 4001)
- ✅ Connexion avec token valide → acceptée
- ✅ Image appartenant à un autre client → refus (code 4003)
- ✅ Image déjà `done` → reçoit `pipeline.done` synthétique et connexion fermée proprement
- ✅ Réception des événements dans l'ordre : started → step×3 → done
- ✅ Déconnexion client en cours de pipeline → désabonnement Redis propre (pas de goroutine/tâche en fuite)
- ✅ Reconnexion → buffer de 60s rejoué
Pour les tests, utiliser `httpx` avec `ASGITransport` ou le client WebSocket de `starlette.testclient` (`with client.websocket_connect(...)`)
---
### Livrable 3.2 — API versioning `/api/v1/` + politique de dépréciation (priorité MOYENNE)
**Objectif** : structurer l'API sous un préfixe versionné pour garantir la stabilité des contrats pour tous les clients intégrés.
**Ce qui doit être créé ou modifié** :
1. `app/main.py` — Restructurer le montage des routers :
**Avant** (sans versioning) :
```python
app.include_router(images_router)
app.include_router(ai_router)
app.include_router(auth_router)
```
**Après** (versioning propre) :
```python
from fastapi import APIRouter
# Router versionné v1
api_v1 = APIRouter(prefix="/api/v1", tags=["v1"])
api_v1.include_router(images_router)
api_v1.include_router(ai_router)
api_v1.include_router(auth_router)
# WebSocket et fichiers signés — pas de préfixe /api/v1
app.include_router(ws_router) # /ws/*
app.include_router(files_router) # /files/*
# Routes non versionnées (infra)
# GET / → info
# GET /health
# GET /health/detailed
# GET /metrics
app.include_router(api_v1)
```
2. `app/middleware/versioning.py` — Middleware de versioning :
- Ajouter le header `X-API-Version: v1` sur toutes les réponses des routes `/api/v1/*`
- Préparer (en commentaire) l'ajout futur de `Deprecation: true` et `Sunset: ` quand v2 existera
- Logger un warning structlog si une route `/api/v1/*` est appelée après la date de sunset (configurable)
3. `app/config.py` — Ajouter :
```python
API_V1_SUNSET_DATE: str = "" # vide = pas de sunset, sinon "2027-06-01"
```
4. `app/main.py` — Mettre à jour la documentation OpenAPI :
```python
app = FastAPI(
title="Imago API",
version="1.0.0",
description="""
## Imago — API v1
Hub centralisé de gestion d'images avec pipeline AI automatique.
### Authentification
Toutes les routes `/api/v1/*` nécessitent un header :
```
Authorization: Bearer
```
### Versioning
Cette API respecte le versioning sémantique. La version actuelle est **v1**.
Les changements breaking seront introduits dans `/api/v2/` avec un préavis minimum de 12 mois.
### Rate Limiting
Les limites varient selon le plan :
- `free` : 20 uploads/h, 50 requêtes AI/h
- `standard` : 100 uploads/h, 200 requêtes AI/h
- `premium` : 500 uploads/h, 1000 requêtes AI/h
""",
openapi_tags=[
{"name": "Images", "description": "Gestion des images et pipeline AI"},
{"name": "Intelligence Artificielle", "description": "Résumé d'URL, rédaction de tâches"},
{"name": "Authentification", "description": "Gestion des clients API"},
{"name": "WebSocket", "description": "Notifications temps réel du pipeline"},
{"name": "Santé", "description": "Health checks et métriques"},
],
)
```
5. `tests/test_versioning.py` — Tests :
- ✅ Tous les endpoints images/ai/auth répondent sous `/api/v1/`
- ✅ Les anciennes URLs sans préfixe retournent `HTTP 404`
- ✅ Header `X-API-Version: v1` présent sur toutes les réponses `/api/v1/*`
- ✅ Header absent sur `/health`, `/metrics`, `/ws/*`
---
### Livrable 3.3 — SDK Python officiel `imago-client` (priorité MOYENNE)
**Objectif** : fournir un SDK Python idiomatique que n'importe quel client (Shaarli, scripts, autres apps) peut installer avec `pip install imago-client` et utiliser sans connaître les détails de l'API REST.
**Architecture du SDK** : le SDK est un **package Python séparé** dans le même dépôt (monorepo), dans le dossier `sdk/`.
**Structure du SDK** :
```
sdk/
├── pyproject.toml # Package config (build-system, metadata)
├── README.md # Documentation d'utilisation du SDK
├── imago_client/
│ ├── __init__.py # Exports publics : HubClient, HubImage, HubError
│ ├── client.py # HubClient — point d'entrée principal
│ ├── resources/
│ │ ├── images.py # ImagesResource — méthodes images
│ │ ├── ai.py # AIResource — résumé URL, tâches
│ │ └── auth.py # AuthResource — gestion clients (admin)
│ ├── models.py # Dataclasses : HubImage, HubExif, HubOcr, HubAI
│ ├── websocket.py # PipelineStream — suivi temps réel
│ ├── exceptions.py # HubError, AuthError, QuotaError, NotFoundError
│ └── utils.py # Retry, timeout, helpers
└── tests/
├── test_client.py
├── test_images.py
└── test_websocket_sdk.py
```
**Ce qui doit être créé** :
1. `sdk/imago_client/exceptions.py` :
```python
class HubError(Exception):
"""Erreur de base du SDK."""
def __init__(self, message: str, status_code: int | None = None):
self.status_code = status_code
super().__init__(message)
class AuthError(HubError):
"""Clé API invalide ou scope manquant (401/403)."""
class QuotaError(HubError):
"""Quota dépassé (413 ou 429)."""
class NotFoundError(HubError):
"""Ressource introuvable (404)."""
class PipelineError(HubError):
"""Erreur lors du traitement AI d'une image."""
```
2. `sdk/imago_client/models.py` — Dataclasses des réponses :
```python
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class HubExif:
camera_make: str | None = None
camera_model: str | None = None
taken_at: datetime | None = None
gps_latitude: float | None = None
gps_longitude: float | None = None
iso: int | None = None
aperture: str | None = None
@dataclass
class HubOcr:
has_text: bool = False
text: str | None = None
language: str | None = None
confidence: float | None = None
@dataclass
class HubAI:
description: str | None = None
tags: list[str] = field(default_factory=list)
confidence: float | None = None
model_used: str | None = None
@dataclass
class HubImage:
id: int
uuid: str
original_name: str
status: str # pending | processing | done | error
exif: HubExif = field(default_factory=HubExif)
ocr: HubOcr = field(default_factory=HubOcr)
ai: HubAI = field(default_factory=HubAI)
width: int | None = None
height: int | None = None
file_size: int | None = None
uploaded_at: datetime | None = None
```
3. `sdk/imago_client/websocket.py` — `PipelineStream` :
```python
class PipelineStream:
"""Suivi temps réel du pipeline via WebSocket avec fallback polling."""
async def __aiter__(self) -> AsyncIterator[PipelineEvent]:
"""Itère sur les événements du pipeline jusqu'à done/error."""
async def wait_until_done(self, timeout: float = 120.0) -> HubImage:
"""Attend la fin du pipeline et retourne l'image complète."""
async def _try_websocket(self) -> bool:
"""Tente la connexion WebSocket. Retourne False si indisponible."""
async def _fallback_polling(self, interval: float = 2.0) -> None:
"""Polling de /status si WebSocket indisponible."""
```
4. `sdk/imago_client/resources/images.py` — `ImagesResource` :
```python
class ImagesResource:
async def upload(
self,
file: str | Path | bytes | BinaryIO,
*,
filename: str | None = None,
) -> PipelineStream:
"""Upload une image et retourne un stream de suivi du pipeline."""
async def get(self, image_id: int) -> HubImage:
"""Récupère les détails complets d'une image."""
async def list(
self,
*,
page: int = 1,
page_size: int = 20,
tag: str | None = None,
status: str | None = None,
search: str | None = None,
) -> tuple[list[HubImage], int]: # (images, total)
"""Liste les images avec pagination et filtres."""
async def delete(self, image_id: int) -> None:
"""Supprime une image."""
async def get_download_url(self, image_id: int, expires_in: int = 900) -> str:
"""Retourne une URL signée pour télécharger l'image."""
async def reprocess(self, image_id: int) -> None:
"""Relance le pipeline AI sur une image existante."""
```
5. `sdk/imago_client/client.py` — `HubClient` :
```python
class HubClient:
def __init__(
self,
api_key: str,
base_url: str = "http://localhost:8000",
*,
timeout: float = 30.0,
max_retries: int = 3,
retry_backoff: float = 1.0,
):
self.images = ImagesResource(self)
self.ai = AIResource(self)
self.auth = AuthResource(self) # admin uniquement
async def __aenter__(self) -> "HubClient": ...
async def __aexit__(self, *args) -> None: ...
# Usage :
# async with HubClient(api_key="sk-...") as hub:
# stream = await hub.images.upload("photo.jpg")
# image = await stream.wait_until_done()
# print(image.ai.description)
```
6. `sdk/imago_client/resources/ai.py` — `AIResource` :
```python
class AIResource:
async def summarize_url(self, url: str, language: str = "français") -> dict:
"""Résumé AI d'une URL web."""
async def draft_task(
self, description: str, context: str | None = None, language: str = "français"
) -> dict:
"""Génère une tâche structurée depuis une description libre."""
```
7. `sdk/pyproject.toml` :
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "imago-client"
version = "1.0.0"
description = "SDK Python officiel pour le Imago"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.27.0",
"websockets>=13.0",
"pydantic>=2.0",
]
[project.optional-dependencies]
dev = ["pytest>=8.0", "pytest-asyncio", "respx"] # respx pour mocker httpx
```
8. `sdk/README.md` — Documentation complète avec exemples :
```markdown
# imago-client
## Installation
pip install imago-client
## Usage rapide
```python
import asyncio
from imago_client import HubClient
async def main():
async with HubClient(api_key="sk-...", base_url="https://hub.example.com") as hub:
# Upload et attente du pipeline
stream = await hub.images.upload("photo.jpg")
image = await stream.wait_until_done(timeout=120)
print(f"Description : {image.ai.description}")
print(f"Tags : {', '.join(image.ai.tags)}")
print(f"Appareil : {image.exif.camera_make} {image.exif.camera_model}")
# Résumé d'URL
result = await hub.ai.summarize_url("https://example.com/article")
print(result["summary"])
```
9. `sdk/tests/` — Tests du SDK avec `respx` pour mocker httpx :
- ✅ `upload()` retourne un `PipelineStream` valide
- ✅ `wait_until_done()` complète via WebSocket
- ✅ `wait_until_done()` complète via polling si WebSocket indisponible
- ✅ `HubError` levée sur HTTP 4xx/5xx
- ✅ `AuthError` sur 401, `QuotaError` sur 413/429, `NotFoundError` sur 404
- ✅ Retry automatique sur erreurs 5xx (max_retries fois)
- ✅ Context manager `async with HubClient(...) as hub:` ferme les connexions proprement
---
### Livrable 3.4 — Dashboard admin (priorité FAIBLE)
**Objectif** : fournir une interface web légère permettant de superviser le hub sans passer par curl — visualiser les clients, leurs quotas, les métriques du pipeline et la santé des composants.
**Choix technique** : **pas de framework frontend séparé**. Le dashboard est une interface HTML/JS servie directement par FastAPI via `Jinja2Templates`. Simple, sans build step, déployé avec le backend.
**Nouvelle dépendance** :
```
jinja2==3.1.4
```
**Ce qui doit être créé** :
1. `app/routers/admin.py` — Router admin avec pages HTML et endpoints JSON :
**Pages HTML** (rendu Jinja2, scope `admin` requis) :
- `GET /admin/` → Dashboard overview (métriques globales, santé)
- `GET /admin/clients` → Liste des clients avec quotas et consommation
- `GET /admin/clients/{id}` → Détail d'un client (images, tokens AI, activité)
- `GET /admin/queue` → État de la file ARQ (jobs en attente, en cours, échoués)
- `GET /admin/storage` → Consommation disque par client
**Endpoints JSON** (appelés par le JS du dashboard) :
- `GET /admin/api/stats` → Statistiques globales (total images, tokens AI, taille stockage)
- `GET /admin/api/clients` → Liste clients avec métriques temps réel
- `GET /admin/api/queue/status` → Jobs ARQ en cours et en attente
- `POST /admin/api/clients/{id}/toggle` → Activer/désactiver un client
- `POST /admin/api/clients/{id}/reset-quota` → Réinitialiser les compteurs de quota
2. `app/templates/admin/` — Templates Jinja2 :
**`base.html`** — Layout commun :
- Sidebar avec navigation (Dashboard, Clients, Queue, Stockage)
- Header avec nom du hub et version
- Zone de contenu principale
- Style minimal avec CSS vanilla (pas de dépendance externe — tout inline ou dans `static/`)
**`dashboard.html`** — Page principale :
- Cartes métriques : total images, clients actifs, tokens AI consommés ce mois, espace utilisé
- Graphique simple (Chart.js CDN) : uploads par jour sur 30 jours
- Tableau santé des composants (BDD, Redis, ARQ, Tesseract, Anthropic) avec badge coloré
**`clients.html`** — Liste des clients :
- Tableau : nom, plan, images, stockage utilisé/quota, tokens AI ce mois, statut actif
- Badge coloré par plan (free = gris, standard = bleu, premium = or)
- Bouton activer/désactiver inline (appel AJAX vers `/admin/api/clients/{id}/toggle`)
- Lien vers le détail de chaque client
**`client_detail.html`** — Détail d'un client :
- En-tête : nom, plan, API key (masquée `sk-...****`), date de création
- Jauges de quota : images (X/Y), stockage (X MB / Y MB), tokens AI (X/Y)
- Tableau des 20 dernières images avec statut pipeline et tags AI
**`queue.html`** — État de la file ARQ :
- Compteurs : jobs en attente, en cours, réussis, échoués (dernières 24h)
- Tableau des jobs actifs : image_id, client, étape en cours, durée
- Tableau des jobs échoués récents avec message d'erreur
3. `app/static/admin/` — Fichiers statiques du dashboard :
- `style.css` — styles du dashboard (palette cohérente avec le projet)
- `dashboard.js` — rafraîchissement automatique des métriques toutes les 30s via `fetch`
4. `app/main.py` — Monter le dashboard :
```python
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
templates = Jinja2Templates(directory="app/templates")
app.mount("/admin/static", StaticFiles(directory="app/static/admin"), name="admin_static")
app.include_router(admin_router)
```
5. `tests/test_admin.py` — Tests du dashboard :
- ✅ `GET /admin/` sans scope `admin` → HTTP 403
- ✅ `GET /admin/` avec scope `admin` → HTTP 200 avec HTML valide
- ✅ `GET /admin/api/stats` → JSON avec les bons champs
- ✅ `POST /admin/api/clients/{id}/toggle` → change `is_active`
- ✅ Client désactivé → ses requêtes API retournent 401
---
### Livrable 3.5 — Intégration Shaarli complète + documentation (priorité HAUTE)
**Objectif** : finaliser l'intégration entre Shaarli (premier client officiel) et le hub, avec une documentation technique complète qui permettrait à n'importe quel développeur d'intégrer le hub en moins d'une heure.
**Ce qui doit être créé** :
1. `integration/shaarli/` — Module d'intégration Shaarli :
**`integration/shaarli/hub_plugin.py`** — Plugin Python utilisable depuis Shaarli :
```python
"""
Plugin d'intégration Shaarli ↔ Imago.
Installe dans Shaarli via : pip install imago-client
"""
import asyncio
from pathlib import Path
from imago_client import HubClient
class ShaarliHubPlugin:
"""
Enrichit les bookmarks Shaarli avec des images analysées par le hub.
Usage :
plugin = ShaarliHubPlugin(api_key="sk-...", hub_url="http://hub:8000")
# Sur ajout d'un bookmark avec image :
result = asyncio.run(plugin.process_bookmark_image("photo.jpg", bookmark_id=42))
"""
def __init__(self, api_key: str, hub_url: str):
self.hub_url = hub_url
self.api_key = api_key
async def process_bookmark_image(
self, image_path: str | Path, bookmark_id: int
) -> dict:
"""Upload une image associée à un bookmark et attend l'analyse AI."""
async with HubClient(self.api_key, self.hub_url) as hub:
stream = await hub.images.upload(image_path)
image = await stream.wait_until_done(timeout=120)
return {
"bookmark_id": bookmark_id,
"image_id": image.id,
"description": image.ai.description,
"tags": image.ai.tags,
"ocr_text": image.ocr.text,
}
async def enrich_bookmark_url(self, url: str) -> dict:
"""Génère un résumé AI d'un lien web pour enrichir un bookmark."""
async with HubClient(self.api_key, self.hub_url) as hub:
return await hub.ai.summarize_url(url)
```
**`integration/shaarli/config_example.env`** :
```bash
# Configuration du plugin Imago
HUB_URL=http://imago:8000
HUB_API_KEY=sk-votre-cle-api-ici
HUB_TIMEOUT=120
```
**`integration/shaarli/example_usage.py`** — Exemples commentés de tous les cas d'usage :
- Upload d'image depuis un bookmark
- Résumé d'URL pour un nouveau lien
- Recherche d'images par tag
- Gestion des erreurs (quota dépassé, timeout, réseau)
2. `docs/` — Documentation technique complète :
**`docs/getting-started.md`** — Guide de démarrage en 5 minutes :
```markdown
# Démarrage rapide
## 1. Lancer le hub
git clone ...
cp .env.example .env
# Éditer .env : ANTHROPIC_API_KEY=sk-ant-...
docker-compose up -d
# Hub disponible sur http://localhost:8000
## 2. Créer votre premier client
curl -X POST http://localhost:8000/api/v1/auth/clients \
-H "Authorization: Bearer $ADMIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Mon App", "plan": "standard", "scopes": ["images:read", "images:write", "ai:use"]}'
# → { "api_key": "sk-VOTRE-CLE-ICI" } ← la clé n'est affichée qu'une seule fois
## 3. Uploader votre première image
curl -X POST http://localhost:8000/api/v1/images/upload \
-H "Authorization: Bearer sk-VOTRE-CLE-ICI" \
-F "file=@photo.jpg"
# → { "id": 1, "status": "pending", ... }
## 4. Suivre le pipeline en temps réel (WebSocket)
# Voir docs/websocket.md
## 5. Récupérer les résultats
curl http://localhost:8000/api/v1/images/1 \
-H "Authorization: Bearer sk-VOTRE-CLE-ICI"
# → { "ai": { "description": "...", "tags": [...] }, "exif": {...}, "ocr": {...} }
```
**`docs/api-reference.md`** — Référence complète de tous les endpoints :
- Chaque endpoint documenté avec : méthode, URL, authentification requise, scopes, paramètres, exemple de réponse JSON
- Codes d'erreur possibles avec leur signification
**`docs/websocket.md`** — Guide WebSocket :
- Connexion, authentification via query param
- Format des événements (avec exemples JSON)
- Gestion de la reconnexion et du buffer 60s
- Exemples en Python, JavaScript et curl (via websocat)
**`docs/sdk.md`** — Guide du SDK Python :
- Installation
- Tous les cas d'usage avec exemples complets
- Gestion des erreurs (chaque exception du SDK documentée)
- Configuration avancée (timeout, retry, proxy)
**`docs/deployment.md`** — Guide de déploiement en production :
- Docker Compose (backend + worker + Redis + MinIO)
- Variables d'environnement obligatoires et optionnelles
- Passage de SQLite à PostgreSQL
- Passage de LocalStorage à S3/MinIO/R2
- Configuration Nginx en reverse proxy
- Recommandations de sécurité (HTTPS, headers, firewall)
**`docs/shaarli-integration.md`** — Guide d'intégration Shaarli :
- Installation du plugin
- Configuration
- Tous les cas d'usage (image bookmark, résumé URL, recherche par tag)
- Dépannage des problèmes courants
3. `tests/test_shaarli_integration.py` — Tests d'intégration end-to-end Shaarli :
- ✅ `ShaarliHubPlugin.process_bookmark_image()` → image uploadée + pipeline terminé
- ✅ `ShaarliHubPlugin.enrich_bookmark_url()` → résumé retourné
- ✅ Gestion d'une `QuotaError` (quota dépassé)
- ✅ Gestion d'un timeout (pipeline trop long)
- ✅ Plugin fonctionne sans event loop existant (appel depuis code synchrone Shaarli)
4. `CHANGELOG.md` — Historique des versions :
```markdown
# Changelog
## [1.0.0] — 2026-02-24
### Phase 3 — Expérience développeur
- WebSockets : suivi temps réel du pipeline avec buffer de reconnexion
- API versioning : tous les endpoints sous /api/v1/ avec header X-API-Version
- SDK Python officiel : imago-client 1.0.0 sur PyPI
- Dashboard admin : interface web de supervision des clients et quotas
- Intégration Shaarli : plugin + documentation complète
## [0.2.0] — Phase 2 — Robustesse et scalabilité
...
## [0.1.0] — Phase 1 — Fondations sécurité
...
```
5. `README.md` (racine) — Réécrire pour être le point d'entrée de toute la documentation :
- Badge CI (passing), badge version, badge Python
- Une phrase de description claire
- Liens rapides vers : getting-started, api-reference, sdk, déploiement
- Section "Architecture" avec le schéma texte du système
- Section "Clients supportés" : Shaarli (officiel) + instructions pour créer son propre client
## Règles d'exécution
### Prérequis avant de commencer
1. Vérifier que les Phases 1 et 2 sont bien en place : `make ci` doit passer sans erreur
2. Redis doit être accessible en local pour les tests WebSocket
3. Lire les fichiers existants **avant** de les modifier — notamment `pipeline.py` (qui publie déjà sur Redis) et `main.py` (pour comprendre la structure de montage des routers)
### Ordre de réalisation
Implémenter dans l'ordre strict : **3.1 → 3.2 → 3.3 → 3.4 → 3.5**. Valider chaque livrable avant de continuer.
### À chaque livrable
1. Lire les fichiers concernés avec l'outil `Read` ou `Glob`
2. Implémenter le code complet (zéro `# TODO`, zéro `...` comme corps de fonction)
3. Mettre à jour `requirements.txt` si nécessaire
4. Lancer `pytest tests/ -v` après chaque livrable — corriger avant de passer au suivant
5. Pour le livrable 3.5 (docs), valider que tous les exemples de code dans la doc fonctionnent réellement
### Qualité du code
- **Typage complet** : toutes les fonctions annotées, compatible `mypy --strict`
- **Docstrings** sur toutes les classes et méthodes publiques des modules créés
- **Zéro secret** : aucune clé, URL ou credential hardcodé
- **Gestion propre des connexions WebSocket** : toujours se désabonner de Redis Pub/Sub et fermer proprement même en cas d'exception
- **SDK** : doit fonctionner de manière totalement indépendante du backend — aucun import depuis `app/`
### Spécificités WebSocket (livrable 3.1)
- Tester la déconnexion client pendant le pipeline : vérifier qu'aucune tâche orpheline ne reste dans l'event loop
- Le buffer Redis (`pipeline:buffer:{image_id}`) doit avoir un TTL de 60 secondes — ne pas oublier de le configurer dans Redis
- Si le pipeline est déjà terminé quand le client se connecte, retourner immédiatement le résultat final depuis la BDD (pas depuis Redis)
### Spécificités SDK (livrable 3.3)
- Le SDK doit gérer le retry automatique sur les erreurs 5xx (backoff exponentiel)
- `wait_until_done()` doit tenter le WebSocket d'abord (timeout 5s pour la connexion), puis basculer automatiquement sur le polling si le WebSocket échoue ou est indisponible
- Les exceptions du SDK doivent contenir le `status_code` HTTP pour faciliter le débogage
- Le SDK ne doit pas avoir de dépendance vers `fastapi`, `sqlalchemy` ou tout autre élément du backend
### Critère de succès final
Les commandes suivantes doivent toutes réussir :
```bash
# Tests backend complets
pytest tests/ -v --cov=app --cov-report=term-missing
# Tests SDK
cd sdk && pytest tests/ -v
# Qualité globale
make ci
# Vérification manuelle du dashboard
python run.py &
python worker.py &
curl -s http://localhost:8000/admin/api/stats | python -m json.tool
# Test WebSocket rapide (nécessite websocat)
websocat "ws://localhost:8000/ws/pipeline/1?token=sk-test-key"
```
## Résumé des livrables attendus
| # | Fichiers créés ou modifiés | Validation |
|---|---|---|
| **3.1** | `app/routers/websocket.py`, `app/metrics.py`, `app/main.py`, `tests/test_websocket.py` | `pytest tests/test_websocket.py` |
| **3.2** | `app/main.py`, `app/middleware/versioning.py`, `app/config.py`, `tests/test_versioning.py` | `pytest tests/test_versioning.py` |
| **3.3** | `sdk/` (package complet : client, resources, models, exceptions, websocket), `sdk/README.md`, `sdk/tests/` | `cd sdk && pytest tests/ -v` |
| **3.4** | `app/routers/admin.py`, `app/templates/admin/` (4 templates), `app/static/admin/`, `app/main.py`, `tests/test_admin.py` | `pytest tests/test_admin.py` |
| **3.5** | `integration/shaarli/`, `docs/` (5 guides), `CHANGELOG.md`, `README.md`, `tests/test_shaarli_integration.py` | `pytest tests/test_shaarli_integration.py` |
**Résultat final de la Phase 3 et du projet complet :**
- Hub multi-clients sécurisé, robuste, observable et entièrement documenté
- Pipeline AI avec suivi temps réel via WebSocket
- API versionnée stable avec contrat de dépréciation
- SDK Python officiel installable via pip
- Dashboard admin pour superviser clients et quotas
- Intégration Shaarli officielle avec documentation complète
- `make ci` vert, coverage > 80%, zéro warning mypy