SPB Git forge

spb/api-ka

Public

API-KA — plateforme centrale : collecte quotidienne des 8 services KA, historisation append-only et API publique sur www.api-ka.com

48commits 1branches 0releases
5.9 MBsize
maindefault branch
19 days agolast push
Python 60.9% HTML 21% TypeScript 7.3% JavaScript 5.2% CSS 4.8% Shell 0.8%
1.9 KB · 56 lines python
Raw Blame History
1# ============================================2# Projet   : API-KA3# Fichier  : src/api/middleware/ratelimit.py4# Node     : m3u96b5# Author   : Simon-Pierre Boucher6# Contact  : contact@spboucher.ai7# Date     : 2026-08-168# ============================================9"""Rate limiting basique par IP (défaut : 120 req/min) — l'API est publique via ngrok."""1011from __future__ import annotations1213import time14from collections import defaultdict, deque1516from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint17from starlette.requests import Request18from starlette.responses import JSONResponse, Response1920from src.config import get_settings2122WINDOW_SECONDS = 60.023EXEMPT_PATHS = ("/health",)242526class RateLimitMiddleware(BaseHTTPMiddleware):27    """Fenêtre glissante de 60 s par IP ; au-delà de la limite → HTTP 429."""2829    def __init__(self, app, limit_per_minute: int | None = None) -> None:  # type: ignore[no-untyped-def]30        super().__init__(app)31        self.limit = limit_per_minute or get_settings().rate_limit_per_minute32        self._hits: dict[str, deque[float]] = defaultdict(deque)3334    async def dispatch(35        self, request: Request, call_next: RequestResponseEndpoint36    ) -> Response:37        if request.url.path in EXEMPT_PATHS:38            return await call_next(request)3940        client_ip = request.client.host if request.client else "unknown"41        now = time.monotonic()42        hits = self._hits[client_ip]43        while hits and now - hits[0] > WINDOW_SECONDS:44            hits.popleft()45        if len(hits) >= self.limit:46            return JSONResponse(47                status_code=429,48                content={49                    "success": False,50                    "error": f"Limite dépassée : {self.limit} requêtes/minute par IP",51                    "meta": {"node": get_settings().node_name},52                },53            )54        hits.append(now)55        return await call_next(request)56