diff --git a/labthings/core/utilities.py b/labthings/core/utilities.py index 20a90796..08da405a 100644 --- a/labthings/core/utilities.py +++ b/labthings/core/utilities.py @@ -2,8 +2,16 @@ import re import base64 import operator +import sys from functools import reduce +try: + from collections.abc import OrderedDict +except ImportError: + from collections import OrderedDict + +PY3 = sys.version_info > (3,) + def get_docstring(obj): ds = obj.__doc__ diff --git a/labthings/server/representations.py b/labthings/server/representations.py new file mode 100644 index 00000000..9ee1a056 --- /dev/null +++ b/labthings/server/representations.py @@ -0,0 +1,28 @@ +from flask import make_response, current_app +from json import dumps + +from ..core.utilities import PY3 + + +def output_json(data, code, headers=None): + """Makes a Flask response with a JSON encoded body""" + + settings = current_app.config.get("RESTFUL_JSON", {}) + + # If we're in debug mode, and the indent is not set, we set it to a + # reasonable value here. Note that this won't override any existing value + # that was set. We also set the "sort_keys" value. + if current_app.debug: + settings.setdefault("indent", 4) + settings.setdefault("sort_keys", not PY3) + + # always end the json dumps with a new line + # see https://github.com/mitsuhiko/flask/pull/1262 + dumped = dumps(data, **settings) + "\n" + + resp = make_response(dumped, code) + resp.headers.extend(headers or {}) + return resp + + +DEFAULT_REPRESENTATIONS = [("application/json", output_json)] diff --git a/labthings/server/utilities.py b/labthings/server/utilities.py index 404bf630..6e4870c2 100644 --- a/labthings/server/utilities.py +++ b/labthings/server/utilities.py @@ -1,15 +1,19 @@ -from ..core.utilities import get_docstring, get_summary - -from .view import View +from ..core.utilities import get_summary +from werkzeug.http import HTTP_STATUS_CODES from flask import current_app +def http_status_message(code): + """Maps an HTTP status code to the textual status""" + return HTTP_STATUS_CODES.get(code, "") + + def description_from_view(view_class): summary = get_summary(view_class) methods = [] - for method_key in View.methods: + for method_key in view_class.methods: if hasattr(view_class, method_key): methods.append(method_key.upper()) @@ -24,3 +28,23 @@ def description_from_view(view_class): def view_class_from_endpoint(endpoint: str): return current_app.view_functions[endpoint].view_class + + +def unpack(value): + """Return a three tuple of data, code, and headers""" + if not isinstance(value, tuple): + return value, 200, {} + + try: + data, code, headers = value + return data, code, headers + except ValueError: + pass + + try: + data, code = value + return data, code, {} + except ValueError: + pass + + return value, 200, {} diff --git a/labthings/server/view.py b/labthings/server/view.py index 0998a199..b58ebba4 100644 --- a/labthings/server/view.py +++ b/labthings/server/view.py @@ -1,4 +1,11 @@ from flask.views import MethodView +from flask import request +from werkzeug.wrappers import Response as ResponseBase + +from labthings.core.utilities import OrderedDict + +from labthings.server.utilities import unpack +from labthings.server.representations import DEFAULT_REPRESENTATIONS class View(MethodView): @@ -15,6 +22,10 @@ class View(MethodView): def __init__(self, *args, **kwargs): MethodView.__init__(self, *args, **kwargs) + # Set the default representations + # TODO: Inherit from parent LabThing. See original flask_restful implementation + self.representations = OrderedDict(DEFAULT_REPRESENTATIONS) + def doc(self): docs = {"operations": {}} if hasattr(self, "__apispec__"): @@ -25,3 +36,31 @@ def doc(self): docs["operations"][meth] = {} docs["operations"][meth] = getattr(self, meth).__apispec__ return docs + + def dispatch_request(self, *args, **kwargs): + meth = getattr(self, request.method.lower(), None) + + # If the request method is HEAD and we don't have a handler for it + # retry with GET. + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + assert meth is not None, "Unimplemented method %r" % request.method + + # Generate basic response + resp = meth(*args, **kwargs) + + if isinstance(resp, ResponseBase): # There may be a better way to test + return resp + + representations = self.representations or OrderedDict() + + # noinspection PyUnresolvedReferences + mediatype = request.accept_mimetypes.best_match(representations, default=None) + if mediatype in representations: + data, code, headers = unpack(resp) + resp = representations[mediatype](data, code, headers) + resp.headers["Content-Type"] = mediatype + return resp + + return resp diff --git a/labthings/server/views/docs/__init__.py b/labthings/server/views/docs/__init__.py index ef09d3d0..ed7176c2 100644 --- a/labthings/server/views/docs/__init__.py +++ b/labthings/server/views/docs/__init__.py @@ -1,11 +1,11 @@ from flask import ( - abort, url_for, jsonify, render_template, Blueprint, current_app, request, + make_response, ) from labthings.core.utilities import get_docstring @@ -14,8 +14,6 @@ from ...find import current_labthing from ...spec import rule_to_path, rule_to_params -import os - class APISpecView(View): """ @@ -35,7 +33,7 @@ class SwaggerUIView(View): """ def get(self): - return render_template("swagger-ui.html") + return make_response(render_template("swagger-ui.html")) class W3CThingDescriptionView(View):