OpServ-Monitoring/opserv-backend

View on GitHub
app/server/apis/endpoint.py

Summary

Maintainability
A
35 mins
Test Coverage
import traceback

import tornado.escape
import tornado.web

from misc import standalone_helper
from server.data_gates.default_data_gate import DefaultDataGate


class Endpoint(tornado.web.RequestHandler):
    """
    The base class for any http endpoint to use. This assures a equal response structure and error handling
    by providing additional methods to the ones provided by `tornado.web.RequestHandler`.

    A response should be made by calling ``respond`` instead of using the build-in `write` function.
    """

    # The sole interface all api endpoints should utilize to receive and persist data
    # Also this allows for easier unit testing through dependency injection.
    _outbound_gate = DefaultDataGate

    def set_default_headers(self):
        """
        As we want to allow HEAD requests to retrieve the allowed HTTP methods of an endpoint we have to add the
        corresponding header to every response.
        In addition data returned in the body of any REST endpoint response is formatted in the JSON.
        """
        super().set_default_headers()

        self.set_header("Content-Type", "application/vnd.api+json")
        self.set_header("Allow", "HEAD")

    def head(self):
        """
        By default any request processed by the `tornado.web.RequestHandler` returns a `HTTPError` with
        code 405 "Method Not Allowed". As we want to allow HEAD requests to retrieve the allowed HTTP methods of an
        endpoint we have to override the default behaviour.
        """

    # TODO Add documentation
    def respond(self, data) -> None:
        """


        :param data:
            The data to be part of the response. Should either be a resource object in form of a dictionary or
             a list of resource object. For more information visit http://jsonapi.org.
        """
        path = self.get_path()

        self.write(
            tornado.escape.json_encode({
                "data": data,
                "links": {
                    "self": path
                }
            })
        )

    def get_path(self) -> str:
        """
        A helper method to retrieve full URL as called by the request striping a possibly trailing slash.

        :return: A string representing the URL called by the request
        """
        path = self.request.protocol + "://" + self.request.host + self.request.uri
        if path.endswith("/"):
            path = path[:-1]

        return path

    @classmethod
    def get_resource_object(cls, resource_type: str, resource_id: str, attributes: dict = None) -> dict:
        """
        A helper method that builds a resource object that may be used as part of a single resource object presentation.

        :param resource_type:
            A string that represent the type of this resource
        :param resource_id:
            A string that uniquely identifies the resource of this resource type
        :param attributes:
            A dictionary holding any information of the resource. It should not include type or id of the resource as
            both of these are treated separately

        :return:
            A dictionary object holding the passed information in form of a resource object with attributes used for
            requests that target single resources. For more information visit http://jsonapi.org.
        """
        data = {
            "type": resource_type,
            "id": resource_id
        }

        if attributes is not None and len(attributes) > 0:
            data["attributes"] = attributes

        return data

    @classmethod
    def get_resource_reference(cls, resource_type: str, resource_id: str, parent_path: str) -> dict:
        """
        A helper method that builds a resource object that may be used as part of a resource object collection.

        :param resource_type:
            A string that represent the type of this resource
        :param resource_id:
            A string that uniquely identifies the resource of this resource type
        :param parent_path:
            The URI of the endpoint that presents the list of resource objects for this resource type

        :return:
            A dictionary object holding the passed information in form of a resource object without attributes used
            for requests that target resource collections. For more information visit http://jsonapi.org.
        """
        encoded_id = standalone_helper.encode_string(resource_id)

        return {
            "type": resource_type,
            "id": resource_id,
            "links": {
                "self": parent_path + "/" + encoded_id
            }
        }

    def write_error(self, status_code, **kwargs) -> None:
        """
        Overrides the built-in function to implement custom error responses.
        If in debug mode and caused by an uncaught exception this will add the stacktrace to the error response.

        If you want to provide more detailed information you may pass a "summary" or more "details" into the kwargs
        of `send_error`.

        :param status_code:
            The status code that was passed either by a raised `HTTPError` or a call to `send_error`
        :param kwargs:
            May include additional information in case such is passed to the kwargs of ``send_error``.
            If this error was caused by an uncaught exception (including HTTPError), an ``exc_info`` triple will be
            available as ``kwargs["exc_info"]`` including the stack trace.
        """
        if "summary" in kwargs and kwargs["summary"] is not None:
            if self._reason is None:
                self._reason = ""
            self._reason += " - "
            self._reason += kwargs["summary"]

        error = {
            "status": status_code,
            "title": self._reason,
        }

        if "details" in kwargs and kwargs["details"] is not None:
            error["detail"] = kwargs["details"]

        # in debug mode, try to append the error trace
        if self.settings.get("serve_traceback") and "exc_info" in kwargs:
            error_trace = []

            for line in traceback.format_exception(*kwargs["exc_info"]):
                error_trace.append(line)

            error["meta"] = {
                "stacktrace": error_trace
            }

        response = {
            "errors": [
                error
            ]
        }

        self.finish(tornado.escape.json_encode(response))