Commit 74f1d55b authored by Joel Collins's avatar Joel Collins
Browse files

Added capture resources to new system

parent 5efd58f5
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -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
+199 −0
Original line number Diff line number Diff line
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="<id>"),
                "mimetype": "application/json",
            },
            "tags": {
                "href": fields.AbsoluteUrlFor("CaptureTags", id="<id>"),
                "mimetype": "application/json",
            },
            "metadata": {
                "href": fields.AbsoluteUrlFor("CaptureMetadata", id="<id>"),
                "mimetype": "application/json",
            },
            "download": {
                "href": fields.AbsoluteUrlFor("CaptureDownload", id="<id>", filename="<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/<id>", endpoint="CaptureResource"
    )
    labthing.add_resource(
        CaptureDownload, f"{prefix}/captures/<id>/download/<filename>", endpoint="CaptureDownload"
    )
    labthing.add_resource(
        CaptureTags, f"{prefix}/captures/<id>/tags", endpoint="CaptureTags"
    )
    labthing.add_resource(
        CaptureMetadata, f"{prefix}/captures/<id>/metadata", endpoint="CaptureMetadata"
    )
+153 −0
Original line number Diff line number Diff line
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='<id>')
        https_url = URLFor('author_get', id='<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='<id>'),
            'collection': URLFor('author_list'),
        })
    `URLFor` objects can be nested within the dictionary. ::
        _links = Hyperlinks({
            'self': {
                'href': URLFor('book', id='<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
+39 −0
Original line number Diff line number Diff line
# -*- 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