Odysseus v1.0
This commit is contained in:
49
src/rate_limiter.py
Normal file
49
src/rate_limiter.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# src/rate_limiter.py
|
||||
"""Generic in-memory rate limiter — sliding window, keyed by IP."""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""Sliding-window rate limiter.
|
||||
|
||||
Usage:
|
||||
limiter = RateLimiter(max_requests=5, window_seconds=60)
|
||||
if not limiter.check(ip):
|
||||
raise HTTPException(429, "Too many requests")
|
||||
"""
|
||||
|
||||
def __init__(self, max_requests: int, window_seconds: int):
|
||||
self.max_requests = max_requests
|
||||
self.window = window_seconds
|
||||
self._log: Dict[str, List[float]] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._last_cleanup = time.monotonic()
|
||||
self._cleanup_interval = max(window_seconds * 2, 120)
|
||||
|
||||
def check(self, key: str) -> bool:
|
||||
"""Return True if the request is allowed, False if rate-limited."""
|
||||
now = time.monotonic()
|
||||
with self._lock:
|
||||
self._maybe_cleanup(now)
|
||||
timestamps = self._log.get(key, [])
|
||||
cutoff = now - self.window
|
||||
timestamps = [t for t in timestamps if t > cutoff]
|
||||
if len(timestamps) >= self.max_requests:
|
||||
self._log[key] = timestamps
|
||||
return False
|
||||
timestamps.append(now)
|
||||
self._log[key] = timestamps
|
||||
return True
|
||||
|
||||
def _maybe_cleanup(self, now: float) -> None:
|
||||
"""Periodically purge stale entries."""
|
||||
if now - self._last_cleanup < self._cleanup_interval:
|
||||
return
|
||||
self._last_cleanup = now
|
||||
cutoff = now - self.window
|
||||
stale = [k for k, v in self._log.items() if not v or v[-1] <= cutoff]
|
||||
for k in stale:
|
||||
del self._log[k]
|
||||
Reference in New Issue
Block a user