import logging import time from collections import namedtuple from http import HTTPStatus from typing import Any, Dict, Iterable, Hashable, Optional import requests from requests.exceptions import ConnectionError, Timeout from update_market_vendors.constants import TIMEOUT, RETRY_COUNT, RETRY_WAIT from update_market_vendors.config import Config def get_session(headers: Dict[str, Any]) -> requests.Session: """Returned session with headers bind.""" session = requests.Session() session.headers.update(headers) return session def send_request( session: requests.Session, request: requests.Request, logger: logging.Logger, timeout: int = TIMEOUT, retry_count: int = RETRY_COUNT, retry_wait: int = RETRY_WAIT, allowed_statuses: tuple = (HTTPStatus.OK,) ) -> Optional[requests.Response]: """Function for sending request to remote service. :param session: session to send requests with. :param request: request object. :param logger: logger object. :param timeout: timeout value for sending request. :param retry_count: number of attempts to get response while getting exception or unexpected status. :param retry_wait: delay between sequential requests if TOO_MANY_REQUESTS was received. :param allowed_statuses: tuple of expected response statuses. """ request = session.prepare_request(request) retry_counter = 0 response = None while retry_counter < retry_count: try: response = session.send(request, timeout=timeout) except (ConnectionError, Timeout) as e: retry_counter += 1 logger.info( f"Got exception {e} on request to {request.url}. Request retry #{retry_counter}.") continue if response.status_code in allowed_statuses: break if response.status_code == HTTPStatus.TOO_MANY_REQUESTS: retry_counter += 1 logger.info( f"Too many requests from {request.url}. Request retry #{retry_counter} after waiting for {retry_wait}.") time.sleep(retry_wait) continue else: logger.info( f"Response from {request.url} has unavailable status {response.status_code}. " f"Request retry #{retry_counter}.") retry_counter += 1 continue if response is None or response.status_code not in allowed_statuses: logger.error(f"No correct response from {request.url}") return return response def get_headers(config: Config) -> Dict[str, Any]: """Return headers dict for markets request""" return { "Content-Type": "application/json", "Authorization": f"{config.VENDORAPI_APPKEY}" } _MarketsResult = namedtuple('MarketsResult', 'confirmed skipped') class MarketsResult(_MarketsResult): """Class for storing result of getting markets.""" def __new__(cls, confirmed: Iterable[Hashable], skipped: Iterable[Hashable]): """Packs data to sets. :param confirmed: iterable collection of markets for which positive responses were obtained. :param skipped: iterable collection of markets for which response could not be received. """ return super().__new__(cls, set(confirmed), set(skipped))