# ============================================ # Projet : API-KA # Fichier : src/api/routes/runs.py # Node : m3u96b # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Date : 2026-08-16 # ============================================ """Route /api/v1/runs : historique des collectes, filtrable par service et statut.""" from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import func, select from sqlalchemy.orm import Session from src.api.routes import envelope from src.config import SERVICES from src.database.db import get_db from src.database.models import CollectionRun router = APIRouter(prefix="/api/v1", tags=["runs"]) VALID_STATUSES = ("success", "failed", "retried") @router.get("/runs") def list_runs( service: str | None = Query(None, description="Filtrer par service KA"), status: str | None = Query(None, description="success / failed / retried"), page: int = Query(1, ge=1), limit: int = Query(100, ge=1, le=500), db: Session = Depends(get_db), ) -> dict[str, Any]: """Historique des runs de collecte, du plus récent au plus ancien.""" if service is not None and service not in SERVICES: raise HTTPException(status_code=404, detail=f"Service inconnu : {service}") if status is not None and status not in VALID_STATUSES: raise HTTPException( status_code=422, detail=f"Statut invalide : {status}. Valides : {', '.join(VALID_STATUSES)}", ) base = select(CollectionRun) if service: base = base.where(CollectionRun.service == service) if status: base = base.where(CollectionRun.status == status) total = db.execute(select(func.count()).select_from(base.subquery())).scalar() or 0 rows = ( db.execute( base.order_by(CollectionRun.started_at.desc()) .offset((page - 1) * limit) .limit(limit) ) .scalars() .all() ) data = [ { "id": run.id, "service": run.service, "date_key": run.date_key.isoformat(), "status": run.status, "records_count": run.records_count, "duration_seconds": run.duration_seconds, "error_message": run.error_message, "node": run.node, "started_at": run.started_at.isoformat(), "finished_at": run.finished_at.isoformat(), } for run in rows ] return envelope(data, page=page, limit=limit, total=total)