""" Rate limiter for Lichess API requests Ensures minimum delay between requests """ import asyncio import time import logging from typing import Optional logger = logging.getLogger(__name__) class RateLimiter: """ Rate limiter that ensures minimum delay between requests. Thread-safe and async-safe. """ def __init__(self, min_delay: float = 0.2): """ Initialize rate limiter. Args: min_delay: Minimum delay in seconds between requests (default: 0.2) """ self.min_delay = min_delay self.last_request_time: Optional[float] = None self.lock = asyncio.Lock() async def wait_if_needed(self): """ Wait if necessary to maintain minimum delay between requests. Should be called before each API request. """ async with self.lock: now = time.time() if self.last_request_time is not None: elapsed = now - self.last_request_time if elapsed < self.min_delay: wait_time = self.min_delay - elapsed logger.debug(f"Rate limiter: waiting {wait_time:.3f} seconds") await asyncio.sleep(wait_time) now = time.time() self.last_request_time = now # Global rate limiter instance _rate_limiter = RateLimiter(min_delay=0.2) def get_rate_limiter() -> RateLimiter: """Get the global rate limiter instance""" return _rate_limiter # wait_if_needed() only paces request STARTS 0.2s apart; it releases the lock # before the request itself runs, so two requests can still be in flight at # once if either takes longer than 0.2s. That's fine for per-user-token calls # (each pair has its own identity), but games/user and user/activity share one # LICHESS_APP_TOKEN process-wide, and Lichess enforces a hard "1 request at a # time" limit per token there (observed as 429 "Please only run 1 request(s) # at a time" under concurrent traffic) — those two calls must hold this lock # for their entire request/response cycle, not just the pre-request wait. _shared_token_lock = asyncio.Lock() def get_shared_token_lock() -> asyncio.Lock: """Lock serializing lichess.org calls made with the shared LICHESS_APP_TOKEN.""" return _shared_token_lock