- 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.
37 KiB
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.
<project_context>
É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. </project_context>
## Mission : implémenter la Phase 3 — Expérience développeurTu 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é :
-
app/routers/websocket.py— Nouveau router WebSocket :Endpoint principal :
WS /ws/pipeline/{image_id}?token=<api_key>- 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.doneoupipeline.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=<admin_api_key>- 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) :{ "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
donequand le client se connecte → envoyer immédiatement un messagepipeline.donesynthétique et fermer - Si l'image est en statut
error→ envoyerpipeline.errorsynthétique et fermer - Si l'image n'appartient pas au client → fermer avec code WebSocket 4003 (Forbidden)
- Authentification via query param
-
app/metrics.py— Ajouter la gaugehub_active_websockets:- Incrémenter à l'ouverture d'une connexion WebSocket
- Décrémenter à la fermeture (succès ou erreur)
-
app/main.py— Monter le router WebSocket :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 -
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çoitpipeline.donesynthé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
httpxavecASGITransportou le client WebSocket destarlette.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é :
-
app/main.py— Restructurer le montage des routers :Avant (sans versioning) :
app.include_router(images_router) app.include_router(ai_router) app.include_router(auth_router)Après (versioning propre) :
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) -
app/middleware/versioning.py— Middleware de versioning :- Ajouter le header
X-API-Version: v1sur toutes les réponses des routes/api/v1/* - Préparer (en commentaire) l'ajout futur de
Deprecation: trueetSunset: <date>quand v2 existera - Logger un warning structlog si une route
/api/v1/*est appelée après la date de sunset (configurable)
- Ajouter le header
-
app/config.py— Ajouter :API_V1_SUNSET_DATE: str = "" # vide = pas de sunset, sinon "2027-06-01" -
app/main.py— Mettre à jour la documentation OpenAPI :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 <votre_api_key>
### 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"}, ], ) -
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: v1présent sur toutes les réponses/api/v1/* - ✅ Header absent sur
/health,/metrics,/ws/*
- ✅ Tous les endpoints images/ai/auth répondent sous
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éé :
-
sdk/imago_client/exceptions.py: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.""" -
sdk/imago_client/models.py— Dataclasses des réponses :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 -
sdk/imago_client/websocket.py—PipelineStream: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.""" -
sdk/imago_client/resources/images.py—ImagesResource: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.""" -
sdk/imago_client/client.py—HubClient: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) -
sdk/imago_client/resources/ai.py—AIResource: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.""" -
sdk/pyproject.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 -
sdk/README.md— Documentation complète avec exemples :# 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"]) -
sdk/tests/— Tests du SDK avecrespxpour mocker httpx :- ✅
upload()retourne unPipelineStreamvalide - ✅
wait_until_done()complète via WebSocket - ✅
wait_until_done()complète via polling si WebSocket indisponible - ✅
HubErrorlevée sur HTTP 4xx/5xx - ✅
AuthErrorsur 401,QuotaErrorsur 413/429,NotFoundErrorsur 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éé :
-
app/routers/admin.py— Router admin avec pages HTML et endpoints JSON :Pages HTML (rendu Jinja2, scope
adminrequis) :GET /admin/→ Dashboard overview (métriques globales, santé)GET /admin/clients→ Liste des clients avec quotas et consommationGET /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éelGET /admin/api/queue/status→ Jobs ARQ en cours et en attentePOST /admin/api/clients/{id}/toggle→ Activer/désactiver un clientPOST /admin/api/clients/{id}/reset-quota→ Réinitialiser les compteurs de quota
-
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
-
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 viafetch
-
app/main.py— Monter le dashboard :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) -
tests/test_admin.py— Tests du dashboard :- ✅
GET /admin/sans scopeadmin→ HTTP 403 - ✅
GET /admin/avec scopeadmin→ HTTP 200 avec HTML valide - ✅
GET /admin/api/stats→ JSON avec les bons champs - ✅
POST /admin/api/clients/{id}/toggle→ changeis_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éé :
-
integration/shaarli/— Module d'intégration Shaarli :integration/shaarli/hub_plugin.py— Plugin Python utilisable depuis Shaarli :""" 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:# Configuration du plugin Imago HUB_URL=http://imago:8000 HUB_API_KEY=sk-votre-cle-api-ici HUB_TIMEOUT=120integration/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)
-
docs/— Documentation technique complète :docs/getting-started.md— Guide de démarrage en 5 minutes :# 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
-
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)
- ✅
-
CHANGELOG.md— Historique des versions :# 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é ... -
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
<execution_rules>
Règles d'exécution
Prérequis avant de commencer
- Vérifier que les Phases 1 et 2 sont bien en place :
make cidoit passer sans erreur - Redis doit être accessible en local pour les tests WebSocket
- Lire les fichiers existants avant de les modifier — notamment
pipeline.py(qui publie déjà sur Redis) etmain.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
- Lire les fichiers concernés avec l'outil
ReadouGlob - Implémenter le code complet (zéro
# TODO, zéro...comme corps de fonction) - Mettre à jour
requirements.txtsi nécessaire - Lancer
pytest tests/ -vaprès chaque livrable — corriger avant de passer au suivant - 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_codeHTTP pour faciliter le débogage - Le SDK ne doit pas avoir de dépendance vers
fastapi,sqlalchemyou tout autre élément du backend
Critère de succès final
Les commandes suivantes doivent toutes réussir :
# 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"
</execution_rules>
<deliverable_summary>
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 civert, coverage > 80%, zéro warning mypy </deliverable_summary>