import requests from abc import abstractmethod from http import HTTPStatus from requests.exceptions import ConnectionError, Timeout from typing import List, Union from flask import current_app as app class BaseAPIError(Exception): """Base class for API client related errors. """ status_code: int = HTTPStatus.BAD_GATEWAY message: str = None response_data: dict = None def __init__( self, message: str = None, status_code: int = None, response_data: dict = None, exception: Exception = None ): if message: self.message = message elif exception: self.message = str(exception) if status_code: self.status_code = status_code self.response_data = response_data class APIUnauthorized(BaseAPIError): """Raised if request to API failed with authorization error. This exception possibly means that app configuration is incorrect. """ message: str = "Authentication failed" status_code: int = HTTPStatus.UNAUTHORIZED class APIMisconfigured(BaseAPIError): """Raised if API configuration is incorrect. """ message: str = "Please set all necessary client settings" class APIUnavailable(BaseAPIError): """Raised if connection to API cannot be established or timeout exceeded. """ message: str = "Service is unavailable" class APIInvalidResponse(BaseAPIError): """Raised if API returned response which can not be handled. """ message: str = "Invalid response" class BaseAPIClient: """Object oriented interface base for some API client. """ _request_timeout: str = None env_request_timeout: str = "DEFAULT_REQUEST_TIMEOUT" def __init__(self, session: requests.Session): self._session = session self._request_timeout = app.config.get(self.env_request_timeout) self._check_args() @abstractmethod def _check_args(self): """Check all client args.""" @abstractmethod def _set_auth_header(self, request: requests.Request): """Set Auth header.""" def _send_request(self, request: requests.Request) -> Union[List[dict], dict]: """Send HTTP request to API endpoint. Args: request: Request to send. Returns: dict: Response JSON content from API. Raises: APIUnavailable: If connection cannot be established or timeout exceeded. APIUnauthorized: If the app is configured with incorrect API app key. APIInvalidResponse: Non OK (200) HTTP code response. """ self._set_auth_header(request) prepared_request = self._session.prepare_request(request) try: response = self._session.send(prepared_request, timeout=self._request_timeout) except (ConnectionError, Timeout): raise APIUnavailable() if response.status_code == HTTPStatus.UNAUTHORIZED: raise APIUnauthorized() try: response_json = response.json() except ValueError: response_json = None if response.status_code != HTTPStatus.OK: raise APIInvalidResponse(status_code=response.status_code, response_data=response_json) return response_json class BaseInternalAPIClient(BaseAPIClient): """Object oriented interface base for some API client. """ _host_name: str = None _app_key: str = None env_host_name: str = None env_app_key: str = None def __init__(self, session: requests.Session): if self.env_host_name: self._host_name = app.config.get(self.env_host_name) if self.env_app_key: self._app_key = app.config.get(self.env_app_key) super().__init__(session) def _check_args(self): if self._host_name is None or self._app_key is None: raise APIMisconfigured() def _set_auth_header(self, request: requests.Request): request.headers.update({"Authorization": self._app_key}) def check_health(self): """Check API health. """ response = self._session.get(f"{self._host_name}/health") return response.status_code == HTTPStatus.OK