Skip to content

Adapters

Base API adapter implementation for odin-control.

Tim Nicholls, STFC Detector System Software Group

ApiAdapter

API adapter base class.

This class defines the basis for all API adapters and provides default methods for supported HTTP verbs and for lifecycle management. Dervied adapters can either override these methods explciitly to provide custom behavior, or adopt the adapter-controller pattern by specifying controller and error clases, and allowing default methods in this class to interface requests to the controller.

Source code in src/odin_control/adapters/adapter.py
class ApiAdapter():
    """API adapter base class.

    This class defines the basis for all API adapters and provides default methods for supported
    HTTP verbs and for lifecycle management. Dervied adapters can either override these methods
    explciitly to provide custom behavior, or adopt the adapter-controller pattern by specifying
    controller and error clases, and allowing default methods in this class to interface requests
    to the controller.
    """

    # Derived classes can specify controller and error classes to use
    controller_cls = None
    error_cls = BaseError

    # Derived classes should override this with a relevant version string
    version = "unknown"

    # Flag to indicate if adapter is async; default to False
    is_async = False

    def __init__(self, **kwargs):
        """Initialise the ApiAdapter object.

        This method initialises the adapter, loading any keyword arguments into the options
        dictionary, and instantiating the controller if a controller class has been specified

        :param kwargs: keyword argument list that is copied into options dictionary
        """
        self.name = type(self).__name__

        # Load any keyword arguments into the adapter options dictionary
        self.options = {}
        for kw in kwargs:
            self.options[kw] = kwargs[kw]

        # Instantiate the controller if a controller class has been specified
        if self.controller_cls:
            self.controller = self.controller_cls(self.options)
        else:
            self.controller = None

        logging.debug("%s loaded", self.name)

    def initialize(self, adapters):
        """Initialize the ApiAdapter after it has been registered by the API Route.

        This method allows the adapter to perform any initialization that requires access to other
        loaded adapters. It receives a dictionary of loaded adapters from the API route and passes
        them to the controller's initialize method, if a controller has been configured.

        :param adapters: a dictionary of the adapters loaded by the API route.
        """
        logging.debug("%s initialize called with %d adapters", self.name, len(adapters))

        # Build a dictionary of other adapters excluding self
        adapters = {name: adapter for name, adapter in adapters.items() if adapter is not self}

        # Initialize the controller with access to other adapters
        if self.controller:
            try:
                self.controller.initialize(adapters)
            except AttributeError:
                logging.warning("%s controller has no initialize method", self.name)
        else:
            logging.warning("%s has no controller configured", self.name)


    @response_types("application/json", default="application/json")
    @require_controller
    def get(self, path, request):
        """Handle an HTTP GET request.

        This method is a default implementation of the GET request handler for adapters. It calls
        the get method of the controller and returns the result. Error handling is provided to
        catch controller errors and return appropriate error responses.

        :param path: URI path of resource
        :param request: HTTP request object passed from handler
        :return: ApiAdapterResponse container of data, content-type and status_code
        """
        content_type = "application/json"

        try:
            response = self.controller.get(path, wants_metadata(request))
            status_code = 200
        except self.error_cls as error:
            response = {"error": str(error)}
            status_code = 400

        return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

    @request_types("application/json", "application/vnd.odin-native")
    @response_types("application/json", default="application/json")
    @require_controller
    def put(self, path, request):
        """Handle an HTTP PUT request.

        This method is a default implementation of the PUT request handler for adapters. It calls
        the set method of the controller with the specified data and returns the result of a
        subsequent get call. Error handling is provided to catch controller errors and return
        appropriate error responses.

        :param path: URI path of resource
        :param request: HTTP request object passed from handler
        :return: ApiAdapterResponse container of data, content-type and status_code
        """
        content_type = "application/json"

        try:
            data = json_decode(request.body)
            self.controller.set(path, data)
            response = self.controller.get(path)
            status_code = 200
        except self.error_cls as error:
            response = {"error": str(error)}
            status_code = 400
        except NotImplementedError:
            response = {
                "error": f"{self.name} does not support PUT requests"
            }
            status_code = 405
        except (TypeError, ValueError) as error:
            response = {"error": f"Failed to decode PUT request body: {str(error)}"}
            status_code = 400

        return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)


    @request_types("application/json", "application/vnd.odin-native")
    @response_types("application/json", default="application/json")
    @require_controller
    def post(self, path, request):
        """Handle an HTTP POST request.

        This method is a default implementation of the POST request handler for adapters. It calls
        the create method of the controller and returns the result. Error handling is provided to
        catch controller errors and return appropriate error responses.

        :param path: URI path of resource
        :param request: HTTP request object passed from handler
        :return: ApiAdapterResponse container of data, content-type and status_code
        """
        content_type = "application/json"

        try:
            data = json_decode(request.body)
            response = self.controller.create(path, data)
            status_code = 200
        except self.error_cls as error:
            response = {"error": str(error)}
            status_code = 400
        except NotImplementedError:
            response = {
                "error": f"{self.name} does not support POST requests"
            }
            status_code = 405
        except (TypeError, ValueError) as error:
            response = {"error": f"Failed to decode POST request body: {str(error)}"}
            status_code = 400

        return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

    @response_types("application/json", default="application/json")
    @require_controller
    def delete(self, path, request):
        """Handle an HTTP DELETE request.

        This method is a default implementation of the DELETE request handler for adapters. It calls
        the delete method of the controller and returns the result. Error handling is provided to
        catch controller errors and return appropriate error responses.

        :param path: URI path of resource
        :param request: HTTP request object passed from handler
        :return: ApiAdapterResponse container of data, content-type and status_code
        """
        content_type = "application/json"

        try:
            response = self.controller.delete(path)
            status_code = 200
        except self.error_cls as error:
            response = {"error": str(error)}
            status_code = 400
        except NotImplementedError:
            response = {
                "error": f"{self.name} does not support DELETE requests"
            }
            status_code = 405

        return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

    def cleanup(self):
        """Clean up adapter state.

        This method is a default implementation of the cleanup mechanism provided to allow adapters
        to clean up their state (e.g. disconnect cleanly from the device being controlled, set
        some status message).
        """
        logging.debug("%s cleanup called",  self.name)

        # If a controller is configured, call its cleanup method
        if self.controller:
            try:
                self.controller.cleanup()
            except AttributeError:
                logging.warning("%s controller has no cleanup method", self.name)
        else:
            logging.warning("%s has no controller configured", self.name)

__init__(**kwargs)

Initialise the ApiAdapter object.

This method initialises the adapter, loading any keyword arguments into the options dictionary, and instantiating the controller if a controller class has been specified

Parameters:

Name Type Description Default
kwargs

keyword argument list that is copied into options dictionary

{}
Source code in src/odin_control/adapters/adapter.py
def __init__(self, **kwargs):
    """Initialise the ApiAdapter object.

    This method initialises the adapter, loading any keyword arguments into the options
    dictionary, and instantiating the controller if a controller class has been specified

    :param kwargs: keyword argument list that is copied into options dictionary
    """
    self.name = type(self).__name__

    # Load any keyword arguments into the adapter options dictionary
    self.options = {}
    for kw in kwargs:
        self.options[kw] = kwargs[kw]

    # Instantiate the controller if a controller class has been specified
    if self.controller_cls:
        self.controller = self.controller_cls(self.options)
    else:
        self.controller = None

    logging.debug("%s loaded", self.name)

cleanup()

Clean up adapter state.

This method is a default implementation of the cleanup mechanism provided to allow adapters to clean up their state (e.g. disconnect cleanly from the device being controlled, set some status message).

Source code in src/odin_control/adapters/adapter.py
def cleanup(self):
    """Clean up adapter state.

    This method is a default implementation of the cleanup mechanism provided to allow adapters
    to clean up their state (e.g. disconnect cleanly from the device being controlled, set
    some status message).
    """
    logging.debug("%s cleanup called",  self.name)

    # If a controller is configured, call its cleanup method
    if self.controller:
        try:
            self.controller.cleanup()
        except AttributeError:
            logging.warning("%s controller has no cleanup method", self.name)
    else:
        logging.warning("%s has no controller configured", self.name)

delete(path, request)

Handle an HTTP DELETE request.

This method is a default implementation of the DELETE request handler for adapters. It calls the delete method of the controller and returns the result. Error handling is provided to catch controller errors and return appropriate error responses.

Parameters:

Name Type Description Default
path

URI path of resource

required
request

HTTP request object passed from handler

required

Returns:

Type Description

ApiAdapterResponse container of data, content-type and status_code

Source code in src/odin_control/adapters/adapter.py
@response_types("application/json", default="application/json")
@require_controller
def delete(self, path, request):
    """Handle an HTTP DELETE request.

    This method is a default implementation of the DELETE request handler for adapters. It calls
    the delete method of the controller and returns the result. Error handling is provided to
    catch controller errors and return appropriate error responses.

    :param path: URI path of resource
    :param request: HTTP request object passed from handler
    :return: ApiAdapterResponse container of data, content-type and status_code
    """
    content_type = "application/json"

    try:
        response = self.controller.delete(path)
        status_code = 200
    except self.error_cls as error:
        response = {"error": str(error)}
        status_code = 400
    except NotImplementedError:
        response = {
            "error": f"{self.name} does not support DELETE requests"
        }
        status_code = 405

    return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

get(path, request)

Handle an HTTP GET request.

This method is a default implementation of the GET request handler for adapters. It calls the get method of the controller and returns the result. Error handling is provided to catch controller errors and return appropriate error responses.

Parameters:

Name Type Description Default
path

URI path of resource

required
request

HTTP request object passed from handler

required

Returns:

Type Description

ApiAdapterResponse container of data, content-type and status_code

Source code in src/odin_control/adapters/adapter.py
@response_types("application/json", default="application/json")
@require_controller
def get(self, path, request):
    """Handle an HTTP GET request.

    This method is a default implementation of the GET request handler for adapters. It calls
    the get method of the controller and returns the result. Error handling is provided to
    catch controller errors and return appropriate error responses.

    :param path: URI path of resource
    :param request: HTTP request object passed from handler
    :return: ApiAdapterResponse container of data, content-type and status_code
    """
    content_type = "application/json"

    try:
        response = self.controller.get(path, wants_metadata(request))
        status_code = 200
    except self.error_cls as error:
        response = {"error": str(error)}
        status_code = 400

    return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

initialize(adapters)

Initialize the ApiAdapter after it has been registered by the API Route.

This method allows the adapter to perform any initialization that requires access to other loaded adapters. It receives a dictionary of loaded adapters from the API route and passes them to the controller's initialize method, if a controller has been configured.

Parameters:

Name Type Description Default
adapters

a dictionary of the adapters loaded by the API route.

required
Source code in src/odin_control/adapters/adapter.py
def initialize(self, adapters):
    """Initialize the ApiAdapter after it has been registered by the API Route.

    This method allows the adapter to perform any initialization that requires access to other
    loaded adapters. It receives a dictionary of loaded adapters from the API route and passes
    them to the controller's initialize method, if a controller has been configured.

    :param adapters: a dictionary of the adapters loaded by the API route.
    """
    logging.debug("%s initialize called with %d adapters", self.name, len(adapters))

    # Build a dictionary of other adapters excluding self
    adapters = {name: adapter for name, adapter in adapters.items() if adapter is not self}

    # Initialize the controller with access to other adapters
    if self.controller:
        try:
            self.controller.initialize(adapters)
        except AttributeError:
            logging.warning("%s controller has no initialize method", self.name)
    else:
        logging.warning("%s has no controller configured", self.name)

post(path, request)

Handle an HTTP POST request.

This method is a default implementation of the POST request handler for adapters. It calls the create method of the controller and returns the result. Error handling is provided to catch controller errors and return appropriate error responses.

Parameters:

Name Type Description Default
path

URI path of resource

required
request

HTTP request object passed from handler

required

Returns:

Type Description

ApiAdapterResponse container of data, content-type and status_code

Source code in src/odin_control/adapters/adapter.py
@request_types("application/json", "application/vnd.odin-native")
@response_types("application/json", default="application/json")
@require_controller
def post(self, path, request):
    """Handle an HTTP POST request.

    This method is a default implementation of the POST request handler for adapters. It calls
    the create method of the controller and returns the result. Error handling is provided to
    catch controller errors and return appropriate error responses.

    :param path: URI path of resource
    :param request: HTTP request object passed from handler
    :return: ApiAdapterResponse container of data, content-type and status_code
    """
    content_type = "application/json"

    try:
        data = json_decode(request.body)
        response = self.controller.create(path, data)
        status_code = 200
    except self.error_cls as error:
        response = {"error": str(error)}
        status_code = 400
    except NotImplementedError:
        response = {
            "error": f"{self.name} does not support POST requests"
        }
        status_code = 405
    except (TypeError, ValueError) as error:
        response = {"error": f"Failed to decode POST request body: {str(error)}"}
        status_code = 400

    return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)

put(path, request)

Handle an HTTP PUT request.

This method is a default implementation of the PUT request handler for adapters. It calls the set method of the controller with the specified data and returns the result of a subsequent get call. Error handling is provided to catch controller errors and return appropriate error responses.

Parameters:

Name Type Description Default
path

URI path of resource

required
request

HTTP request object passed from handler

required

Returns:

Type Description

ApiAdapterResponse container of data, content-type and status_code

Source code in src/odin_control/adapters/adapter.py
@request_types("application/json", "application/vnd.odin-native")
@response_types("application/json", default="application/json")
@require_controller
def put(self, path, request):
    """Handle an HTTP PUT request.

    This method is a default implementation of the PUT request handler for adapters. It calls
    the set method of the controller with the specified data and returns the result of a
    subsequent get call. Error handling is provided to catch controller errors and return
    appropriate error responses.

    :param path: URI path of resource
    :param request: HTTP request object passed from handler
    :return: ApiAdapterResponse container of data, content-type and status_code
    """
    content_type = "application/json"

    try:
        data = json_decode(request.body)
        self.controller.set(path, data)
        response = self.controller.get(path)
        status_code = 200
    except self.error_cls as error:
        response = {"error": str(error)}
        status_code = 400
    except NotImplementedError:
        response = {
            "error": f"{self.name} does not support PUT requests"
        }
        status_code = 405
    except (TypeError, ValueError) as error:
        response = {"error": f"Failed to decode PUT request body: {str(error)}"}
        status_code = 400

    return ApiAdapterResponse(response, content_type=content_type, status_code=status_code)