from __future__ import annotations import time from collections import defaultdict, deque from collections.abc import Callable, Iterator from contextlib import contextmanager from threading import Lock from qonto_assistant.errors import ConcurrencyLimitExceededError, RateLimitExceededError class RateLimiter: def __init__( self, *, limit: int, window_seconds: int, clock: Callable[[], float] = time.monotonic, ) -> None: self.limit = limit self.window_seconds = window_seconds self.clock = clock self._events: dict[str, deque[float]] = defaultdict(deque) self._lock = Lock() def check(self, key: str) -> None: now = self.clock() with self._lock: window_start = now - self.window_seconds bucket = self._events[key] while bucket and bucket[0] < window_start: bucket.popleft() if len(bucket) >= self.limit: raise RateLimitExceededError(f"Rate limit exceeded for {key}") bucket.append(now) class ConcurrencyLimiter: def __init__(self, *, limit: int) -> None: self.limit = limit self._counts: dict[str, int] = defaultdict(int) self._lock = Lock() @contextmanager def slot(self, key: str) -> Iterator[None]: with self._lock: if self._counts[key] >= self.limit: raise ConcurrencyLimitExceededError(f"Concurrency limit exceeded for {key}") self._counts[key] += 1 try: yield finally: with self._lock: self._counts[key] -= 1 if self._counts[key] <= 0: del self._counts[key]