Loading openflexure_microscope/api/app.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading openflexure_microscope/api/v2/views/captures.py 0 → 100644 +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" ) openflexure_microscope/common/labthings/fields.py +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 openflexure_microscope/common/labthings/schema.py +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 Loading
openflexure_microscope/api/app.py +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
openflexure_microscope/api/v2/views/captures.py 0 → 100644 +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" )
openflexure_microscope/common/labthings/fields.py +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
openflexure_microscope/common/labthings/schema.py +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