# Prompt Claude Code — Phase 2 : Robustesse et scalabilité # Modèle : claude-opus-4-6 # Usage : claude --model claude-opus-4-6 -p "$(cat PROMPT_PHASE2_claude-opus.md)" # ou coller directement dans une session Claude Code interactive --- Tu es un ingénieur backend senior Python spécialisé en systèmes distribués, FastAPI async et infrastructure de production. Tu travailles sur le projet Imago, un hub centralisé de gestion d'images servant plusieurs applications clientes simultanément. La Phase 1 (authentification, isolation multi-tenants, rate limiting, tests d'intégration) est entièrement terminée et validée. Tu prends le relais pour rendre ce hub robuste, scalable et observable en production. ## État du projet après Phase 1 ### Stack en place (Phase 1 complète) - **FastAPI + Uvicorn** — framework web async - **SQLAlchemy async + Alembic** — ORM + migrations - **Pydantic v2 + pydantic-settings** — validation + config - **python-jose + passlib** — JWT + hachage API Keys - **slowapi** — rate limiting par client - **Pillow + piexif** — traitement images + EXIF - **pytesseract** — OCR Tesseract - **Anthropic Claude via httpx** — Vision AI - **aiofiles** — I/O async - **pytest-asyncio + httpx** — tests d'intégration ### Structure complète du projet (après Phase 1) ``` imago/ ├── app/ │ ├── main.py # Application FastAPI, lifespan, middlewares │ ├── config.py # Settings depuis .env (inclut ADMIN_API_KEY, JWT_SECRET) │ ├── database.py # Engine SQLAlchemy async + session factory │ ├── models/ │ │ ├── __init__.py │ │ ├── image.py # Image (avec client_id FK, EXIF, OCR, AI, statut) │ │ └── client.py # APIClient (id, name, api_key_hash, scopes, plan, quotas) │ ├── schemas/ │ │ └── __init__.py # Schémas Pydantic complets │ ├── dependencies/ │ │ ├── __init__.py │ │ └── auth.py # verify_api_key, require_scope, get_current_client │ ├── middleware/ │ │ └── rate_limit.py # Rate limiting par plan client (slowapi) │ ├── routers/ │ │ ├── __init__.py │ │ ├── auth.py # CRUD clients, rotation de clé │ │ ├── images.py # Endpoints images (filtrés par client_id) │ │ └── ai.py # Endpoints AI (résumé URL, tâches) │ └── services/ │ ├── __init__.py │ ├── storage.py # Stockage fichiers (uploads/{client_id}/{filename}) │ ├── exif_service.py # Extraction EXIF │ ├── ocr_service.py # OCR Tesseract │ ├── ai_vision.py # Vision AI Claude + summarize_url + draft_task │ ├── scraper.py # Scraping web BeautifulSoup │ └── pipeline.py # Pipeline EXIF → OCR → AI (via BackgroundTasks) ├── tests/ │ ├── conftest.py # Fixtures : test_db, client_a/b, auth_headers │ ├── test_services.py # Tests unitaires services │ ├── test_auth.py # Tests authentification │ ├── test_isolation.py # Tests isolation multi-tenants │ └── test_rate_limit.py # Tests rate limiting ├── alembic/ │ └── versions/ # Migrations : table images + table api_clients ├── requirements.txt ├── .env ├── Dockerfile └── docker-compose.yml ``` ### Ce qui reste problématique (cible de la Phase 2) **Pipeline non persistant** : les `BackgroundTasks` FastAPI perdent toutes les tâches en file si le serveur redémarre. Aucun retry en cas d'échec de l'API Anthropic. Aucune priorité entre clients `free` et `premium`. Concurrence non contrôlée. **Stockage non abstrait** : fichiers sur disque local uniquement, couplé aux chemins absolus. Impossible de scaler, aucune réplication, URLs statiques exposent les chemins réels. **Observabilité absente** : tous les logs sont des `print()`. Aucune métrique applicative. Health check basique qui retourne toujours "ok". Impossible de monitorer en production. **CI/CD inexistant** : pas d'automatisation des tests, pas de pipeline de déploiement, pas de contrôles qualité automatisés. ## Mission : implémenter la Phase 2 — Robustesse et scalabilité 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 2.1 — Migration BackgroundTasks → ARQ + Redis (priorité HAUTE) **Objectif** : remplacer le pipeline fragile par un système de file de tâches persistant avec retry automatique, priorités par plan client et concurrence contrôlée. **Nouvelles dépendances à ajouter dans `requirements.txt`** : ``` arq==0.25.0 redis==5.0.8 ``` **Ce qui doit être créé ou modifié** : 1. `app/config.py` — Ajouter les variables Redis et worker : ```python REDIS_URL: str = "redis://localhost:6379" WORKER_MAX_JOBS: int = 10 # concurrence max globale WORKER_JOB_TIMEOUT: int = 180 # secondes avant timeout forcé WORKER_MAX_TRIES: int = 3 # tentatives avant dead-letter AI_STEP_TIMEOUT: int = 120 # timeout spécifique appel Vision AI OCR_STEP_TIMEOUT: int = 30 # timeout spécifique OCR ``` 2. `app/workers/__init__.py` + `app/workers/image_worker.py` — Worker ARQ : - Fonction `process_image_task(ctx, image_id: int, client_id: str)` qui appelle `pipeline.py` - `WorkerSettings` avec : - `functions = [process_image_task]` - `redis_settings` depuis `settings.REDIS_URL` - `max_jobs = settings.WORKER_MAX_JOBS` - `job_timeout = settings.WORKER_JOB_TIMEOUT` - `retry_jobs = True` - `max_tries = settings.WORKER_MAX_TRIES` - `on_job_start`, `on_job_end`, `on_job_abort` — hooks de logging - Backoff exponentiel : délais `[1, 4, 16]` secondes entre les tentatives - Dead-letter : après `max_tries` échecs, marquer l'image `status=error` + log `ERROR` 3. `app/workers/redis_client.py` — Client Redis partagé : - Pool de connexions async (`aioredis` ou `redis.asyncio`) - Fonction `get_redis_pool()` injectable comme dépendance FastAPI - Gestion propre de la connexion dans le lifespan de `main.py` 4. `app/services/pipeline.py` — Ajouter publication d'événements Redis : - À chaque étape complétée, publier sur le channel `pipeline:{image_id}` : ```python await redis.publish(f"pipeline:{image_id}", json.dumps({ "event": "step.completed", "step": "exif", "duration_ms": elapsed, "data": { ... } # résumé des données extraites })) ``` - Événements à publier : `pipeline.started`, `step.completed` (×3), `pipeline.done`, `pipeline.error` 5. `app/routers/images.py` — Modifier `POST /images/upload` : - Remplacer `background_tasks.add_task(process_image_pipeline, ...)` par : ```python queue_name = "premium" if client.plan == "premium" else "standard" await arq_pool.enqueue_job( "process_image_task", image.id, str(client.id), _queue_name=queue_name ) ``` 6. `app/main.py` — Modifier le `lifespan` : - Créer et stocker le pool ARQ au démarrage : `app.state.arq_pool` - Créer et stocker le pool Redis au démarrage : `app.state.redis` - Fermer proprement les deux à l'arrêt 7. `worker.py` — Script de démarrage du worker (à la racine du projet) : ```python # Lancer avec : python worker.py import asyncio from arq import run_worker from app.workers.image_worker import WorkerSettings if __name__ == "__main__": asyncio.run(run_worker(WorkerSettings)) ``` 8. `docker-compose.yml` — Ajouter le service Redis et le worker : ```yaml redis: image: redis:7-alpine ports: ["6379:6379"] volumes: ["redis_data:/data"] command: redis-server --appendonly yes # persistance AOF worker: build: . command: python worker.py depends_on: [backend, redis] env_file: .env ``` **Contraintes** : - Le pool ARQ doit être créé une seule fois au démarrage, pas à chaque requête - Les tâches doivent survivre à un redémarrage du serveur FastAPI (elles restent dans Redis) - Un job qui dépasse `job_timeout` doit être marqué `error` en base, pas silencieusement ignoré - Les files `premium` et `standard` doivent être des queues ARQ distinctes (pas juste un label) --- ### Livrable 2.2 — Abstraction StorageBackend + support MinIO/S3 (priorité HAUTE) **Objectif** : découpler le code du stockage physique pour permettre une migration transparente vers S3/MinIO sans modifier les routers ni les services métier. **Nouvelle dépendance** : ``` aioboto3==13.0.0 ``` **Ce qui doit être créé ou modifié** : 1. `app/services/storage_backend.py` — Interface abstraite + deux implémentations : **Classe de base** : ```python class StorageBackend(ABC): @abstractmethod async def save(self, content: bytes, path: str, content_type: str) -> str: """Sauvegarde un fichier. Retourne le chemin stocké.""" @abstractmethod async def delete(self, path: str) -> None: """Supprime un fichier.""" @abstractmethod async def get_signed_url(self, path: str, expires_in: int = 900) -> str: """Retourne une URL d'accès temporaire signée.""" @abstractmethod async def exists(self, path: str) -> bool: """Vérifie qu'un fichier existe.""" @abstractmethod async def get_size(self, path: str) -> int: """Retourne la taille en bytes.""" ``` **`LocalStorage(StorageBackend)`** : - `save()` : écrit dans `{UPLOAD_DIR}/{path}` avec `aiofiles`, crée les dossiers parents si besoin - `delete()` : supprime le fichier, ignore si absent - `get_signed_url()` : génère un token HMAC-SHA256 signé avec `itsdangerous.URLSafeTimedSerializer` incluant `path` + expiration. Retourne `/files/signed/{token}` - `exists()` : `Path(full_path).exists()` - `get_size()` : `Path(full_path).stat().st_size` **`S3Storage(StorageBackend)`** : - Constructeur : `bucket`, `prefix`, `session` (`aioboto3.Session`) - `save()` : `put_object` avec le `content_type` correct - `delete()` : `delete_object` - `get_signed_url()` : `generate_presigned_url("get_object", ExpiresIn=expires_in)` - `exists()` : `head_object` → True/False - `get_size()` : `head_object` → `ContentLength` - Compatible S3, MinIO et Cloudflare R2 (même API boto3) 2. `app/services/storage.py` — Refactoring complet : - Supprimer le code de stockage direct - La fonction `save_upload(file, client_id)` utilise maintenant le backend injecté - La fonction `delete_files(paths)` utilise le backend - Exposer `get_storage_backend() -> StorageBackend` : factory qui lit `settings.STORAGE_BACKEND` et instancie le bon backend 3. `app/config.py` — Ajouter : ```python STORAGE_BACKEND: str = "local" # "local" | "s3" S3_BUCKET: str = "" S3_REGION: str = "us-east-1" S3_ENDPOINT_URL: str = "" # vide = AWS, sinon MinIO/R2 S3_ACCESS_KEY: str = "" S3_SECRET_KEY: str = "" S3_PREFIX: str = "imago" # préfixe dans le bucket SIGNED_URL_SECRET: str = "changez-moi" # secret HMAC pour LocalStorage ``` 4. `app/routers/images.py` — Ajouter les endpoints d'URLs signées : - `GET /images/{id}/download-url?expires_in=900` → `{ "url": "...", "expires_in": 900 }` - `GET /images/{id}/thumbnail-url?expires_in=900` → `{ "url": "...", "expires_in": 900 }` - Vérifier que l'image appartient au client avant de générer l'URL - Appliquer `require_scope("images:read")` 5. `app/routers/files.py` — Nouveau router pour le stockage local signé : - `GET /files/signed/{token}` — valide le token HMAC, retourne le fichier via `FileResponse` - Lever `HTTP 403` si token invalide ou expiré - Lever `HTTP 410 Gone` si le fichier n'existe plus sur disque - Ce router n'est monté que si `STORAGE_BACKEND == "local"` 6. `app/main.py` — Supprimer les `StaticFiles` mounts `/static/*` et les remplacer par le router `files.py` **Contraintes** : - Le reste du code (routers, pipeline) ne doit **jamais** importer `LocalStorage` ou `S3Storage` directement — uniquement `StorageBackend` et la factory - Les tokens HMAC doivent avoir une durée de vie stricte (vérification côté serveur à la lecture) - Le backend S3 doit fonctionner avec MinIO en local (via `S3_ENDPOINT_URL`) --- ### Livrable 2.3 — URLs signées : quota et tracking de l'espace disque (priorité HAUTE) **Objectif** : enrichir le système d'URLs signées avec le suivi de la consommation de stockage par client pour permettre l'application des quotas. **Ce qui doit être créé ou modifié** : 1. `app/models/client.py` — Ajouter la méthode de calcul de quota : ```python async def get_storage_used_bytes(self, db: AsyncSession) -> int: """Somme de file_size pour toutes les images actives du client.""" result = await db.execute( select(func.sum(Image.file_size)) .where(Image.client_id == self.id) ) return result.scalar_one() or 0 ``` 2. `app/services/storage.py` — Dans `save_upload()` : - Avant de sauvegarder, vérifier que `storage_used + file_size <= quota_storage_mb * 1024 * 1024` - Lever `HTTP 413` avec message clair si quota dépassé : `"Quota de stockage atteint (X MB / Y MB)"` - Vérifier aussi `count(images) < quota_images` avant l'upload 3. `app/routers/images.py` — Ajouter dans `GET /images` la consommation actuelle dans la réponse : ```json { "total": 42, "storage_used_mb": 128.5, "storage_quota_mb": 500, "quota_pct": 25.7 } ``` 4. `app/schemas/__init__.py` — Mettre à jour `PaginatedImages` avec les champs de quota **Contraintes** : - La vérification de quota doit être atomique (SELECT + INSERT dans la même transaction si possible) - Ne pas recalculer la somme à chaque requête GET — utiliser une colonne `storage_used_bytes` dénormalisée sur `APIClient`, mise à jour lors des uploads et suppressions --- ### Livrable 2.4 — Logs structurés + métriques Prometheus (priorité MOYENNE) **Objectif** : remplacer tous les `print()` par des logs JSON structurés et exposer des métriques Prometheus exploitables pour le monitoring de production. **Nouvelles dépendances** : ``` structlog==24.4.0 prometheus-fastapi-instrumentator==0.14.0 ``` **Ce qui doit être créé ou modifié** : 1. `app/logging_config.py` — Configuration structlog centralisée : ```python import structlog def configure_logging(debug: bool = False): structlog.configure( processors=[ structlog.contextvars.merge_contextvars, structlog.processors.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.dev.ConsoleRenderer() if debug else structlog.processors.JSONRenderer(), ], wrapper_class=structlog.make_filtering_bound_logger( logging.DEBUG if debug else logging.INFO ), ) ``` 2. Remplacer **chaque `print()`** dans le projet par le logger structlog approprié : Dans `app/services/pipeline.py` : ```python log = structlog.get_logger() # Remplacer print(f"[Pipeline:{image_id}] Étape 1/3 — EXIF") par : log.info("pipeline.step.started", image_id=image_id, step="exif") # Remplacer print(f"[Pipeline:{image_id}] EXIF OK") par : log.info("pipeline.step.completed", image_id=image_id, step="exif", duration_ms=elapsed, camera=image.exif_make) # En cas d'erreur : log.error("pipeline.step.failed", image_id=image_id, step="exif", error=str(e), exc_info=True) ``` Dans `app/services/storage.py`, `exif_service.py`, `ocr_service.py`, `ai_vision.py` : même principe. Dans `app/workers/image_worker.py` : logs sur `on_job_start`, `on_job_end`, `on_job_abort`. 3. `app/middleware/logging_middleware.py` — Middleware HTTP de logs : - Logger chaque requête : `method`, `path`, `status_code`, `duration_ms`, `client_id` (si authentifié) - Exclure `/health` et `/metrics` pour éviter le bruit - Format : `log.info("http.request", method="POST", path="/api/v1/images/upload", status=201, duration_ms=45, client_id="abc")` 4. `app/main.py` — Intégration Prometheus : ```python from prometheus_fastapi_instrumentator import Instrumentator Instrumentator( should_group_status_codes=True, should_ignore_untemplated=True, excluded_handlers=["/health", "/metrics"], ).instrument(app).expose(app, endpoint="/metrics", include_in_schema=False) ``` 5. `app/metrics.py` — Métriques custom applicatives : ```python from prometheus_client import Counter, Histogram, Gauge images_uploaded = Counter( "hub_images_uploaded_total", "Total uploads par client et par plan", ["client_id", "plan"] ) pipeline_duration = Histogram( "hub_pipeline_duration_seconds", "Durée du pipeline par étape", ["step"], buckets=[0.1, 0.5, 1, 2, 5, 10, 30, 60, 120] ) ai_tokens_consumed = Counter( "hub_ai_tokens_consumed_total", "Tokens AI consommés par client", ["client_id", "token_type"] # prompt / output ) pipeline_errors = Counter( "hub_pipeline_errors_total", "Erreurs pipeline par étape", ["step"] ) storage_used_bytes = Gauge( "hub_storage_used_bytes", "Espace disque utilisé par client", ["client_id"] ) arq_queue_size = Gauge( "hub_arq_queue_size", "Tâches en attente dans ARQ", ["queue"] ) ``` - Incrémenter ces métriques aux bons endroits (upload, pipeline, suppression) 6. `app/main.py` — Améliorer `/health/detailed` : ```python @app.get("/health/detailed") async def health_detailed(db: AsyncSession = Depends(get_db)): checks = {} t0 = time() # Base de données try: await db.execute(text("SELECT 1")) checks["database"] = {"status": "ok", "latency_ms": round((time()-t0)*1000)} except Exception as e: checks["database"] = {"status": "error", "detail": str(e)} # Redis try: await app.state.redis.ping() checks["redis"] = {"status": "ok"} except Exception as e: checks["redis"] = {"status": "error", "detail": str(e)} # ARQ queue try: queue_info = await app.state.arq_pool.queued_jobs() checks["queue"] = {"status": "ok", "pending_jobs": len(queue_info)} except Exception as e: checks["queue"] = {"status": "error", "detail": str(e)} # Tesseract try: pytesseract.get_tesseract_version() checks["tesseract"] = {"status": "ok"} except Exception as e: checks["tesseract"] = {"status": "error", "detail": str(e)} # API Anthropic (vérification clé uniquement, sans appel facturable) checks["anthropic"] = { "status": "ok" if settings.ANTHROPIC_API_KEY else "error", "configured": bool(settings.ANTHROPIC_API_KEY) } # Espace disque try: usage = shutil.disk_usage(settings.UPLOAD_DIR) pct = round(usage.used / usage.total * 100, 1) checks["disk"] = { "status": "warning" if pct > 85 else "ok", "used_pct": pct, "free_gb": round(usage.free / 1e9, 2) } except Exception as e: checks["disk"] = {"status": "error", "detail": str(e)} overall = "healthy" if all( c.get("status") in ("ok", "warning") for c in checks.values() ) else "degraded" return { "status": overall, "version": settings.APP_VERSION, "checks": checks, "checked_at": datetime.utcnow().isoformat() } ``` **Contraintes** : - Zéro `print()` restant dans tout le projet après ce livrable - Les logs de requêtes HTTP ne doivent pas logger les valeurs des headers `Authorization` - Les métriques Prometheus ne doivent pas exposer de données sensibles (pas de `client_id` en clair dans les labels si trop nombreux — utiliser le `plan` à la place) --- ### Livrable 2.5 — CI/CD complet avec GitHub Actions (priorité HAUTE) **Objectif** : automatiser entièrement la qualité du code, les tests, le build Docker et le déploiement. **Nouvelles dépendances dev** — créer `requirements-dev.txt` : ``` pytest==8.3.0 pytest-asyncio==0.24.0 pytest-cov==5.0.0 httpx==0.27.2 ruff==0.6.0 black==24.8.0 mypy==1.11.0 bandit==1.7.10 pre-commit==3.8.0 faker==27.0.0 # génération de données de test ``` **Ce qui doit être créé** : 1. `.github/workflows/ci.yml` — Pipeline CI complet : **Job `quality`** — sur chaque push et PR : ```yaml - name: Lint (ruff) run: ruff check . --output-format=github - name: Format check (black) run: black --check . --line-length 100 - name: Type check (mypy) run: mypy app/ --strict --ignore-missing-imports - name: Security scan (bandit) run: bandit -r app/ -ll -x tests/ ``` **Job `tests`** — sur chaque push et PR : ```yaml services: redis: image: redis:7-alpine ports: ["6379:6379"] steps: - run: pytest tests/ -v --cov=app --cov-report=xml --cov-fail-under=80 - uses: codecov/codecov-action@v4 ``` **Job `docker`** — sur push vers `main` uniquement : ```yaml needs: [quality, tests] steps: - uses: docker/build-push-action@v5 with: push: true tags: ghcr.io/${{ github.repository }}:latest ``` **Job `deploy-staging`** — sur push vers `main`, après `docker` : - Déploiement vers un environnement de staging via SSH ou webhook - Commenter dans le workflow s'il n'y a pas encore d'environnement de staging 2. `.pre-commit-config.yaml` — Hooks locaux : ```yaml repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.0 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.11.0 hooks: - id: mypy args: [--strict, --ignore-missing-imports] additional_dependencies: [types-all] - repo: https://github.com/PyCQA/bandit rev: 1.7.10 hooks: - id: bandit args: [-ll, -x, tests/] ``` 3. `pyproject.toml` — Configuration centralisée des outils : ```toml [tool.ruff] line-length = 100 target-version = "py312" select = ["E", "F", "I", "N", "W", "UP", "S", "B", "A"] ignore = ["S101"] # autorise les assert dans les tests [tool.black] line-length = 100 target-version = ["py312"] [tool.mypy] python_version = "3.12" strict = true ignore_missing_imports = true [tool.pytest.ini_options] asyncio_mode = "auto" testpaths = ["tests"] addopts = "--tb=short -q" [tool.coverage.run] source = ["app"] omit = ["app/main.py", "*/migrations/*"] [tool.coverage.report] fail_under = 80 show_missing = true ``` 4. `Makefile` — Raccourcis de développement : ```makefile .PHONY: dev test lint format typecheck security ci dev: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 worker: python worker.py test: pytest tests/ -v --cov=app --cov-report=term-missing lint: ruff check . --fix format: black . --line-length 100 typecheck: mypy app/ --strict --ignore-missing-imports security: bandit -r app/ -ll -x tests/ ci: lint format typecheck security test @echo "✅ Tous les checks CI passent" migrate: alembic upgrade head docker-up: docker-compose up -d docker-logs: docker-compose logs -f backend worker ``` 5. `tests/test_pipeline_arq.py` — Tests du pipeline ARQ : - Mocker `arq_pool.enqueue_job` pour vérifier que la bonne queue est utilisée selon le plan - Tester que `process_image_task` est appelé avec les bons paramètres - Tester le comportement en cas d'échec (image marquée `error` après `max_tries`) - Tester que les événements Redis sont publiés aux bonnes étapes 6. `tests/test_storage.py` — Tests du StorageBackend : - Tester `LocalStorage` avec un répertoire temporaire (`tmp_path` de pytest) - Tester que les tokens HMAC expirent correctement - Tester que `S3Storage` appelle les bonnes méthodes boto3 (avec `moto` pour mocker AWS) - Tester la vérification de quota dans `save_upload()` 7. `tests/test_observability.py` — Tests des métriques et logs : - Vérifier que `GET /metrics` retourne `HTTP 200` avec du texte Prometheus - Vérifier que `GET /health/detailed` retourne un statut structuré - Vérifier que les métriques `hub_images_uploaded_total` s'incrémentent à chaque upload **Contraintes** : - Le pipeline CI doit échouer si la couverture de tests descend sous 80% - `mypy --strict` doit passer sans erreur sur tout le répertoire `app/` - `bandit` ne doit rapporter aucun problème de sévérité HIGH ou MEDIUM - Aucun secret ne doit apparaître dans les logs du CI (utiliser `${{ secrets.* }}` pour tout) ## Règles d'exécution ### Prérequis avant de commencer 1. Vérifier que la Phase 1 est bien en place : `pytest tests/ -v` doit passer sans erreur 2. Lire les fichiers existants concernés **avant** de les modifier 3. S'assurer que Redis est disponible localement pour les tests ARQ ### Ordre de réalisation Implémenter dans l'ordre strict : **2.1 → 2.2 → 2.3 → 2.4 → 2.5**. Valider chaque livrable avec `pytest` 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` et `requirements-dev.txt` si nécessaire 4. Créer la migration Alembic si le schéma change 5. Lancer `pytest tests/ -v` et corriger avant de passer au suivant 6. Pour le livrable 2.5, lancer `make ci` pour valider l'ensemble ### Qualité du code - **Typage complet** : toutes les fonctions annotées, compatible `mypy --strict` - **Docstrings** sur toutes les classes et méthodes publiques - **Zéro secret hardcodé** : tout passe par `app/config.py` + `.env` - **Zéro `print()`** : remplacé par `structlog` dans tout le projet - **Gestion explicite des exceptions** : chaque `except` doit logger + réagir, jamais `pass` - **Fermeture propre des ressources** : pools Redis, connexions S3 fermées dans le lifespan ### Infrastructure locale pour le développement Ajouter dans `docker-compose.yml` (si pas déjà présent) : ```yaml services: redis: image: redis:7-alpine ports: ["6379:6379"] volumes: ["redis_data:/data"] command: redis-server --appendonly yes minio: image: minio/minio ports: ["9000:9000", "9001:9001"] environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin command: server /data --console-address ":9001" volumes: ["minio_data:/data"] volumes: redis_data: minio_data: ``` ### Critère de succès final Les commandes suivantes doivent toutes réussir sans erreur : ```bash # Tests complets avec couverture pytest tests/ -v --cov=app --cov-report=term-missing # Qualité de code make ci # Démarrage du serveur python run.py # Démarrage du worker ARQ (dans un autre terminal) python worker.py # Vérification de santé curl http://localhost:8000/health/detailed | python -m json.tool ``` La mise a jour des documentations API_GUIDE.md et README.md est aussi requise. ## Résumé des livrables attendus | # | Fichiers créés ou modifiés | Validation | |---|---|---| | **2.1** | `app/workers/image_worker.py`, `app/workers/redis_client.py`, `app/services/pipeline.py`, `app/routers/images.py`, `app/main.py`, `app/config.py`, `worker.py`, `docker-compose.yml` | `pytest tests/test_pipeline_arq.py` | | **2.2** | `app/services/storage_backend.py`, `app/services/storage.py`, `app/routers/files.py`, `app/routers/images.py`, `app/main.py`, `app/config.py` | `pytest tests/test_storage.py` | | **2.3** | `app/models/client.py`, `app/services/storage.py`, `app/routers/images.py`, `app/schemas/__init__.py` | `pytest tests/test_storage.py -k quota` | | **2.4** | `app/logging_config.py`, `app/metrics.py`, `app/middleware/logging_middleware.py`, `app/main.py`, + remplacement de tous les `print()` dans `services/` et `workers/` | `pytest tests/test_observability.py` | | **2.5** | `.github/workflows/ci.yml`, `.pre-commit-config.yaml`, `pyproject.toml`, `Makefile`, `requirements-dev.txt`, `tests/test_pipeline_arq.py`, `tests/test_storage.py`, `tests/test_observability.py` | `make ci` | **Résultat final de la Phase 2 :** hub production-ready avec pipeline persistant et retry automatique, stockage abstrait compatible S3/MinIO, logs JSON structurés, métriques Prometheus et pipeline CI/CD complet.