# 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