diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index cb952312..9ce27a69 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -17,7 +17,6 @@ from openflexure_microscope.api.exceptions import JSONExceptionHandler from openflexure_microscope.api.utilities import list_routes from openflexure_microscope.config import settings_file_path, JSONEncoder -from openflexure_microscope.api.v1 import blueprints from openflexure_microscope.api import v2 from openflexure_microscope.common.labthings.labthing import LabThing @@ -88,6 +87,9 @@ labthing.register_device(api_microscope, "openflexure_microscope") for _plugin in find_plugins(USER_PLUGINS_PATH): labthing.register_plugin(_plugin) +from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing +add_captures_to_labthing(labthing, prefix="") + # WEBAPP ROUTES ### V2 diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py new file mode 100644 index 00000000..76e0f7ba --- /dev/null +++ b/openflexure_microscope/api/v2/views/captures.py @@ -0,0 +1,199 @@ +import logging +from flask import abort, request, redirect, url_for, send_file, jsonify + +from openflexure_microscope.api.utilities import get_bool, JsonResponse + +from openflexure_microscope.common.labthings.schema import Schema +from openflexure_microscope.common.labthings import fields +from openflexure_microscope.common.labthings.resource import Resource + +from openflexure_microscope.common.labthings.find import find_device + + +class CaptureSchema(Schema): + id = fields.String() + file = fields.String(data_key="path") + exists = fields.Bool(data_key="available") + filename = fields.String() + metadata = fields.Dict() + + # TODO: Add HTTP methods + links = fields.Hyperlinks( + { + "self": { + "href": fields.AbsoluteUrlFor("CaptureResource", id=""), + "mimetype": "application/json", + }, + "tags": { + "href": fields.AbsoluteUrlFor("CaptureTags", id=""), + "mimetype": "application/json", + }, + "metadata": { + "href": fields.AbsoluteUrlFor("CaptureMetadata", id=""), + "mimetype": "application/json", + }, + "download": { + "href": fields.AbsoluteUrlFor("CaptureDownload", id="", filename=""), + "mimetype": "image/jpeg", + } + } + ) + + +capture_schema = CaptureSchema() +capture_list_schema = CaptureSchema(many=True) + + +class CaptureList(Resource): + def get(self): + microscope = find_device("openflexure_microscope") + image_list = microscope.camera.images + return capture_list_schema.jsonify(image_list) + + +class CaptureResource(Resource): + def get(self, id): + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return capture_schema.jsonify(capture_obj) + + def delete(self, id): + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + capture_obj.delete() + + return ("", 204) + + +class CaptureDownload(Resource): + def get(self, id, filename): + + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + thumbnail = get_bool(request.args.get("thumbnail")) + + # If no filename is specified, redirect to the capture's currently set filename + if not filename: + return redirect( + url_for( + "DownloadAPI", + id=id, + filename=capture_obj.filename, + thumbnail=thumbnail, + ), + code=307, + ) + + # Download the image data using the requested filename + if thumbnail: + img = capture_obj.thumbnail + else: + img = capture_obj.data + + return send_file(img, mimetype="image/jpeg") + + +class CaptureTags(Resource): + def get(self, id): + + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return jsonify(capture_obj.tags) + + def put(self, id): + + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + capture_obj.put_tags(data_dict) + + return jsonify(capture_obj.tags) + + def delete(self, capture_id): + + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + for tag in data_dict: + capture_obj.delete_tag(str(tag)) + + return jsonify(capture_obj.tags) + + +class CaptureMetadata(Resource): + def get(self, id): + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + return jsonify(capture_obj.metadata) + + def put(self, id): + microscope = find_device("openflexure_microscope") + capture_obj = microscope.camera.image_from_id(id) + + if not capture_obj: + return abort(404) # 404 Not Found + + data_dict = JsonResponse(request).json + + if type(data_dict) != list: + return abort(400) + + # TODO: Allow putting system metadata maybe? + capture_obj.put_metadata(data_dict) + + return jsonify(capture_obj.metadata) + + +def add_captures_to_labthing(labthing, prefix=""): + """ + Add all capture resources to a labthing + """ + labthing.add_resource(CaptureList, f"{prefix}/captures", endpoint="CaptureList") + labthing.add_resource( + CaptureResource, f"{prefix}/captures/", endpoint="CaptureResource" + ) + labthing.add_resource( + CaptureDownload, f"{prefix}/captures//download/", endpoint="CaptureDownload" + ) + labthing.add_resource( + CaptureTags, f"{prefix}/captures//tags", endpoint="CaptureTags" + ) + labthing.add_resource( + CaptureMetadata, f"{prefix}/captures//metadata", endpoint="CaptureMetadata" + ) diff --git a/openflexure_microscope/common/labthings/fields.py b/openflexure_microscope/common/labthings/fields.py index e69de29b..7920f005 100644 --- a/openflexure_microscope/common/labthings/fields.py +++ b/openflexure_microscope/common/labthings/fields.py @@ -0,0 +1,153 @@ +from marshmallow.fields import * +from marshmallow import missing +import re +from flask import url_for + +_tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*") + + +def _tpl(val): + """Return value within ``< >`` if possible, else return ``None``.""" + match = _tpl_pattern.match(val) + if match: + return match.groups()[0] + return None + + +def _get_value(obj, key, default=missing): + """Slightly-modified version of marshmallow.utils.get_value. + If a dot-delimited ``key`` is passed and any attribute in the + path is `None`, return `None`. + """ + if "." in key: + return _get_value_for_keys(obj, key.split("."), default) + else: + return _get_value_for_key(obj, key, default) + + +def _get_value_for_keys(obj, keys, default): + if len(keys) == 1: + return _get_value_for_key(obj, keys[0], default) + else: + value = _get_value_for_key(obj, keys[0], default) + # XXX This differs from the marshmallow implementation + if value is None: + return None + return _get_value_for_keys(value, keys[1:], default) + + +def _get_value_for_key(obj, key, default): + if not hasattr(obj, "__getitem__"): + return getattr(obj, key, default) + + try: + return obj[key] + except (KeyError, IndexError, TypeError, AttributeError): + return getattr(obj, key, default) + + +class URLFor(Field): + """Field that outputs the URL for an endpoint. Acts identically to + Flask's ``url_for`` function, except that arguments can be pulled from the + object to be serialized. + Usage: :: + url = URLFor('author_get', id='') + https_url = URLFor('author_get', id='', _scheme='https', _external=True) + :param str endpoint: Flask endpoint name. + :param kwargs: Same keyword arguments as Flask's url_for, except string + arguments enclosed in `< >` will be interpreted as attributes to pull + from the object. + """ + + _CHECK_ATTRIBUTE = False + + def __init__(self, endpoint, **kwargs): + self.endpoint = endpoint + self.params = kwargs + Field.__init__(self, **kwargs) + + def _serialize(self, value, key, obj): + """Output the URL for the endpoint, given the kwargs passed to + ``__init__``. + """ + param_values = {} + for name, attr_tpl in self.params.items(): + attr_name = _tpl(str(attr_tpl)) + if attr_name: + attribute_value = _get_value(obj, attr_name, default=missing) + if attribute_value is None: + return None + if attribute_value is not missing: + param_values[name] = attribute_value + else: + raise AttributeError( + "{attr_name!r} is not a valid " + "attribute of {obj!r}".format(attr_name=attr_name, obj=obj) + ) + else: + param_values[name] = attr_tpl + return url_for(self.endpoint, **param_values) + + +UrlFor = URLFor + + +class AbsoluteURLFor(URLFor): + """Field that outputs the absolute URL for an endpoint.""" + + def __init__(self, endpoint, **kwargs): + kwargs["_external"] = True + URLFor.__init__(self, endpoint=endpoint, **kwargs) + + +AbsoluteUrlFor = AbsoluteURLFor + + +def _rapply(d, func, *args, **kwargs): + """Apply a function to all values in a dictionary or list of dictionaries, recursively.""" + if isinstance(d, (tuple, list)): + return [_rapply(each, func, *args, **kwargs) for each in d] + if isinstance(d, dict): + return {key: _rapply(value, func, *args, **kwargs) for key, value in d.items()} + else: + return func(d, *args, **kwargs) + + +def _url_val(val, key, obj, **kwargs): + """Function applied by `HyperlinksField` to get the correct value in the + schema. + """ + if isinstance(val, URLFor): + return val.serialize(key, obj, **kwargs) + else: + return val + + +class Hyperlinks(Field): + """Field that outputs a dictionary of hyperlinks, + given a dictionary schema with :class:`~flask_marshmallow.fields.URLFor` + objects as values. + Example: :: + _links = Hyperlinks({ + 'self': URLFor('author', id=''), + 'collection': URLFor('author_list'), + }) + `URLFor` objects can be nested within the dictionary. :: + _links = Hyperlinks({ + 'self': { + 'href': URLFor('book', id=''), + 'title': 'book detail' + } + }) + :param dict schema: A dict that maps names to + :class:`~fields.URLFor` fields. + """ + + _CHECK_ATTRIBUTE = False + + def __init__(self, schema, **kwargs): + self.schema = schema + Field.__init__(self, **kwargs) + + def _serialize(self, value, attr, obj): + return _rapply(self.schema, _url_val, key=attr, obj=obj) \ No newline at end of file diff --git a/openflexure_microscope/common/labthings/schema.py b/openflexure_microscope/common/labthings/schema.py index e69de29b..4c2f3c95 100644 --- a/openflexure_microscope/common/labthings/schema.py +++ b/openflexure_microscope/common/labthings/schema.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +import flask +import marshmallow + +_MARSHMALLOW_VERSION_INFO = tuple( + [int(part) for part in marshmallow.__version__.split(".") if part.isdigit()] +) + +sentinel = object() + + +class Schema(marshmallow.Schema): + """Base serializer with which to define custom serializers. + See `marshmallow.Schema` for more details about the `Schema` API. + """ + + def jsonify(self, obj, many=sentinel, *args, **kwargs): + """Return a JSON response containing the serialized data. + :param obj: Object to serialize. + :param bool many: Whether `obj` should be serialized as an instance + or as a collection. If unset, defaults to the value of the + `many` attribute on this Schema. + :param kwargs: Additional keyword arguments passed to `flask.jsonify`. + .. versionchanged:: 0.6.0 + Takes the same arguments as `marshmallow.Schema.dump`. Additional + keyword arguments are passed to `flask.jsonify`. + .. versionchanged:: 0.6.3 + The `many` argument for this method defaults to the value of + the `many` attribute on the Schema. Previously, the `many` + argument of this method defaulted to False, regardless of the + value of `Schema.many`. + """ + if many is sentinel: + many = self.many + if _MARSHMALLOW_VERSION_INFO[0] >= 3: + data = self.dump(obj, many=many) + else: + data = self.dump(obj, many=many).data + return flask.jsonify(data, *args, **kwargs) \ No newline at end of file