Added capture resources to new system
This commit is contained in:
parent
5efd58f561
commit
74f1d55bb2
4 changed files with 394 additions and 1 deletions
|
|
@ -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
openflexure_microscope/api/v2/views/captures.py
Normal file
199
openflexure_microscope/api/v2/views/captures.py
Normal 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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue