import requests from requests.exceptions import ConnectionError, Timeout from abc import abstractmethod from apollo_utils.service.exceptions import BadGateway, ServiceUnavailable from flask import current_app as app from http import HTTPStatus from typing import List, Union class APIUnauthorized(BadGateway): """Raised if request to API failed with authorization error. This exception possibly means that app configuration is incorrect. """ detail: str = "Authentication failed" class APIMisconfigured(BadGateway): """Raised if API configuration is incorrect.""" detail: str = "Please set all necessary client settings" class APIUnavailable(ServiceUnavailable): """Raised if connection to API cannot be established or timeout exceeded.""" pass class APIInvalidResponse(BadGateway): """Raised if API returned response which can not be handled.""" detail: str = "Invalid response" class BaseAPIClient: """Object oriented interface base for some API client.""" _request_timeout: str = None env_request_timeout: str = "DEFAULT_REQUEST_TIMEOUT" service_name: str = None 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(service=self.service_name) if response.status_code == HTTPStatus.UNAUTHORIZED: raise APIUnauthorized(service=self.service_name) try: response_json = response.json() except ValueError: response_json = None if response.status_code != HTTPStatus.OK: raise APIInvalidResponse( service=self.service_name, original_status_code=response.status_code, original_response=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