from __future__ import annotations import asyncio import time from dataclasses import dataclass @dataclass class Bucket: tokens: float updated: float class RateLimited(RuntimeError): def __init__(self, retry_after: int = 1) -> None: self.retry_after = retry_after class ConservativeRateLimiter: """Process-local fallback. Redis may accelerate this, never own correctness.""" def __init__(self, text_rps: int, file_rps: int) -> None: self.rates = {"text": text_rps, "file": file_rps} now = time.monotonic() self.buckets = {kind: Bucket(float(rate), now) for kind, rate in self.rates.items()} self.lock = asyncio.Lock() async def acquire(self, kind: str) -> None: async with self.lock: now = time.monotonic() bucket = self.buckets[kind] rate = self.rates[kind] bucket.tokens = min(float(rate), bucket.tokens + (now - bucket.updated) * rate) bucket.updated = now if bucket.tokens < 1: raise RateLimited bucket.tokens -= 1