import json import logging import time import urllib.parse from os import getenv import requests from notifications_python_client import __version__ from notifications_python_client.authentication import create_jwt_token from notifications_python_client.errors import HTTPError, InvalidResponse logger = logging.getLogger(__name__) API_HOST_NAME = getenv("API_HOST_NAME", "http://localhost:6011") class BaseAPIClient: """ Base class for GOV.UK Notify API client. This class is not thread-safe. """ def __init__(self, api_key, base_url=API_HOST_NAME, timeout=30): """ Initialise the client Error if either of base_url or secret missing :param base_url - base URL of GOV.UK Notify API: :param secret - application secret - used to sign the request: :param timeout - request timeout on the client :return: """ service_id = api_key[-73:-37] api_key = api_key[-36:] assert base_url, "Missing base url" assert service_id, "Missing service ID" assert api_key, "Missing API key" self.base_url = base_url self.service_id = service_id self.api_key = api_key self.timeout = timeout self.request_session = requests.Session() def put(self, url, data): return self.request("PUT", url, data=data) def get(self, url, params=None): return self.request("GET", url, params=params) def post(self, url, data): return self.request("POST", url, data=data) def delete(self, url, data=None): return self.request("DELETE", url, data=data) def generate_headers(self, api_token): return { "Content-type": "application/json", "Authorization": f"Bearer {api_token}", "User-agent": f"NOTIFY-API-PYTHON-CLIENT/{__version__}", } def request(self, method, url, data=None, params=None): logger.debug("API request %s %s", method, url) url, kwargs = self._create_request_objects(url, data, params) response = self._perform_request(method, url, kwargs) return self._process_json_response(response) def _create_request_objects(self, url, data, params): api_token = create_jwt_token(self.api_key, self.service_id) kwargs = {"headers": self.generate_headers(api_token), "timeout": self.timeout} if data is not None: kwargs.update(data=self._serialize_data(data)) if params is not None: kwargs.update(params=params) url = urllib.parse.urljoin(str(self.base_url), str(url)) return url, kwargs def _serialize_data(self, data): return json.dumps(data, default=self._extended_json_encoder) def _extended_json_encoder(self, obj): if isinstance(obj, set): return list(obj) raise TypeError def _perform_request(self, method, url, kwargs): start_time = time.monotonic() try: response = self.request_session.request(method, url, **kwargs) response.raise_for_status() return response except requests.RequestException as e: api_error = HTTPError.create(e) logger.warning( f"API {method} request failed with {api_error.status_code} {api_error.message}" ) raise api_error from e finally: elapsed_time = time.monotonic() - start_time logger.debug(f"API {method} request finished in {elapsed_time}") def _process_json_response(self, response): try: if response.status_code == 204: return return response.json() except ValueError as e: raise InvalidResponse( response, message="No JSON response object could be decoded" ) from e