from __future__ import annotations import threading import time class TokenBucket: def __init__(self, rate_per_minute: float, capacity: float | None = None, clock=time.monotonic) -> None: self.rate_per_second = max(0.001, rate_per_minute / 60.0) self.capacity = float(capacity if capacity is not None else rate_per_minute) self._tokens = self.capacity self._updated = clock() self._clock = clock self._lock = threading.Lock() def acquire(self, tokens: float = 1.0, block: bool = True) -> bool: while True: with self._lock: now = self._clock() elapsed = max(0.0, now - self._updated) self._tokens = min(self.capacity, self._tokens + elapsed * self.rate_per_second) self._updated = now if self._tokens >= tokens: self._tokens -= tokens return True wait = (tokens - self._tokens) / self.rate_per_second if not block: return False time.sleep(min(wait, 0.05)) @property def remaining(self) -> float: with self._lock: now = self._clock() elapsed = max(0.0, now - self._updated) return min(self.capacity, self._tokens + elapsed * self.rate_per_second)