""" mangabaka_rate_limit.py ======================= Process-wide rate limiter for the MangaBaka API. Apply via: from MangaBakaRateLimit import apply_to_session apply_to_session(session) This mounts a custom ``requests.adapters.HTTPAdapter`` on the given ``requests.Session`` for the ``api.mangabaka.dev`` host. Every request going through that adapter is: * throttled so that no two requests are dispatched within ``_MIN_INTERVAL`` seconds of one another, and * retried on HTTP 429, honouring the ``Retry-After`` header when present, otherwise exponential backoff capped at ``_MAX_BACKOFF``. Throttle state is module-global, so even if several sessions exist in the same process they share one budget — important because they all hit the same upstream IP-based limit. """ from __future__ import annotations import threading import time from requests.adapters import HTTPAdapter # Tune these if MangaBaka tightens or loosens limits. _MIN_INTERVAL = 1.1 # seconds between consecutive requests _MAX_RETRIES = 6 # retries on 429 before giving up _MAX_BACKOFF = 60.0 # cap on per-attempt backoff sleep # --- shared throttle state -------------------------------------------------- _state_lock = threading.Lock() _last_request_time = 0.0 def _wait_for_slot() -> None: """Block until the next request slot is available, then reserve it.""" global _last_request_time while True: with _state_lock: now = time.monotonic() wait = _MIN_INTERVAL - (now - _last_request_time) if wait <= 0: _last_request_time = now return time.sleep(wait) class _MangaBakaRateLimitAdapter(HTTPAdapter): def send(self, request, **kwargs): response = None for attempt in range(_MAX_RETRIES + 1): _wait_for_slot() response = super().send(request, **kwargs) if response.status_code != 429: return response retry_after = response.headers.get("Retry-After") try: wait = (float(retry_after) if retry_after else min(_MAX_BACKOFF, 2.0 * (2 ** attempt))) except ValueError: wait = min(_MAX_BACKOFF, 2.0 * (2 ** attempt)) print(f"[MangaBaka] 429 — backing off {wait:.1f}s " f"(attempt {attempt + 1}/{_MAX_RETRIES})", flush=True) response.close() time.sleep(wait) # Retries exhausted — let the caller deal with the last 429. return response def apply_to_session(session) -> None: """ Mount the rate-limit adapter on ``session`` so every MangaBaka call is automatically throttled. Safe to call multiple times (later mounts just replace the earlier adapter for the same prefix). """ adapter = _MangaBakaRateLimitAdapter() session.mount("https://api.mangabaka.dev/", adapter) session.mount("http://api.mangabaka.dev/", adapter)