Added capture resources to new system

This commit is contained in:
Joel Collins 2019-12-18 11:55:51 +00:00
parent 5efd58f561
commit 74f1d55bb2
4 changed files with 394 additions and 1 deletions

View file

@ -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

View file

@ -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="<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"
)

View file

@ -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='<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)

View file

@ -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)