Source code for cdh.rest.client.clients.resources

from django.core.exceptions import ImproperlyConfigured
from requests.exceptions import ConnectionError

from ._base import BaseClient, host_unreachable
from ..registry import registry
from ..operations import Operations
from cdh.rest.exceptions import OperationNotEnabled
from ..logging import transaction_logger as logger


[docs]class ResourceClient(BaseClient): """Default API client for resources"""
[docs] def __init__(self): super(ResourceClient, self).__init__() self.supported_operations = None self._get_enabled = False self._get_over_post_enabled = False self._delete_enabled = False self._update_enabled = False self._put_enabled = False self._send_as_json = False
[docs] def contribute_to_class(self, cls, _) -> None: """This configures the client to the specific resources class""" meta = cls._meta self.path = meta.path self.path_variables = meta.path_variables self.meta = meta self.supported_operations = meta.supported_operations self._send_as_json = meta.default_send_as_json # The rest is irrelevant if we don't have a path configured if not self.path: return # Looping like this feels faster than 5 'x in self.supported_operations' # calls for operation in self.supported_operations: if not isinstance(operation, Operations): raise ImproperlyConfigured("Invalid operation supplied!") if operation == Operations.get: self._get_enabled = True logger.debug(f"{repr(self)}: GET enabled") elif operation == Operations.get_over_post: self._get_over_post_enabled = True logger.debug(f"{repr(self)}: GET over POST enabled") elif operation == Operations.delete: self._delete_enabled = True logger.debug(f"{repr(self)}: DELETE enabled") elif operation == Operations.put: self._put_enabled = True logger.debug(f"{repr(self)}: PUT enabled") else: raise NotImplementedError( "Operation not implemented! Please add operation '{}' " "support!".format( operation.name ) ) if self._get_enabled and self._get_over_post_enabled: logger.warning(f"{repr(self)}: GET and GET_OVER_POST are " f"both enabled. Will use GET_OVER_POST. Please " f"don't do this.")
[docs] def get(self, **kwargs): """Gets a resources from the API. Either over GET or GET_OVER_POST, depending on configuration. Any kwargs supplied, that are not used for path variables, will be sent to the API. In case of GET, these will be supplied as parameters. In case of GET_OVER_POST, these will be supplied in the POST body. :param kwargs: Any additional info to be sent. :return: """ if not self._get_enabled and not self._get_over_post_enabled: raise OperationNotEnabled method = self._http_client.get if self._get_over_post_enabled: method = self._http_client.post url, kwargs = self._make_url(**kwargs) try: logger.info(f"{repr(self)}: GETting {url}") request = method( url, kwargs, headers=self._make_auth_headers(), # TODO use auth param instead ) except ConnectionError: logger.warning(f"{repr(self)}: Host {url} unreachable") host_unreachable() return None if request.ok: logger.info(f"{repr(self)}: Data retrieved") logger.debug(f"{repr(self)}: {request.content}") return self.meta.resource(**request.json()) self._handle_api_error(request)
[docs] def put(self, obj, return_resource=None, as_json=None, **kwargs): """Posts a resources to the API. Please note that while it's called put, the actual HTTP method used is POST. PUT is not as supported as POST in many API frameworks, including Django. Any kwargs supplied, that are not used for path variables, will be sent to the API as parameters. Update/creation/update behaviour is up to the API. :param obj: The resources to be sent in the POST body. :param return_resource: An optional class that describes the resources that the server returns as a response. (A default can be specified on a resources level) :param as_json: If the request's json argument should be used over 'data'. Defaults to False. When True, the client will send the data as a JSON string to the server. If False, it will instead encode the data as 'multipart/form-data'. JSON is more flexible, as it allows for nested data structures. :param kwargs: Any additional info to be sent. :return: A return_response instance, or a Boolean (False indicated a connection error) """ logger.debug(f"{repr(self)}: Preparing PUT transaction") if not self._put_enabled: raise OperationNotEnabled if not return_resource: logger.debug(f"{repr(self)}: No return resource specified, falling" f" back to default return response") return_resource = self.meta.default_return_resource if isinstance(return_resource, str): app_label = obj._meta.app_label logger.debug(f"{repr(self)}: Resolving return resource") if len(return_resource.split('.')) == 2: app_label, return_resource = return_resource.split('.') return_resource = registry.get_resource(app_label, return_resource) if not as_json: logger.debug(f"{repr(self)}: No as_json specified, falling back on default") as_json = self._send_as_json url, kwargs = self._make_url(obj, **kwargs) try: request_kwargs = { 'params': kwargs, 'headers': self._make_auth_headers(), } logger.info(f"{repr(self)}: PUTting {url}") if as_json: request_kwargs['json'] = obj.to_api() else: request_kwargs['data'] = obj.to_api() logger.debug(f"{repr(self)}: {request_kwargs}") request = self._http_client.post( url, **request_kwargs ) except ConnectionError: logger.warning(f"{repr(self)}: Host {url} unreachable") host_unreachable() return False if request.ok: if return_resource: logger.info(f"{repr(self)}: Data retrieved") return return_resource(**request.json()) logger.info(f"{repr(self)}: Transaction successful") return True self._handle_api_error(request)
[docs] def delete(self, obj=None, **kwargs): """Performs a delete operation. :param obj: A resources object to retrieve path variable values from :param kwargs: Any additional http parameters to sent :return: a bool indicating if the request was executed successfully """ if not self._delete_enabled: raise OperationNotEnabled url, kwargs = self._make_url(obj, **kwargs) try: logger.info(f"{repr(self)}: DELETEing {url}") request = self._http_client.delete( url, params=kwargs, headers=self._make_auth_headers(), ) except ConnectionError: logger.warning(f"{repr(self)}: Host {url} unreachable") host_unreachable() return False return request.ok
def __str__(self): return '{} for {}'.format( self.__class__.__name__, self.meta.resource.__name__ ) def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self)