import logging from http import HTTPStatus from time import sleep from typing import Any, Callable, Coroutine, Dict, List, Optional, Type, Union from requests import Session, exceptions # type: ignore import config from utils.external_clients.exceptions import APIError log = logging.getLogger(__name__) def make_request( session: Session, url: str, method: str = "GET", params: Union[dict, List[tuple], None] = None, headers: Optional[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: with session.request(method, url, params=params, headers=headers, auth=auth, json=body, data=data) as resp: if resp.content and resp.headers.get("Content-Type") == "application/json": result = resp.json() try: resp.raise_for_status() except exceptions.HTTPError as e: raise error_cls( detail=error_message or str(e), headers=e.response.headers, # type: ignore original_status_code=e.response.status_code, # type: ignore original_response=result, status_code=e.response.status_code, # type: ignore ) except exceptions.ConnectionError as ce: raise error_cls( detail=ce.strerror if hasattr(ce, "strerror") else "Connection error", # type: ignore status_code=400, # type: ignore ) 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: Optional[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): def wrapped(self, *args, **kwargs): wait_time = 0 for i in range(count + 1): try: return 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 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 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 sleep(current_time) return wrapped return inner