import asyncio import logging import httpx logger = logging.getLogger(__name__) MAX_RETRIES = 3 BASE_DELAY = 2.0 async def retry_with_backoff(coro_factory, max_retries: int = MAX_RETRIES): for attempt in range(max_retries + 1): try: resp = await coro_factory() if resp.status_code == 429: retry_after = resp.headers.get("Retry-After", str(BASE_DELAY * (2**attempt))) wait = float(retry_after) logger.warning("clickup rate limited (429), retry after %.1fs, attempt %d", wait, attempt + 1) await asyncio.sleep(wait) continue return resp except httpx.HTTPStatusError: if attempt < max_retries: await asyncio.sleep(BASE_DELAY * (2**attempt)) else: raise except Exception: if attempt < max_retries: await asyncio.sleep(BASE_DELAY * (2**attempt)) else: raise raise RuntimeError("clickup rate limit exceeded after max retries")