diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 528800d8..070a8739 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -16,16 +16,21 @@ from flask_cors import CORS 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.config import ( + settings_file_path, + JSONEncoder, + USER_PLUGINS_PATH, +) from openflexure_microscope.api import v2 from openflexure_microscope.common.labthings.labthing import LabThing from openflexure_microscope.common.labthings.find import registered_plugins from openflexure_microscope.common.labthings.plugins import find_plugins -from openflexure_microscope.config import USER_PLUGINS_PATH from openflexure_microscope.api.microscope import default_microscope as api_microscope +from openflexure_microscope.api.v2 import views + # Handle logging is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") @@ -78,33 +83,53 @@ CORS(app, resources=r"*") # Make errors more API friendly handler = JSONExceptionHandler(app) -# Attach lab devices +# Build a labthing labthing = LabThing(app, prefix="/api/v2") labthing.description = "Test LabThing-based API for OpenFlexure Microscope" +# Attach lab devices labthing.register_device(api_microscope, "openflexure_microscope") +# Attach plugins for _plugin in find_plugins(USER_PLUGINS_PATH): labthing.register_plugin(_plugin) -from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing +# Attach captures resources +labthing.add_resource(views.CaptureList, f"/captures", endpoint="CaptureList") +labthing.register_property(views.CaptureList) +labthing.add_resource( + views.CaptureResource, f"/captures/", endpoint="CaptureResource" +) +labthing.add_resource( + views.CaptureDownload, + f"/captures//download/", + endpoint="CaptureDownload", +) +labthing.add_resource(views.CaptureTags, f"/captures//tags", endpoint="CaptureTags") +labthing.add_resource( + views.CaptureMetadata, f"/captures//metadata", endpoint="CaptureMetadata" +) -add_captures_to_labthing(labthing, prefix="") -from openflexure_microscope.api.v2.views.state import add_states_to_labthing +# Attach settings and status resources +labthing.add_resource(views.SettingsProperty, f"/settings") +labthing.register_property(views.SettingsProperty) +labthing.add_resource(views.NestedSettingsProperty, "/settings/") +labthing.add_resource(views.StatusProperty, "/status") +labthing.register_property(views.StatusProperty) +labthing.add_resource(views.NestedStatusProperty, "/status/") -add_states_to_labthing(labthing, prefix="") +# Attach streams resources +labthing.add_resource(views.MjpegStream, f"/streams/mjpeg") +labthing.register_property(views.MjpegStream) +labthing.add_resource(views.SnapshotStream, f"/streams/snapshot") +labthing.register_property(views.SnapshotStream) -from openflexure_microscope.api.v2.views.tasks import add_tasks_to_labthing - -add_tasks_to_labthing(labthing, prefix="") - -from openflexure_microscope.api.v2.views.streams import add_streams_to_labthing - -add_streams_to_labthing(labthing, prefix="") - -from openflexure_microscope.api.v2.views.actions import add_actions_to_labthing - -add_actions_to_labthing(labthing) +# Attach microscope action resources +for name, action in views.enabled_root_actions().items(): + view_class = action["view_class"] + rule = action["rule"] + labthing.add_resource(view_class, "/actions{rule}") + labthing.register_action(view_class) @app.route("/routes") diff --git a/openflexure_microscope/api/v2/__init__.py b/openflexure_microscope/api/v2/__init__.py index 3f07e575..e69de29b 100644 --- a/openflexure_microscope/api/v2/__init__.py +++ b/openflexure_microscope/api/v2/__init__.py @@ -1 +0,0 @@ -from . import blueprints diff --git a/openflexure_microscope/api/v2/blueprints/__init__.py b/openflexure_microscope/api/v2/blueprints/__init__.py deleted file mode 100644 index f4716594..00000000 --- a/openflexure_microscope/api/v2/blueprints/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import root, captures, settings, status, tasks, streams, actions diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py deleted file mode 100644 index e57c2c04..00000000 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Top-level representation of enabled actions -""" - -from flask import Blueprint, url_for, jsonify - -from openflexure_microscope.api.utilities import blueprint_for_module -from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.views import MicroscopeView - -from . import camera, stage, system - - -class ActionsAPI(MicroscopeView): - def get(self): - return jsonify(actions_representation()) - - -_actions = { - "capture": { - "rule": "/camera/capture/", - "view_class": camera.CaptureAPI, - "conditions": True, - }, - "previewStart": { - "rule": "/camera/preview/start", - "view_class": camera.GPUPreviewStartAPI, - "conditions": True, - }, - "previewStop": { - "rule": "/camera/preview/stop", - "view_class": camera.GPUPreviewStopAPI, - "conditions": True, - }, - "move": { - "rule": "/stage/move/", - "view_class": stage.MoveStageAPI, - "conditions": True, - }, - "zeroStage": { - "rule": "/stage/zero/", - "view_class": stage.ZeroStageAPI, - "conditions": True, - }, - "shutdown": { - "rule": "/system/shutdown/", - "view_class": system.ShutdownAPI, - "conditions": system.is_raspberrypi(), - }, - "reboot": { - "rule": "/system/reboot/", - "view_class": system.RebootAPI, - "conditions": system.is_raspberrypi(), - }, -} - - -def enabled_actions(): - global _actions - return {k: v for k, v in _actions.items() if v["conditions"]} - - -def actions_representation(): - global _actions - - actions = {} - for name, action in enabled_actions().items(): - d = { - "links": {"self": url_for(f".{name}")}, - "rule": action["rule"], - "view_class": str(action["view_class"]), - } - - d.update(description_from_view(action["view_class"])) - - actions[name] = d - - return actions - - -def construct_blueprint(microscope_obj): - global _actions - - blueprint = blueprint_for_module(__name__) - - # For each enabled action route defined in our dictionary above - for name, action in enabled_actions().items(): - # Add the action to our blueprint - blueprint.add_url_rule( - action["rule"], - view_func=action["view_class"].as_view(name, microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=ActionsAPI.as_view("actions", microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py deleted file mode 100644 index 3b899102..00000000 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ /dev/null @@ -1,172 +0,0 @@ -from openflexure_microscope.api.utilities import get_bool, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.utilities import filter_dict - -import logging -from flask import jsonify, request, abort, url_for, redirect, send_file - - -class CaptureAPI(MicroscopeView): - """ - Create a new image capture. - """ - - def post(self): - """ - Create a new image capture. - - .. :quickref: Actions; New capture - - **Example request**: - - .. sourcecode:: http - - POST /actions/camera/capture HTTP/1.1 - Accept: application/json - - { - "filename": "myfirstcapture", - "temporary": false, - "use_video_port": true, - "bayer": true, - "size": { - "width": 640, - "height": 480 - } - } - - :>header Accept: application/json - - :json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean temporary: delete the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :
header Accept: application/json - - :
header Accept: application/json - - :
header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>jsonarr boolean available: availability of capture data - :>jsonarr string filename: filename of capture - :>jsonarr string id: unique id of the capture object - :>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing - :>jsonarr boolean locked: file locked for modifications (mostly used for video recording) - :>jsonarr string path: path on pi storage to the capture file, if available - :>jsonarr boolean stream: capture stored in-memory as a BytesIO stream - :>jsonarr json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - :>header Content-Type: application/json - :status 200: capture found - :status 404: no capture found with that id - """ - representation = captures_representation(self.microscope.camera.images) - - return jsonify(representation) - - def delete(self): - """ - Delete all captures - - .. :quickref: Captures; Delete all captures - """ - for image in self.microscope.camera.images: - image.delete() - - captures = [image.state for image in self.microscope.camera.images] - - return jsonify(captures) - - -class CaptureAPI(MicroscopeView): - def get(self, capture_id): - """ - Get JSON representation of a capture - - .. :quickref: Capture; Get capture - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "available": true, - "bytestream": false, - "keep_on_disk": false, - "locked": false, - "metadata": { - "filename": "2018-11-16_10-21-53.jpeg", - "id": "d0b2067abbb946f19351e075c5e7cd5b", - "path": "capture/2018-11-16_10-21-53.jpeg", - "time": "2018-11-16_10-21-53" - } - "uri": { - "download": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/download", - "state": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/" - } - } - - :>json boolean available: availability of capture data - :>json string filename: filename of capture - :>json string id: unique id of the capture object - :>json boolean keep_on_disk: keep the capture file on microscope after closing - :>json boolean locked: file locked for modifications (mostly used for video recording) - :>json string path: path on pi storage to the capture file, if available - :>json boolean stream: capture stored in-memory as a BytesIO stream - :>json json uri: - **download** *(string)*: api uri to the capture file download - - **state** *(string)*: api uri to the capture json representation - - """ - - try: - representation = captures_representation(self.microscope.camera.images)[ - capture_id - ] - except KeyError: - return abort(404) # 404 Not Found - - return jsonify(representation) - - def delete(self, capture_id): - """ - Delete all capture data from the Pi, even if `keep_on_disk=true;`. - - .. :quickref: Capture; Delete capture. - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj: - return abort(404) # 404 Not Found - - capture_obj.delete() - - return jsonify({"return": capture_id}) - - def put(self, capture_id): - """ - Add arbitrary metadata to the capture - - .. :quickref: Capture; Update capture metadata - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1 - Accept: application/json - - { - "user_id": "ofm_1234", - "patient_number": 1452, - } - - :>header Accept: application/json - - :
header Accept: image/jpeg - :query thumbnail: return an image thumbnail e.g. ?thumbnail=true - - :>header Content-Type: image/jpeg - :status 200: capture data found - :status 404: no capture found with that id - - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - thumbnail = get_bool(request.args.get("thumbnail")) - - return redirect( - url_for( - ".capture_download", - capture_id=capture_id, - filename=capture_obj.filename, - thumbnail=thumbnail, - ), - code=307, - ) - - -class DownloadAPI(MicroscopeView): - def get(self, capture_id, filename): - - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - 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( - "capture_download", - capture_id=capture_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 TagsAPI(MicroscopeView): - def get(self, capture_id): - """ - Return tag list for a capture. - - .. :quickref: Capture; Show capture tags - - **Example request**: - - .. sourcecode:: http - - GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: text/json - - :>header Accept: text/json - - :>header Content-Type: text/json - :status 200: capture data found - :status 404: no capture found with that id - """ - capture_obj = self.microscope.camera.image_from_id(capture_id) - - if not capture_obj or not capture_obj.state["available"]: - return abort(404) # 404 Not Found - - return jsonify(capture_obj.tags) - - def put(self, capture_id): - """ - Add tags to the capture - - .. :quickref: Capture; Update capture tags - - **Example request**: - - .. sourcecode:: http - - PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1 - Accept: application/json - - ["tests", "mytag", "someothertag"] - - :>header Accept: application/json - - :
header Accept: application/json - - :
/tags", - view_func=TagsAPI.as_view("capture_tags", microscope=microscope_obj), - ) - - # Capture routes - blueprint.add_url_rule( - "//download/", - view_func=DownloadAPI.as_view("capture_download", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "//download", - view_func=DownloadRedirectAPI.as_view( - "capture_download_redirect", microscope=microscope_obj - ), - ) - - blueprint.add_url_rule( - "//", - view_func=CaptureAPI.as_view("capture", microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=ListAPI.as_view("captures", microscope=microscope_obj) - ) - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py deleted file mode 100644 index 0e8d148e..00000000 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ /dev/null @@ -1,51 +0,0 @@ -from flask import Blueprint, jsonify, url_for -from openflexure_microscope import Microscope -from openflexure_microscope.api.views import MicroscopeView - -from openflexure_microscope.utilities import get_docstring, bottom_level_name -from openflexure_microscope.api.utilities import blueprint_name_for_module -from openflexure_microscope.api.v2.blueprints import ( - settings, - status, - captures, - actions, - streams, -) - -# List of submodules containing create_blueprint methods using standard blueprint_for_module naming -_root_blueprint_modules = [settings, status, captures, actions, streams] - - -def root_representation(): - """ - Generate a dictionar representation of all top-level blueprint rules - """ - global _root_blueprint_modules - d = {} - - for blueprint_module in _root_blueprint_modules: - module_short_name = bottom_level_name(blueprint_module) - blueprint_name = blueprint_name_for_module(blueprint_module.__name__) - - d[module_short_name] = { - "name": blueprint_module.__name__, - "description": get_docstring(blueprint_module), - "links": {"self": url_for(f"{blueprint_name}.{module_short_name}")}, - } - - return d - - -class RootAPI(MicroscopeView): - def get(self): - - return jsonify(root_representation()) - - -def construct_blueprint(microscope_obj): - blueprint = Blueprint("root_blueprint", __name__) - - blueprint.add_url_rule( - "/", view_func=RootAPI.as_view("root_repr", microscope=microscope_obj) - ) - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/settings.py b/openflexure_microscope/api/v2/blueprints/settings.py deleted file mode 100644 index e56e5b36..00000000 --- a/openflexure_microscope/api/v2/blueprints/settings.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Writeable settings for the microscope, and attached hardware -""" - -from openflexure_microscope.api.utilities import gen, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import blueprint_for_module - -from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path - -from flask import Blueprint, jsonify, request, abort -import logging - - -class SettingsAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope settings. - - .. :quickref: Settings; Get microscope settings - - **Example request**: - - .. sourcecode:: http - - GET /settings/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.read_settings()) - - def put(self): - """ - Modify microscope configuration - - .. :quickref: Settings; Update microscope settings - - **Example request**: - - .. sourcecode:: http - - PUT /config HTTP/1.1 - Accept: application/json - - { - "id": "0e2c6fac5421429aac67c7903107bdd8", - "name": "docuscope-2000", - "fov": [4100, 3146], - "camera_settings": { - "image_resolution": [2592, 1944], - "numpy_resolution": [1312, 976], - "video_resolution": [832, 624], - "jpeg_quality": 75, - "picamera_settings": { - "analog_gain": 1.0, - "digital_gain": 1.0, - "awb_gains": [0.92578125, 2.94921875], - "awb_mode": "off", - "exposure_mode": "off", - "framerate": 24.0, - "saturation": 0, - "shutter_speed": 5378 - }, - }, - "stage_settings": { - "backlash": { - "x": 256, - "y": 256, - "z": 0 - } - } - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" - ] - } - - :>header Accept: application/json - - :", - view_func=NestedSettingsAPI.as_view( - "nested_settings", microscope=microscope_obj - ), - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/status.py b/openflexure_microscope/api/v2/blueprints/status.py deleted file mode 100644 index 846a594f..00000000 --- a/openflexure_microscope/api/v2/blueprints/status.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -Read-only status of the microscope, and attached hardware info -""" - -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import blueprint_for_module -from openflexure_microscope.utilities import get_by_path - -from flask import Blueprint, jsonify, abort - - -class StatusAPI(MicroscopeView): - def get(self): - """ - JSON representation of the microscope status (read-only properties, modifiable with actions) - - .. :quickref: Status; Microscope status - - **Example request**: - - .. sourcecode:: http - - GET /status/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "camera": { - "preview_active": false, - "record_active": false, - "stream_active": true - }, - "plugin": {}, - "stage": { - "backlash": { - "x": 128, - "y": 128, - "z": 128 - }, - "position": { - "x": -8080, - "y": 5665, - "z": -12600 - } - } - } - - :>header Accept: application/json - :>header Content-Type: application/json - :status 200: state available - """ - return jsonify(self.microscope.status) - - -class NestedStatusAPI(MicroscopeView): - def get(self, route): - - keys = route.split("/") - - try: - value = get_by_path(self.microscope.status, keys) - except KeyError: - return abort(404) - - return jsonify(value) - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - blueprint.add_url_rule( - "/", view_func=StatusAPI.as_view("status", microscope=microscope_obj) - ) - - blueprint.add_url_rule( - "/", - view_func=NestedStatusAPI.as_view("nested_status", microscope=microscope_obj), - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/streams.py b/openflexure_microscope/api/v2/blueprints/streams.py deleted file mode 100644 index bc193d9a..00000000 --- a/openflexure_microscope/api/v2/blueprints/streams.py +++ /dev/null @@ -1,107 +0,0 @@ -""" -Top-level description of routes related to live camera stream data -""" - -from openflexure_microscope.api.utilities import gen, JsonResponse -from openflexure_microscope.api.views import MicroscopeView -from openflexure_microscope.api.utilities import ( - blueprint_for_module, - blueprint_name_for_module, -) -from openflexure_microscope.utilities import description_from_view - -from flask import Response, Blueprint, jsonify, request, url_for - - -class StreamAPI(MicroscopeView): - def get(self): - return jsonify(streams_representation()) - - -class MjpegAPI(MicroscopeView): - """ - Real-time MJPEG stream from the microscope camera - """ - - def get(self): - """ - Real-time MJPEG stream from the microscope camera - - .. :quickref: Streams; Camera MJPEG stream - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response( - gen(self.microscope.camera), - mimetype="multipart/x-mixed-replace; boundary=frame", - ) - - -class SnapshotAPI(MicroscopeView): - """ - Single JPEG snapshot from the camera stream - """ - - def get(self): - """ - Single snapshot from the camera stream - - .. :quickref: Streams; Camera snapshot - - :>header Accept: image/jpeg - :>header Content-Type: image/jpeg - :status 200: stream active - """ - # Restart stream worker thread - self.microscope.camera.start_worker() - - return Response(self.microscope.camera.get_frame(), mimetype="image/jpeg") - - -_streams = { - "mjpeg": {"rule": "/mjpeg", "view_class": MjpegAPI, "conditions": True}, - "snapshot": {"rule": "/snapshot", "view_class": SnapshotAPI, "conditions": True}, -} - - -def enabled_streams(): - global _streams - return {k: v for k, v in _streams.items() if v["conditions"]} - - -def streams_representation(): - global _streams - - streams = {} - for name, stream in enabled_streams().items(): - d = {"links": {"self": url_for(f".{name}")}} - - d.update(description_from_view(stream["view_class"])) - - streams[name] = d - - return streams - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - # For each enabled stream route defined in our dictionary above - for name, stream in enabled_streams().items(): - # Add the action to our blueprint - blueprint.add_url_rule( - stream["rule"], - view_func=stream["view_class"].as_view(name, microscope=microscope_obj), - ) - - blueprint.add_url_rule( - "/", view_func=StreamAPI.as_view("streams", microscope=microscope_obj) - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/tasks.py b/openflexure_microscope/api/v2/blueprints/tasks.py deleted file mode 100644 index cf841bb2..00000000 --- a/openflexure_microscope/api/v2/blueprints/tasks.py +++ /dev/null @@ -1,158 +0,0 @@ -from openflexure_microscope.api.views import MethodView -from flask import jsonify, abort, Blueprint, url_for - -from openflexure_microscope.common import tasks - - -def tasks_representation(): - """ - Generate a dictionary representation of all tasks, including Flask route URLs - - Returns: - dict: Dictionary representation of all tasks - """ - tasks_dict = tasks.states() - - for task_key, task_repr in tasks_dict.items(): - # Add API routes to returned representations - extra_state = { - "links": {"self": "{}".format(url_for(".task", task_id=task_key))} - } - - tasks_dict[task_key].update(extra_state) - - return tasks_dict - - -class TaskListAPI(MethodView): - def get(self): - """ - Get list of long-running tasks. - - .. :quickref: Tasks; Get collection of tasks - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - [ - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - }, - { - "end_time": null, - "id": "df46558cc8844924821bd0181881871e", - "return": null, - "start_time": "2019-01-23 16-34-54", - "status": "running" - } - ] - - :>header Accept: application/json - :query include_unavailable: return json representations of captures that have been completely deleted - - :>header Content-Type: application/json - """ - - return jsonify(tasks_representation()) - - def delete(self): - """ - Clean list of long-running tasks (running tasks persist). - - .. :quickref: Tasks; Clean collection of tasks - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - tasks.cleanup_tasks() - - return jsonify(tasks_representation()) - - -class TaskAPI(MethodView): - def get(self, task_id): - """ - Get JSON representation of a task - - .. :quickref: Tasks; Get task - - **Example request**: - - .. sourcecode:: http - - GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1 - Accept: application/json - - **Example response**: - - .. sourcecode:: http - - HTTP/1.1 200 OK - Vary: Accept - Content-Type: application/json - - { - "end_time": "2019-01-23 16-33-33", - "id": "db13a66787e1419bb06b1504e4d80b0c", - "return": [ - 0.848622546386467, - 0.6106785018091292, - ], - "start_time": "2019-01-23 16-33-13", - "status": "success" - } - - """ - - try: - task = tasks_representation()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - # Return task state - return jsonify(task) - - def delete(self, task_id): - """ - Terminate a particular task. - - .. :quickref: Tasks; Terminate a task - - :>header Accept: application/json - - :>header Content-Type: application/json - """ - - try: - task = tasks.tasks()[task_id] - except KeyError: - return abort(404) # 404 Not Found - - task.terminate() - - return jsonify(task.state) - - -def construct_blueprint(microscope_obj): - - blueprint = Blueprint("v2_tasks_blueprint", __name__) - - blueprint.add_url_rule("/", view_func=TaskListAPI.as_view("task_list")) - - blueprint.add_url_rule("//", view_func=TaskAPI.as_view("task")) - - return blueprint diff --git a/openflexure_microscope/api/v2/views/__init__.py b/openflexure_microscope/api/v2/views/__init__.py new file mode 100644 index 00000000..473c368c --- /dev/null +++ b/openflexure_microscope/api/v2/views/__init__.py @@ -0,0 +1,5 @@ +from .actions import enabled_root_actions +from .captures import * +from .state import * +from .streams import * +from .tasks import * diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py index 897d67c6..878c0c4e 100644 --- a/openflexure_microscope/api/v2/views/actions/__init__.py +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -49,17 +49,6 @@ _actions = { } -def enabled_actions(): +def enabled_root_actions(): global _actions return {k: v for k, v in _actions.items() if v["conditions"]} - - -def add_actions_to_labthing(labthing, prefix=""): - """ - Add all capture resources to a labthing - """ - for name, action in enabled_actions().items(): - view_class = action["view_class"] - rule = action["rule"] - labthing.add_resource(view_class, f"{prefix}/actions{rule}") - labthing.register_action(view_class) diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py index c2dae44a..e755289a 100644 --- a/openflexure_microscope/api/v2/views/state.py +++ b/openflexure_microscope/api/v2/views/state.py @@ -70,25 +70,3 @@ class NestedStatusProperty(Resource): return jsonify(value) - -def add_states_to_labthing(labthing, prefix=""): - """ - Add all settings and status resources to a labthing - """ - labthing.add_resource( - SettingsProperty, f"{prefix}/settings", endpoint="SettingsProperty" - ) - labthing.register_property(SettingsProperty) - labthing.add_resource( - NestedSettingsProperty, - f"{prefix}/settings/", - endpoint="NestedSettingsProperty", - ) - labthing.add_resource(StatusProperty, f"{prefix}/status", endpoint="StatusProperty") - labthing.register_property(StatusProperty) - labthing.add_resource( - NestedStatusProperty, - f"{prefix}/status/", - endpoint="NestedStatusProperty", - ) - diff --git a/openflexure_microscope/api/v2/views/tasks.py b/openflexure_microscope/api/v2/views/tasks.py index 67a7e3da..6a7e39d3 100644 --- a/openflexure_microscope/api/v2/views/tasks.py +++ b/openflexure_microscope/api/v2/views/tasks.py @@ -59,13 +59,3 @@ class TaskResource(Resource): task.terminate() return jsonify(task.state) - - -def add_tasks_to_labthing(labthing, prefix=""): - """ - Add all settings and status resources to a labthing - """ - labthing.add_resource(TaskList, f"{prefix}/tasks", endpoint="TasksProperty") - labthing.register_property(TaskList) - labthing.add_resource(TaskResource, f"{prefix}/tasks/", endpoint="TaskResource") - diff --git a/openflexure_microscope/common/labthings/labthing.py b/openflexure_microscope/common/labthings/labthing.py index b65a848e..0f23f258 100644 --- a/openflexure_microscope/common/labthings/labthing.py +++ b/openflexure_microscope/common/labthings/labthing.py @@ -2,8 +2,7 @@ from flask import current_app, _app_ctx_stack, request, url_for, jsonify from .plugins import BasePlugin from .views.plugins import PluginListResource - -from .resource import Resource +from .views.tasks import TaskList, TaskResource from ..utilities import get_docstring @@ -53,6 +52,11 @@ class LabThing(object): self.app.add_url_rule(self._complete_url("/", ""), "td", self.td) # Add plugin overview self.add_resource(PluginListResource, "/plugins") + self.register_property(PluginListResource) + # Add task routes + self.add_resource(TaskList, "/tasks", endpoint="TasksProperty") + self.register_property(TaskList) + self.add_resource(TaskResource, "/tasks/", endpoint="TaskResource") ### Device stuff @@ -209,6 +213,7 @@ class LabThing(object): actions[key]["links"] = [{"href": self.url_for(prop, _external=True)}] td = { + "id": url_for("td", _external=True), "title": self.title, "description": self.description, "properties": props, diff --git a/openflexure_microscope/common/labthings/views/plugins.py b/openflexure_microscope/common/labthings/views/plugins.py index dabc1180..fa160b53 100644 --- a/openflexure_microscope/common/labthings/views/plugins.py +++ b/openflexure_microscope/common/labthings/views/plugins.py @@ -39,7 +39,7 @@ def plugins_representation(plugin_dict): for view_id, view_data in plugin.views.items(): logging.debug(f"Representing view {view_id}") - uri = url_for(f"pluginlistresource") + "/" + view_data["rule"][1:] + uri = url_for(f"PluginListResource") + "/" + view_data["rule"][1:] # uri = view_data["rule"] # Make links dictionary if it doesn't yet exist view_d = {"links": {"self": uri}} diff --git a/openflexure_microscope/common/labthings/views/tasks.py b/openflexure_microscope/common/labthings/views/tasks.py new file mode 100644 index 00000000..afcba39b --- /dev/null +++ b/openflexure_microscope/common/labthings/views/tasks.py @@ -0,0 +1,61 @@ +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 import tasks + + +class TaskSchema(Schema): + _ID = fields.String(data_key="id") + target_string = fields.String(data_key="function") + _status = fields.String(data_key="status") + progress = fields.String() + data = fields.Raw() + _return_value = fields.Raw(data_key="return") + _start_time = fields.String(data_key="start_time") + _end_time = fields.String(data_key="end_time") + + # TODO: Add HTTP methods + links = fields.Hyperlinks( + { + "self": { + "href": fields.AbsoluteUrlFor("TaskResource", id=""), + "mimetype": "application/json", + } + } + ) + + +task_schema = TaskSchema() +task_list_schema = TaskSchema(many=True) + + +class TaskList(Resource): + def get(self): + return task_list_schema.jsonify(tasks.tasks()) + + +class TaskResource(Resource): + def get(self, id): + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + # Return task state + return task_schema.jsonify(task) + + def delete(self, id): + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + task.terminate() + + return task_schema.jsonify(task) diff --git a/openflexure_microscope/plugins/v2/autofocus.py b/openflexure_microscope/plugins/v2/autofocus.py index 6271c99d..71c3c938 100644 --- a/openflexure_microscope/plugins/v2/autofocus.py +++ b/openflexure_microscope/plugins/v2/autofocus.py @@ -360,6 +360,9 @@ autofocus_plugin_v2.add_method(fast_autofocus, "fast_autofocus") autofocus_plugin_v2.add_method(autofocus, "autofocus") autofocus_plugin_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness") -autofocus_plugin_v2.add_view(AutofocusAPI, "/autofocus") -autofocus_plugin_v2.add_view(FastAutofocusAPI, "/fast_autofocus") +autofocus_plugin_v2.add_view(AutofocusAPI, "/autofocus") +autofocus_plugin_v2.register_action(AutofocusAPI) + +autofocus_plugin_v2.add_view(FastAutofocusAPI, "/fast_autofocus") +autofocus_plugin_v2.register_action(FastAutofocusAPI)