Files
Imago/sdk/imago_client/utils.py
T
bruno 2f2a5e9b4a
CI / Lint & Format (push) Failing after 28s
CI / Tests (push) Has been skipped
CI / Security Scan (push) Failing after 8s
CI / Docker Build (push) Has been skipped
Add admin panel, WebSocket support, and API versioning
Introduce an admin portal (React + Nginx), WebSocket routing, and
API versioning middleware with `/api/v1/` prefix deprecation.
Add master API key authentication, new Prometheus metrics for AI
token consumption and active WebSockets, and extend S3 config
with a public endpoint URL. Update test paths and fixtures to
align with the new routing structure.
2026-06-22 11:25:22 -04:00

78 lines
2.9 KiB
Python

"""
Utility functions for the Imago Client SDK.
"""
import asyncio
import logging
from typing import Any, Callable, Coroutine, TypeVar
from httpx import HTTPStatusError, RequestError
from .exceptions import APIError, AuthError, NotFoundError, QuotaError, ImagoError
logger = logging.getLogger("imago_client")
T = TypeVar("T")
async def with_retry(
coro_func: Callable[[], Coroutine[Any, Any, T]],
max_retries: int = 3,
initial_backoff: float = 1.0,
max_backoff: float = 10.0,
backoff_factor: float = 2.0,
) -> T:
"""Executes an async function with exponential backoff on retryable HTTP errors."""
retries = 0
backoff = initial_backoff
while True:
try:
return await coro_func()
except RequestError as e:
if retries >= max_retries:
raise ImagoError(f"Request failed after {max_retries} retries: {e}") from e
logger.warning(f"Request failed ({e}), retrying in {backoff:.1f}s...")
await asyncio.sleep(backoff)
retries += 1
backoff = min(backoff * backoff_factor, max_backoff)
except HTTPStatusError as e:
# Handle specific status codes
status = e.response.status_code
if status in (408, 429, 500, 502, 503, 504):
if status == 429 and retries >= max_retries:
raise QuotaError("Rate limit exceeded") from e
if retries >= max_retries:
raise APIError(f"HTTP {status} failed after {max_retries} retries", status_code=status) from e
# specific handling for 429 Retry-After header
if status == 429 and "Retry-After" in e.response.headers:
try:
backoff = float(e.response.headers["Retry-After"])
except ValueError:
pass
logger.warning(f"HTTP {status}, retrying in {backoff:.1f}s...")
await asyncio.sleep(backoff)
retries += 1
backoff = min(backoff * backoff_factor, max_backoff)
else:
_handle_http_error(e)
def _handle_http_error(e: HTTPStatusError) -> None:
"""Raises appropriate SDK exceptions based on HTTP status code."""
status = e.response.status_code
try:
details = e.response.json()
except Exception:
details = {"detail": e.response.text}
if status in (401, 403):
# 403 might indicate missing scope or quota limit depending on details
if status == 403 and "quota" in str(details).lower():
raise QuotaError("Quota exceeded")
raise AuthError(f"Authentication failed: {details.get('detail')}")
elif status == 404:
raise NotFoundError("Resource not found")
else:
raise APIError(f"API returned {status}: {details.get('detail')}", status_code=status, details=details)