import asyncio import logging from http import HTTPStatus from typing import Any, Dict, Callable, Coroutine, List, Optional, Type, Union from aiohttp import ClientConnectionError, ClientResponseError, ClientSession import config from server.core.exceptions import APIError log = logging.getLogger(__name__) async def make_request( session: ClientSession, url: str, method: str = "GET", params: Union[dict, List[tuple], None] = None, headers: Dict = None, auth: Optional[Callable] = None, body: Optional[Dict] = None, data: Optional[Dict] = None, error_cls: Type[APIError] = APIError, error_message: Optional[str] = None, ): """Make http request, handle errors. :param session: Http app_client session. :param url: Request URL. :param method: HTTP method. :param params: Request params passed as a dict. :param headers: Headers. :param auth: Auth tuple. :param body: Request body. :param data: Request data. :param error_cls: Error class. :param error_message: Custom error message. """ result = None try: async with session.request( method, url, params=params, headers=headers, auth=auth, json=body, data=data, raise_for_status=False ) as resp: if resp.content and resp.content_type == "application/json": result = await resp.json() try: resp.raise_for_status() except ClientResponseError as e: raise error_cls( detail=error_message or e.message, headers=e.headers, # type: ignore original_status_code=e.status, # type: ignore original_response=result, status_code=e.status, # type: ignore ) except ClientConnectionError as e: raise error_cls( detail=e.strerror if hasattr(e, "strerror") else "Connection error", # type: ignore status_code=e.status if e.status else 400, # type: ignore ) except asyncio.CancelledError: raise error_cls(detail="Request was cancelled") return result def retry( count: int = config.DEFAULT_RETRY_COUNT, wait_rate: int = config.DEFAULT_RETRY_WAIT, max_timeout: int = config.DEFAULT_RETRY_MAX_TIMEOUT, auth_handler: Callable[[Any, APIError], Coroutine] = None, ): """Retry decorator. :param count: Retry count. :param wait_rate: Wait multiplier (current attempt number * wait_rate seconds). :param max_timeout: Max wait timeout. :param auth_handler: Auth error handler. """ def inner(f: Callable): async def wrapped(self, *args, **kwargs): wait_time = 0 for i in range(count + 1): try: return await f(self, *args, **kwargs) except APIError as e: # exit if it is the last try if i == count: raise # handle rate limits if e.get_status_code() == HTTPStatus.TOO_MANY_REQUESTS: retry_after = int(e.headers.get("Retry-After", 1)) wait_time = wait_time + retry_after # exit if it is needed to wait too much if wait_time > max_timeout: raise await asyncio.sleep(retry_after) # handle auth errors elif e.get_status_code() in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN, 419): # try to handle this only first time if i > 0 or not auth_handler: raise await auth_handler(self, e) # exit if it is an error where retry doesn't help elif e.get_status_code() < 500: raise else: # otherwise wait and retry current_time = wait_rate * (i + 1) wait_time = wait_time + current_time if wait_time > max_timeout: raise await asyncio.sleep(current_time) return wrapped return inner