qonto-assistant/src/qonto_assistant/rate_limits.py

57 lines
1.7 KiB
Python
Raw Permalink Normal View History

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]