From 6581612312c17de3ebb2a1c2345773d35fc1d687 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Thu, 14 Nov 2019 16:33:00 +0000 Subject: [PATCH 01/27] Implemented root API v2 routes --- openflexure_microscope/api/app.py | 28 +- .../api/v1/blueprints/base.py | 8 +- .../api/v1/blueprints/camera/capture.py | 11 +- .../api/v1/blueprints/camera/function.py | 4 +- .../api/v1/blueprints/plugins.py | 14 +- openflexure_microscope/api/v2/__init__.py | 1 + .../api/v2/blueprints/__init__.py | 1 + .../api/v2/blueprints/captures.py | 422 ++++++++++++++++++ .../api/v2/blueprints/plugins.py | 151 +++++++ .../api/v2/blueprints/settings.py | 153 +++++++ .../api/v2/blueprints/status.py | 64 +++ .../api/v2/blueprints/stream.py | 57 +++ .../api/v2/blueprints/tasks.py | 160 +++++++ openflexure_microscope/camera/base.py | 22 +- openflexure_microscope/camera/capture.py | 18 +- openflexure_microscope/camera/mock.py | 10 +- openflexure_microscope/camera/pi.py | 44 +- openflexure_microscope/config.py | 6 +- openflexure_microscope/microscope.py | 73 +-- openflexure_microscope/plugins/__init__.py | 4 +- .../default/camera_calibration/plugin.py | 6 +- .../plugins/default/scan/plugin.py | 21 +- openflexure_microscope/plugins/loader.py | 35 +- openflexure_microscope/stage/base.py | 8 +- openflexure_microscope/stage/mock.py | 12 +- openflexure_microscope/stage/sanga.py | 12 +- tests/test_plugins.py | 8 +- 27 files changed, 1205 insertions(+), 148 deletions(-) create mode 100644 openflexure_microscope/api/v2/blueprints/captures.py create mode 100644 openflexure_microscope/api/v2/blueprints/plugins.py create mode 100644 openflexure_microscope/api/v2/blueprints/settings.py create mode 100644 openflexure_microscope/api/v2/blueprints/status.py create mode 100644 openflexure_microscope/api/v2/blueprints/stream.py create mode 100644 openflexure_microscope/api/v2/blueprints/tasks.py diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 0d5e342e..8d9c8ec1 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -21,6 +21,7 @@ from openflexure_microscope import Microscope from openflexure_microscope.camera.capture import build_captures_from_exif from openflexure_microscope.config import settings_file_path, JSONEncoder from openflexure_microscope.api.v1 import blueprints +from openflexure_microscope.api import v2 # Import device modules # NB this will eventually be handled by the RC file, so you can choose what device @@ -141,6 +142,31 @@ app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1")) task_blueprint = blueprints.task.construct_blueprint(api_microscope) app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) +### V2 +# Tasks routes +v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope) +app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2")) + +# Captures routes +v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope) +app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2")) + +# Settings routes +v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope) +app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2")) + +# Status routes +v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope) +app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2")) + +# Plugins routes +v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope) +app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2")) + +# Tasks routes +v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope) +app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2")) + @app.route("/routes") def routes(): @@ -184,7 +210,7 @@ def cleanup(): # Save config logging.debug("Saving config for teardown...") - api_microscope.save_config(backup=True) + api_microscope.save_settings() logging.debug("Settling...") time.sleep(0.5) diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py index 35c6ad89..cf3b014b 100644 --- a/openflexure_microscope/api/v1/blueprints/base.py +++ b/openflexure_microscope/api/v1/blueprints/base.py @@ -152,7 +152,7 @@ class ConfigAPI(MicroscopeView): :>header Content-Type: application/json :status 200: state available """ - return jsonify(self.microscope.read_config(json_safe=True)) + return jsonify(self.microscope.read_settings(json_safe=True)) def post(self): """ @@ -223,10 +223,10 @@ class ConfigAPI(MicroscopeView): logging.debug("Updating settings from POST request:") logging.debug(payload.json) - self.microscope.apply_config(payload.json) - self.microscope.save_config() + self.microscope.apply_settings(payload.json) + self.microscope.save_settings() - return jsonify(self.microscope.read_config(json_safe=True)) + return jsonify(self.microscope.read_settings(json_safe=True)) def construct_blueprint(microscope_obj): diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 615ea009..8afe5443 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -130,16 +130,19 @@ class ListAPI(MicroscopeView): output.file, use_video_port=use_video_port, resize=resize, bayer=bayer ) - metadata.update( - { - "microscope_settings": self.microscope.read_config(), + # Inject system metadata + system_metadata = { + "microscope_settings": self.microscope.read_settings(), "microscope_state": self.microscope.state, "microscope_id": self.microscope.id, "microscope_name": self.microscope.name, } - ) + output.system_metadata.update(system_metadata) + # Insert custom metadata output.put_metadata(metadata) + + # Insert custom tags output.put_tags(tags) return jsonify(output.state) diff --git a/openflexure_microscope/api/v1/blueprints/camera/function.py b/openflexure_microscope/api/v1/blueprints/camera/function.py index f9e8f35f..fb365483 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/function.py +++ b/openflexure_microscope/api/v1/blueprints/camera/function.py @@ -16,7 +16,7 @@ class ZoomAPI(MicroscopeView): :
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 + """ + include_unavailable = get_bool(request.args.get("include_unavailable")) + + representation = captures_representation(self.microscope.camera.images, include_unavailable=include_unavailable) + + 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 + + """ + capture_obj = self.microscope.camera.image_from_id(capture_id) + + if not capture_obj: + return abort(404) # 404 Not Found + + # Get capture state + capture_metadata = capture_obj.state + + # Add API routes to returned state + uri_dict = { + "uri": { + "state": "{}".format(url_for(".capture", capture_id=capture_obj.id)) + } + } + + # If available, also add download link + if capture_metadata["available"]: + uri_dict["uri"]["download"] = "{}download/{}".format( + url_for(".capture", capture_id=capture_obj.id), capture_obj.filename + ) + + capture_metadata.update(uri_dict) + + return jsonify(capture_metadata) + + 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("capture_list", microscope=microscope_obj), + ) + return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py new file mode 100644 index 00000000..9d9a6868 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -0,0 +1,151 @@ +from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin +from openflexure_microscope.api.views import MicroscopeViewPlugin + +from flask import Blueprint, jsonify +from openflexure_microscope.api.views import MicroscopeView + +import copy +import logging +import warnings + + +def plugins_representation(plugin_loader_object: PluginLoader): + """ + Generate a dictionary representation of all plugins, including Flask route URLs + + Args: + plugin_loader_object (:py:class:`openflexure_microscope.plugins.PluginLoader`): Microscope plugin loader + + Returns: + dict: Dictionary representation of all plugins + """ + plugins = [] + for plugin in plugin_loader_object.active: + d = { + "name": plugin["name"], + "plugin": str(plugin["plugin"]), + "routes": plugin["routes"], + "form": plugin["form"] + } + plugins.append(d) + + return plugins + + +class PluginFormAPI(MicroscopeView): + def get(self): + """ + Return the current plugin forms + + .. :quickref: Plugin; Get forms + + Returns an array of present plugin forms (describing plugin user interfaces.) + Please note, this is *not* a list of all enabled plugins, only those with associated + user interface forms. + + A complete list of enabled plugins can be found in the microscope state. + + """ + return jsonify(plugins_representation(self.microscope.plugins)) + + +def construct_blueprint(microscope_obj): + + blueprint = Blueprint("plugins_blueprint", __name__) + + # Create a base route to return plugin API forms, if any exist + blueprint.add_url_rule( + "/", + view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj), + ) + + all_routes = [] + + # For each plugin attached to the microscope object + for plugin_representation in microscope_obj.plugins.active: + + plugin_obj = plugin_representation["plugin"] + plugin_name = plugin_representation["name"] + + # If plugin contains valid endpoints + if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): + + # We'll keep a record of how each route was expanded + expanded_routes = {} + + # For each defined endpoint + for view_route, view_class in plugin_obj.api_views.items(): + + # Remove all leading slashes from view route + cleaned_route = view_route + while cleaned_route[0] == "/": + cleaned_route = cleaned_route[1:] + + # Construct a full view route from the plugin name + full_view_route = "/{}/{}".format(plugin_name, cleaned_route) + logging.debug(full_view_route) + + # Record how the view_route got expanded + expanded_routes[view_route] = full_view_route + + # Check if endpoint name clashes + if full_view_route not in all_routes and issubclass( + view_class, MicroscopeViewPlugin + ): + # Add route to main route dictionary + all_routes.append(full_view_route) + + # Create a Python-safe name for the route + plugin_route_id = "plugin{}".format(full_view_route).replace( + "/", "_" + ) + + # Add route to the plugins blueprint + blueprint.add_url_rule( + full_view_route, + view_func=view_class.as_view( + plugin_route_id, + microscope=microscope_obj, + plugin=plugin_obj, + ), + ) + + # Add route to the plugin representation dictionary + plugin_representation["routes"].append(full_view_route) + + else: + warnings.warn( + "An endpoint /{} has already been loaded. Skipping {}.".format( + full_view_route, view_class + ) + ) + + # If plugin includes an API form + if hasattr(plugin_obj, "api_form") and isinstance( + plugin_obj.api_form, dict + ): + # TODO: We deep copy this to avoid clashing between API versions. Can be removed when v1 is removed. + api_form_info = copy.deepcopy(plugin_obj.api_form) + api_form_info["id"] = plugin_name + if "forms" in api_form_info and isinstance( + api_form_info["forms"], list + ): + for form in api_form_info["forms"]: + if "route" in form and form["route"] in expanded_routes.keys(): + form["route"] = expanded_routes[form["route"]] + else: + logging.warn( + "No valid expandable route found for {}".format( + form["route"] + ) + ) + + # Store the complete form in Microscope().plugin.form + plugin_representation["form"] = api_form_info + print(microscope_obj.plugins.forms) + + else: + warnings.warn( + "No valid 'api_views' dictionary found in {}".format(plugin_obj) + ) + return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/settings.py b/openflexure_microscope/api/v2/blueprints/settings.py new file mode 100644 index 00000000..972785d3 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/settings.py @@ -0,0 +1,153 @@ +from openflexure_microscope.api.utilities import gen, JsonResponse +from openflexure_microscope.api.views import MicroscopeView + +from flask import Blueprint, jsonify, request +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: Config; Set microscope config + + **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 + + :header Accept: application/json + :>header Content-Type: application/json + :status 200: state available + """ + return jsonify(self.microscope.status) + + +def construct_blueprint(microscope_obj): + + blueprint = Blueprint("v2_status_blueprint", __name__) + + blueprint.add_url_rule( + "/", view_func=StatusAPI.as_view("status", microscope=microscope_obj) + ) + + return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/stream.py b/openflexure_microscope/api/v2/blueprints/stream.py new file mode 100644 index 00000000..a6f40a42 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/stream.py @@ -0,0 +1,57 @@ +from openflexure_microscope.api.utilities import gen, JsonResponse +from openflexure_microscope.api.views import MicroscopeView + +from flask import Response, Blueprint, jsonify, request + + +class StreamAPI(MicroscopeView): + def get(self): + """ + Real-time MJPEG stream from the microscope camera + + .. :quickref: Stream; 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): + def get(self): + """ + Single snapshot from the camera stream + + .. :quickref: Stream; 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") + + +def construct_blueprint(microscope_obj): + + blueprint = Blueprint("v2_stream_blueprint", __name__) + + blueprint.add_url_rule( + "/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj) + ) + + blueprint.add_url_rule( + "/snapshot", + view_func=SnapshotAPI.as_view("snapshot", microscope=microscope_obj), + ) + + return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/tasks.py b/openflexure_microscope/api/v2/blueprints/tasks.py new file mode 100644 index 00000000..ffeb9785 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/tasks.py @@ -0,0 +1,160 @@ +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": { + "properties": "{}".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.tasks()[task_id] + except KeyError: + return abort(404) # 404 Not Found + + # Return task state + return jsonify(task.state) + + 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/camera/base.py b/openflexure_microscope/camera/base.py index ae8c35c2..ce7c9143 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -100,7 +100,7 @@ class BaseCamera(metaclass=ABCMeta): last_access (time): Time of last client access to the camera stream_timeout (int): Number of inactive seconds before timing out the stream stream_timeout_enabled (bool): Enable or disable timing out the stream - state (dict): Dictionary for capture state + status (dict): Dictionary for capture state paths (dict): Dictionary of capture paths images (list): List of image capture objects videos (list): List of video capture objects @@ -120,7 +120,7 @@ class BaseCamera(metaclass=ABCMeta): self.stream_timeout = 20 self.stream_timeout_enabled = False - self.state = {"board": None} + self.status = {"board": None} # TODO: Load/save these to config self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH} @@ -130,7 +130,7 @@ class BaseCamera(metaclass=ABCMeta): self.videos = [] @abstractmethod - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """Update settings from a config dictionary""" with self.lock: # Apply valid config params to camera object @@ -139,11 +139,11 @@ class BaseCamera(metaclass=ABCMeta): setattr(self, key, value) # Set to the target value @abstractmethod - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" return {"paths": self.paths} - def save_config(self): + def save_settings(self): """(Optional) Save any settings to disk that need to be stored""" return @@ -187,7 +187,7 @@ class BaseCamera(metaclass=ABCMeta): self.last_access = time.time() self.stop = False - if not self.state["stream_active"]: + if not self.status["stream_active"]: # start background frame thread self.thread = threading.Thread(target=self._thread) self.thread.daemon = True @@ -207,12 +207,12 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("Stopping worker thread") timeout_time = time.time() + timeout - if self.state["stream_active"]: + if self.status["stream_active"]: self.stop = True self.thread.join() # Wait for stream thread to exit logging.debug("Waiting for stream thread to exit.") - while self.state["stream_active"]: + while self.status["stream_active"]: if time.time() > timeout_time: logging.debug("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.") @@ -352,7 +352,7 @@ class BaseCamera(metaclass=ABCMeta): self.frames_iterator = self.frames() logging.debug("Entering worker thread.") - self.state["stream_active"] = True + self.status["stream_active"] = True for frame in self.frames_iterator: self.frame = frame @@ -365,7 +365,7 @@ class BaseCamera(metaclass=ABCMeta): and ( # If using timeout time.time() - self.last_access > self.stream_timeout ) - and not self.state[ # And timeout time + and not self.status[ # And timeout time "preview_active" ] # And GPU preview is not active ): @@ -383,4 +383,4 @@ class BaseCamera(metaclass=ABCMeta): logging.debug("BaseCamera worker thread exiting...") # Set stream_activate state - self.state["stream_active"] = False + self.status["stream_active"] = False diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 410c5560..9a66bb79 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -97,7 +97,7 @@ def capture_from_exif(path, exif_dict): capture.timestring = exif_dict["time"] capture.format = exif_dict["format"] - capture._metadata = exif_dict["custom"] + capture.custom_metadata = exif_dict["custom"] capture.tags = exif_dict["tags"] return capture @@ -110,7 +110,7 @@ class CaptureObject(object): Attributes: timestring (str): Timestring of capture creation time - _metadata (dict): Dictionary of custom metadata to be included in metadata file + custom_metadata (dict): Dictionary of custom metadata to be included in metadata file tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation filefolder (str): Folder in which the capture file will be stored filename (str): Full name of the capture file @@ -132,7 +132,9 @@ class CaptureObject(object): self.split_file_path(self.file) # Dictionary for storing custom metadata - self._metadata = {} + self.custom_metadata = {} + # Dictionary for adding top-level metadata (cannmot be accessed through web API) + self.system_metadata = {} # List for storing tags self.tags = [] @@ -203,7 +205,7 @@ class CaptureObject(object): Args: data (dict): Dictionary of metadata to be added """ - self._metadata.update(data) + self.custom_metadata.update(data) self.save_metadata() def save_metadata(self) -> None: @@ -225,6 +227,7 @@ class CaptureObject(object): # Insert exif into file piexif.insert(exif_bytes, self.file) + @property def metadata(self) -> dict: """ @@ -233,13 +236,14 @@ class CaptureObject(object): """ d = { "id": self.id, - "filename": self.filename, "time": self.timestring, "format": self.format, "tags": self.tags, - "custom": self._metadata, + "custom": self.custom_metadata, } + d.update(self.system_metadata) + # Add custom metadata to dictionary return d @@ -250,7 +254,7 @@ class CaptureObject(object): """ # Create basic state dictionary - d = {"path": self.file, "metadata": self.metadata} + d = {"path": self.file, "filename": self.filename, "metadata": self.metadata} # Combined availability of data if self.exists: diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index 0b779e88..1d252cdf 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -26,7 +26,7 @@ class MockStreamer(BaseCamera): BaseCamera.__init__(self) # Store state of PiCameraStreamer - self.state.update( + self.status.update( {"stream_active": False, "record_active": False, "board": None} ) @@ -74,13 +74,13 @@ class MockStreamer(BaseCamera): BaseCamera.close(self) # HANDLE SETTINGS - def read_config(self) -> dict: + def read_settings(self) -> dict: """ Return config dictionary of the PiCameraStreamer. """ # Get config items from the base class - conf_dict = BaseCamera.read_config(self) + conf_dict = BaseCamera.read_settings(self) # Include device-specific config items conf_dict.update( @@ -94,7 +94,7 @@ class MockStreamer(BaseCamera): return conf_dict - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -110,7 +110,7 @@ class MockStreamer(BaseCamera): with self.lock: # Apply valid config params to camera object - if not self.state["record_active"]: # If not recording a video + if not self.status["record_active"]: # If not recording a video for key, value in config.items(): # For each provided setting if hasattr(self, key): diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index 68f4d59b..8a75b817 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -75,8 +75,8 @@ class PiCameraStreamer(BaseCamera): picamera.PiCamera() ) #: :py:class:`picamera.PiCamera`: Picamera object - # Store state of PiCameraStreamer - self.state.update( + # Store status of PiCameraStreamer + self.status.update( { "stream_active": False, "record_active": False, @@ -97,7 +97,7 @@ class PiCameraStreamer(BaseCamera): self.picamera_lst_path = settings_file_path("picamera_lst.npy") #: str: Path of .npy lens shading table file # Update board identifier - self.state.update({}) + self.status.update({}) # Create an empty stream self.stream = io.BytesIO() @@ -118,13 +118,13 @@ class PiCameraStreamer(BaseCamera): self.camera.close() # HANDLE SETTINGS - def read_config(self) -> dict: + def read_settings(self) -> dict: """ Return config dictionary of the PiCameraStreamer. """ # Get config items from the base class - conf_dict = BaseCamera.read_config(self) + conf_dict = BaseCamera.read_settings(self) # Include device-specific config items conf_dict.update( @@ -149,12 +149,12 @@ class PiCameraStreamer(BaseCamera): return conf_dict - def save_config(self): + def save_settings(self): """Save lens-shading table to disk""" logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path)) self.save_lens_shading_table() - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """ Write a config dictionary to the PiCameraStreamer config. @@ -172,10 +172,10 @@ class PiCameraStreamer(BaseCamera): with self.lock: # Apply valid config params to Picamera object - if not self.state["record_active"]: # If not recording a video + if not self.status["record_active"]: # If not recording a video # Pause stream while changing settings - if self.state["stream_active"]: # If stream is active + if self.status["stream_active"]: # If stream is active logging.info("Pausing stream to update config.") self.stop_stream_recording() # Pause stream paused_stream = True # Remember to unpause stream when done @@ -299,13 +299,13 @@ class PiCameraStreamer(BaseCamera): Change the camera zoom, handling re-centering and scaling. """ with self.lock: - self.state["zoom_value"] = float(zoom_value) - if self.state["zoom_value"] < 1: - self.state["zoom_value"] = 1 + self.status["zoom_value"] = float(zoom_value) + if self.status["zoom_value"] < 1: + self.status["zoom_value"] = 1 # Richard's code for zooming ! fov = self.camera.zoom centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) - size = 1.0 / self.state["zoom_value"] + size = 1.0 / self.status["zoom_value"] # If the new zoom value would be invalid, move the centre to # keep it within the camera's sensor (this is only relevant # when zooming out, if the FoV is not centred on (0.5, 0.5) @@ -332,7 +332,7 @@ class PiCameraStreamer(BaseCamera): self.camera.preview.window = window if fullscreen: self.camera.preview.fullscreen = fullscreen - self.state["preview_active"] = True + self.status["preview_active"] = True except picamera.exc.PiCameraMMALError as e: logging.error( "Suppressed a MMALError in start_preview. Exception: {}".format(e) @@ -347,7 +347,7 @@ class PiCameraStreamer(BaseCamera): def stop_preview(self): """Stop the on board GPU camera preview.""" self.camera.stop_preview() - self.state["preview_active"] = False + self.status["preview_active"] = False def start_recording(self, output, fmt: str = "h264", quality: int = 15): """Start recording. @@ -365,7 +365,7 @@ class PiCameraStreamer(BaseCamera): """ with self.lock: # Start recording method only if a current recording is not running - if not self.state["record_active"]: + if not self.status["record_active"]: # Start the camera video recording on port 2 logging.info("Recording to {}".format(output)) @@ -378,8 +378,8 @@ class PiCameraStreamer(BaseCamera): quality=quality, ) - # Update state dictionary - self.state["record_active"] = True + # Update status dictionary + self.status["record_active"] = True return output @@ -398,8 +398,8 @@ class PiCameraStreamer(BaseCamera): self.camera.stop_recording(splitter_port=2) logging.info("Recording stopped") - # Update state dictionary - self.state["record_active"] = False + # Update status dictionary + self.status["record_active"] = False def stop_stream_recording( self, splitter_port: int = 1, resolution: Tuple[int, int] = None @@ -467,7 +467,7 @@ class PiCameraStreamer(BaseCamera): self.camera.resolution = resolution # If the stream should be active - if self.state["stream_active"]: + if self.status["stream_active"]: try: # Start recording on stream port self.camera.start_recording( @@ -633,7 +633,7 @@ class PiCameraStreamer(BaseCamera): # Start stream recording (and set resolution) self.start_stream_recording() - # Update state + # Update status logging.debug("STREAM ACTIVE") # While the iterator is not closed diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 5b6b2c1c..77ab3c48 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -85,16 +85,16 @@ class OpenflexureSettingsFile: # If the loaded config was in contracted format if self.expand: # Contract self._config into self.raw_config - save_config = self.contract_config(config) + save_settings = self.contract_config(config) else: - save_config = config + save_settings = config if backup: if os.path.isfile(self.config_path): shutil.copyfile(self.config_path, self.config_path + ".bk") logging.debug("Saving settings dictionary to disk") - save_json_file(self.config_path, save_config) + save_json_file(self.config_path, save_settings) def merge(self, config: dict, backup: bool = True): logging.debug("Merging settings with file on disk") diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index d52efdf7..820d5986 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -11,7 +11,7 @@ from openflexure_microscope.stage.mock import MockStage from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.camera.mock import MockStreamer -from openflexure_microscope.plugins import PluginMount +from openflexure_microscope.plugins import PluginLoader from openflexure_microscope.task import TaskOrchestrator from openflexure_microscope.common.lock import CompositeLock from openflexure_microscope.config import user_settings @@ -30,7 +30,7 @@ class Microscope: stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing background tasks using microscope hardware - plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins + plugins (:py:class:`openflexure_microscope.plugins.PluginLoader`): Mounting point for all microscope plugins """ def __init__(self): @@ -49,10 +49,10 @@ class Microscope: self.task = TaskOrchestrator() # Apply settings loaded from file - self.apply_config(user_settings.load()) + self.apply_settings(user_settings.load()) # Create plugin mount-point and attach plugins from maps - self.plugin = PluginMount(self) + self.plugins = PluginLoader(self) self.attach_plugins(self.plugin_maps) def __enter__(self): @@ -84,7 +84,7 @@ class Microscope: stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object """ - settings_full = self.read_config() + settings_full = self.read_settings() logging.debug("Attaching camera...") self.camera = ( @@ -117,7 +117,7 @@ class Microscope: self.lock.locks.append(self.stage.lock) logging.info("Reapplying settings to newly attached devices") - self.apply_config(settings_full) + self.apply_settings(settings_full) def attach_plugins(self, plugin_maps: list): """ @@ -125,7 +125,7 @@ class Microscope: """ if plugin_maps: for plugin_map in plugin_maps: - self.plugin.attach(plugin_map) + self.plugins.attach(plugin_map) else: logging.warning("No plugins specified. Skipping.") @@ -134,7 +134,7 @@ class Microscope: Empty the plugin mount and re-attach from config. """ logging.info("Tearing down existing PluginMount...") - self.plugin = PluginMount(self) + self.plugins = PluginLoader(self) logging.info("Repopulating PluginMount...") self.attach_plugins(self.plugin_maps) @@ -157,17 +157,34 @@ class Microscope: Return: dict: Dictionary containing position data, - and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state` + and :py:attr:`openflexure_microscope.camera.base.BaseCamera.status` """ + # DEPRECATED + logging.warning("Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version.") state = { - "camera": self.camera.state, - "stage": self.stage.state, - "plugin": self.plugin.state, + "camera": self.camera.status, + "stage": self.stage.status, + "plugin": self.plugins.state, "version": pkg_resources.get_distribution("openflexure_microscope").version, } return state - def apply_config(self, config: dict): + # Create unified status + @property + def status(self): + """Dictionary of the basic microscope status. + + Return: + dict: Dictionary containing complete microscope status + """ + state = { + "camera": self.camera.status, + "stage": self.stage.status, + "version": pkg_resources.get_distribution("openflexure_microscope").version, + } + return state + + def apply_settings(self, config: dict): """ Applies a config dictionary. Missing parameters will be left untouched. """ @@ -175,11 +192,11 @@ class Microscope: # If attached to a camera if ("camera_settings" in config) and self.camera: - self.camera.apply_config(config["camera_settings"]) + self.camera.apply_settings(config["camera_settings"]) # If attached to a stage if ("stage_settings" in config) and self.stage: - self.stage.apply_config(config["stage_settings"]) + self.stage.apply_settings(config["stage_settings"]) # Todo: tidy up with some loopy goodness if "id" in config: @@ -191,7 +208,7 @@ class Microscope: if "plugins" in config: self.plugin_maps = config["plugins"] - def read_config(self, json_safe=False): + def read_settings(self, json_safe=False): """ Get an updated settings dictionary. @@ -211,47 +228,43 @@ class Microscope: # If attached to a camera if self.camera: - settings_current_camera = self.camera.read_config() + settings_current_camera = self.camera.read_settings() settings_current["camera_settings"] = settings_current_camera # If attached to a stage if self.stage: - settings_current_stage = self.stage.read_config() + settings_current_stage = self.stage.read_settings() settings_current["stage_settings"] = settings_current_stage settings_full = user_settings.merge(settings_current) return settings_full - def save_config(self): + def save_settings(self): """ Merges the current settings back to disk """ # Read curent config - current_config = self.read_config() - # Merge in server version responsible for saving the config file - current_config["server_version"] = pkg_resources.get_distribution( - "openflexure_microscope" - ).version + current_config = self.read_settings() # Save config to file if self.camera: - self.camera.save_config() + self.camera.save_settings() if self.stage: - self.stage.save_config() + self.stage.save_settings() user_settings.save(current_config, backup=True) @property def config(self) -> dict: logging.warning( "Reading microscope through config property is deprecated.\ - Please use read_config method instead." + Please use read_settings method instead." ) - return self.read_config() + return self.read_settings() @config.setter def config(self, config: dict) -> None: logging.warning( "Setting microscope through config property is deprecated.\ - Please use apply_config method instead." + Please use apply_settings method instead." ) - self.apply_config(config) + self.apply_settings(config) diff --git a/openflexure_microscope/plugins/__init__.py b/openflexure_microscope/plugins/__init__.py index df920221..c22207f5 100644 --- a/openflexure_microscope/plugins/__init__.py +++ b/openflexure_microscope/plugins/__init__.py @@ -3,7 +3,7 @@ __all__ = [ "load_plugin_class", "load_plugin_module", "class_from_map", - "PluginMount", + "PluginLoader", "MicroscopePlugin", ] @@ -12,6 +12,6 @@ from .loader import ( load_plugin_class, load_plugin_module, class_from_map, - PluginMount, + PluginLoader, MicroscopePlugin, ) diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py index 5b942ab8..6708cbba 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ b/openflexure_microscope/plugins/default/camera_calibration/plugin.py @@ -37,10 +37,10 @@ class Plugin(MicroscopePlugin): """ scamera = self.microscope.camera with scamera.lock: - assert not scamera.state[ + assert not scamera.status[ "record_active" ], "Can't recalibrate while recording!" - streaming = scamera.state["stream_active"] + streaming = scamera.status["stream_active"] if streaming: logging.info("Stopping stream before recalibration") scamera.stop_stream_recording(resolution=(640, 480)) @@ -51,7 +51,7 @@ class Plugin(MicroscopePlugin): recalibrate_camera(scamera.camera) finally: scamera.camera.resolution = old_resolution - self.microscope.save_config() + self.microscope.save_settings() if streaming: logging.info("Restarting stream after recalibration") scamera.start_stream_recording() diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index 46889e99..35c29e9c 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -97,18 +97,19 @@ class ScanPlugin(MicroscopePlugin): if "scan" not in tags: tags.append("scan") - metadata.update( - { - "scan_id": scan_id, - "basename": basename, - "microscope_settings": self.microscope.read_config(), - "microscope_state": self.microscope.state, - "microscope_id": self.microscope.id, - "microscope_name": self.microscope.name, - } - ) + # Inject system metadata + system_metadata = { + "microscope_settings": self.microscope.read_settings(), + "microscope_state": self.microscope.state, + "microscope_id": self.microscope.id, + "microscope_name": self.microscope.name, + } + output.system_metadata.update(system_metadata) + # Insert custom metadata output.put_metadata(metadata) + + # Insert custom tags output.put_tags(tags) def tile( diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 691af293..120c36ef 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -137,7 +137,7 @@ def class_from_map(plugin_map): return load_plugin_class(*plugin_arr) -class PluginMount(object): +class PluginLoader(object): """ A mount-point for all loaded plugins. Attaches to a Microscope object. @@ -147,30 +147,19 @@ class PluginMount(object): def __init__(self, parent): self.parent = parent - self.plugins = [] # List of plugin objects + self._plugins = [] # List of plugin objects self.forms = [] # List of plugin forms logging.info("Creating plugin mount") @property def state(self): - return [m[0] for m in self.members] + # DEPRECATED + logging.warning("PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version.") + return [m["python_name"] for m in self._plugins] @property - def members(self): - ignores = ["state", "members", "attach"] - plugin_array = [] - for obj_name in dir(self): - if not obj_name in ignores and not obj_name[:2] == "__": - obj = getattr(self, obj_name) - if isinstance(obj, MicroscopePlugin): - plugin_members = [ - member - for member in inspect.getmembers(obj) - if not member[0][:2] == "__" - ] - plugin_info = (obj_name, plugin_members) - plugin_array.append(plugin_info) - return plugin_array + def active(self): + return self._plugins def attach(self, plugin_map): """ @@ -204,7 +193,15 @@ class PluginMount(object): ): # If plugin_object is an instance of MicroscopePlugin # Attach plugin_object to the plugin mount setattr(self, pythonsafe_plugin_name, plugin_object) - self.plugins.append((plugin_name, plugin_object)) + + # Store the plugin object, and it's properties + self._plugins.append({ + "name": plugin_name, + "python_name": pythonsafe_plugin_name, + "plugin": plugin_object, + "routes": [], + "form": None + }) # Grant plugin access to the hardware plugin_object.microscope = self.parent diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 5ae45563..4eca632e 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -14,22 +14,22 @@ class BaseStage(metaclass=ABCMeta): self.lock = StrictLock(timeout=5) @abstractmethod - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """Update settings from a config dictionary""" pass @abstractmethod - def read_config(self): + def read_settings(self): """Return the current settings as a dictionary""" pass - def save_config(self): + def save_settings(self): """(Optional) Save any settings to disk that need to be stored""" return @property @abstractmethod - def state(self): + def status(self): """The general state dictionary of the board. Should at least contain 'position', and 'board' keys. Note: A None/Null value for 'board' will disable stage diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index e37a7271..7e3be54b 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -16,9 +16,9 @@ class MockStage(BaseStage): self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property - def state(self): - """The general state dictionary of the board.""" - state = { + def status(self): + """The general status dictionary of the board.""" + status = { "position": { "x": self.position[0], "y": self.position[1], @@ -27,9 +27,9 @@ class MockStage(BaseStage): "board": None, "version": "0", } - return state + return status - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels @@ -38,7 +38,7 @@ class MockStage(BaseStage): backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index d25d5691..5588a929 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -31,9 +31,9 @@ class SangaStage(BaseStage): self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis @property - def state(self): - """The general state dictionary of the board.""" - state = { + def status(self): + """The general status dictionary of the board.""" + status = { "position": { "x": self.position[0], "y": self.position[1], @@ -42,7 +42,7 @@ class SangaStage(BaseStage): "board": self.board.board, "firmware": self.board.firmware, } - return state + return status @property def n_axes(self): @@ -85,7 +85,7 @@ class SangaStage(BaseStage): else: self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) - def apply_config(self, config: dict): + def apply_settings(self, config: dict): """Update settings from a config dictionary""" # Set backlash. Expects a dictionary with axis labels @@ -94,7 +94,7 @@ class SangaStage(BaseStage): backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0]) self.backlash = backlash - def read_config(self) -> dict: + def read_settings(self) -> dict: """Return the current settings as a dictionary""" blsh = self.backlash.tolist() config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}} diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 4a215c66..a6c9895a 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -14,18 +14,18 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) class TestPluginMethods(unittest.TestCase): def test_plugin_load(self): - plugin_arr = microscope.plugin.plugins + plugin_arr = microscope.plugins.plugins plugin_names = [plugin[0] for plugin in plugin_arr] self.assertTrue("testing" in plugin_names) def test_camera_access(self): - identify = microscope.plugin.testing.identify() + identify = microscope.plugins.testing.identify() self.assertTrue(identify[0] is microscope.camera) def test_stage_access(self): - identify = microscope.plugin.testing.identify() + identify = microscope.plugins.testing.identify() self.assertTrue(identify[1] is microscope.stage) @@ -35,7 +35,7 @@ if __name__ == "__main__": microscope.attach(PiCameraStreamer(), OpenFlexureStage()) - microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin") + microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin") suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)] From d79fd5530103781a280ef4f90ce4cef8fc2bc01d Mon Sep 17 00:00:00 2001 From: jtc42 Date: Fri, 15 Nov 2019 16:40:25 +0000 Subject: [PATCH 02/27] Drafted actions routes --- openflexure_microscope/api/app.py | 6 +- .../api/v2/blueprints/actions/__init__.py | 87 +++++++++ .../api/v2/blueprints/actions/camera.py | 166 ++++++++++++++++++ .../api/v2/blueprints/actions/stage.py | 54 ++++++ .../api/v2/blueprints/actions/system.py | 33 ++++ .../api/v2/blueprints/captures.py | 29 +-- .../api/v2/blueprints/tasks.py | 2 +- openflexure_microscope/camera/pi.py | 8 +- 8 files changed, 360 insertions(+), 25 deletions(-) diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 8d9c8ec1..9e91b40a 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -143,7 +143,7 @@ task_blueprint = blueprints.task.construct_blueprint(api_microscope) app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) ### V2 -# Tasks routes +# Root routes v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope) app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2")) @@ -167,6 +167,10 @@ app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2")) v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope) app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2")) +# Actions routes +v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope) +app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2")) + @app.route("/routes") def routes(): diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index e69de29b..6900e796 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -0,0 +1,87 @@ +from flask import Blueprint, url_for, jsonify +from sys import platform + +from . import camera, stage, system + +_actions = { + "capture": { + "rule": "/camera/capture/", + "view_class": camera.CaptureAPI, + "description": "Take a single still capture", + "conditions": True + }, + "preview_start": { + "rule": "/camera/preview/start", + "view_class": camera.GPUPreviewStartAPI, + "description": "Start the on-board GPU preview", + "conditions": True + }, + "preview_stop": { + "rule": "/camera/preview/stop", + "view_class": camera.GPUPreviewStopAPI, + "description": "Stop the on-board GPU preview", + "conditions": True + }, + "move": { + "rule": "/stage/move/", + "view_class": stage.MoveStageAPI, + "description": "Move the microscope stage", + "conditions": True + }, + "shutdown": { + "rule": "/system/shutdown/", + "view_class": system.ShutdownAPI, + "description": "Shutdown the device", + "conditions": (platform == "linux") + }, + "reboot": { + "rule": "/system/reboot/", + "view_class": system.RebootAPI, + "description": "Reboot the device", + "conditions": (platform == "linux") + } +} + + +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}")}, + "description": action["description"], + "rule": action["rule"], + "view_class": str(action["view_class"]) + } + + actions[name] = d + + return actions + + +def construct_blueprint(microscope_obj): + global _actions + + blueprint = Blueprint("actions_blueprint", __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.route("/") + def representation(): + return jsonify(actions_representation()) + + return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py index e69de29b..1a91a832 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ b/openflexure_microscope/api/v2/blueprints/actions/camera.py @@ -0,0 +1,166 @@ +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): + 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 + + :
Date: Fri, 15 Nov 2019 16:51:48 +0000 Subject: [PATCH 03/27] Tidied and improved JSON representations --- .../api/v2/blueprints/actions/__init__.py | 2 -- .../api/v2/blueprints/actions/system.py | 8 ++------ openflexure_microscope/api/v2/blueprints/captures.py | 12 ++++-------- openflexure_microscope/api/v2/blueprints/tasks.py | 4 ++-- .../plugins/default/autofocus/api.py | 1 + openflexure_microscope/task.py | 2 +- 6 files changed, 10 insertions(+), 19 deletions(-) diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index 6900e796..c2270552 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -45,7 +45,6 @@ _actions = { def enabled_actions(): global _actions - return {k: v for k, v in _actions.items() if v["conditions"]} @@ -53,7 +52,6 @@ def actions_representation(): global _actions actions = {} - for name, action in enabled_actions().items(): d = { "links": {"self": url_for(f".{name}")}, diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/blueprints/actions/system.py index 6e3d9b0d..86878026 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/blueprints/actions/system.py @@ -1,12 +1,8 @@ from openflexure_microscope.api.views import MicroscopeView -from flask import jsonify - import subprocess -import logging -# TODO: Make robust against different host OS class ShutdownAPI(MicroscopeView): def post(self): """ @@ -17,7 +13,7 @@ class ShutdownAPI(MicroscopeView): """ subprocess.Popen(['shutdown', '-h', 'now']) - return '', 202 + return "{}", 201 class RebootAPI(MicroscopeView): @@ -30,4 +26,4 @@ class RebootAPI(MicroscopeView): """ subprocess.Popen(['sudo', 'shutdown', '-r', 'now']) - return '', 202 + return "{}", 201 diff --git a/openflexure_microscope/api/v2/blueprints/captures.py b/openflexure_microscope/api/v2/blueprints/captures.py index 0ffc3dff..f9bae501 100644 --- a/openflexure_microscope/api/v2/blueprints/captures.py +++ b/openflexure_microscope/api/v2/blueprints/captures.py @@ -61,9 +61,7 @@ class ListAPI(MicroscopeView): :status 200: capture found :status 404: no capture found with that id """ - include_unavailable = get_bool(request.args.get("include_unavailable")) - - representation = captures_representation(self.microscope.camera.images, include_unavailable=include_unavailable) + representation = captures_representation(self.microscope.camera.images) return jsonify(representation) @@ -132,11 +130,9 @@ class CaptureAPI(MicroscopeView): """ - all_captures = captures_representation(self.microscope.camera.images, include_unavailable=True) - - if capture_id in all_captures: - representation = all_captures[capture_id] - else: + try: + representation = captures_representation(self.microscope.camera.images)[capture_id] + except KeyError: return abort(404) # 404 Not Found return jsonify(representation) diff --git a/openflexure_microscope/api/v2/blueprints/tasks.py b/openflexure_microscope/api/v2/blueprints/tasks.py index d2a5c440..327dfe02 100644 --- a/openflexure_microscope/api/v2/blueprints/tasks.py +++ b/openflexure_microscope/api/v2/blueprints/tasks.py @@ -121,7 +121,7 @@ class TaskAPI(MethodView): """ try: - task = tasks.tasks()[task_id] + task = tasks_representation()[task_id] except KeyError: return abort(404) # 404 Not Found @@ -140,7 +140,7 @@ class TaskAPI(MethodView): """ try: - task = tasks.tasks()[task_id] + task = tasks_representation()[task_id] except KeyError: return abort(404) # 404 Not Found diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index 100f5f13..5f646921 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -34,6 +34,7 @@ class AutofocusAPI(MicroscopeViewPlugin): else: abort(503, 'No stage connected. Unable to autofocus.') + class FastAutofocusAPI(MicroscopeViewPlugin): def post(self): payload = JsonResponse(request) diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py index e9dd7e3d..c195e3fc 100644 --- a/openflexure_microscope/task.py +++ b/openflexure_microscope/task.py @@ -7,7 +7,7 @@ from openflexure_microscope.common.tasks import ( remove_task, ) - +# DEPRECATED class TaskOrchestrator: """ DEPRECATED: Class responsible for spawning threaded tasks, and storing their returns. From 76af0891e9749325af043d47339212e0e9039e4c Mon Sep 17 00:00:00 2001 From: jtc42 Date: Fri, 15 Nov 2019 16:52:57 +0000 Subject: [PATCH 04/27] Blackened --- .../api/v1/blueprints/camera/capture.py | 10 +++--- .../api/v2/blueprints/actions/__init__.py | 16 ++++----- .../api/v2/blueprints/actions/camera.py | 10 +++--- .../api/v2/blueprints/actions/system.py | 4 +-- .../api/v2/blueprints/captures.py | 23 ++++++++----- .../api/v2/blueprints/plugins.py | 5 ++- .../api/v2/blueprints/tasks.py | 4 +-- openflexure_microscope/camera/capture.py | 5 +-- openflexure_microscope/camera/pi.py | 34 ++++++++++++++----- openflexure_microscope/config.py | 3 ++ openflexure_microscope/microscope.py | 4 ++- .../plugins/default/autofocus/api.py | 4 +-- .../plugins/default/scan/plugin.py | 16 +++++---- openflexure_microscope/plugins/loader.py | 20 ++++++----- 14 files changed, 95 insertions(+), 63 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index 8afe5443..d5e66dc5 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -132,11 +132,11 @@ class ListAPI(MicroscopeView): # Inject system metadata system_metadata = { - "microscope_settings": self.microscope.read_settings(), - "microscope_state": self.microscope.state, - "microscope_id": self.microscope.id, - "microscope_name": self.microscope.name, - } + "microscope_settings": self.microscope.read_settings(), + "microscope_state": self.microscope.state, + "microscope_id": self.microscope.id, + "microscope_name": self.microscope.name, + } output.system_metadata.update(system_metadata) # Insert custom metadata diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index c2270552..88285f12 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -8,38 +8,38 @@ _actions = { "rule": "/camera/capture/", "view_class": camera.CaptureAPI, "description": "Take a single still capture", - "conditions": True + "conditions": True, }, "preview_start": { "rule": "/camera/preview/start", "view_class": camera.GPUPreviewStartAPI, "description": "Start the on-board GPU preview", - "conditions": True + "conditions": True, }, "preview_stop": { "rule": "/camera/preview/stop", "view_class": camera.GPUPreviewStopAPI, "description": "Stop the on-board GPU preview", - "conditions": True + "conditions": True, }, "move": { "rule": "/stage/move/", "view_class": stage.MoveStageAPI, "description": "Move the microscope stage", - "conditions": True + "conditions": True, }, "shutdown": { "rule": "/system/shutdown/", "view_class": system.ShutdownAPI, "description": "Shutdown the device", - "conditions": (platform == "linux") + "conditions": (platform == "linux"), }, "reboot": { "rule": "/system/reboot/", "view_class": system.RebootAPI, "description": "Reboot the device", - "conditions": (platform == "linux") - } + "conditions": (platform == "linux"), + }, } @@ -57,7 +57,7 @@ def actions_representation(): "links": {"self": url_for(f".{name}")}, "description": action["description"], "rule": action["rule"], - "view_class": str(action["view_class"]) + "view_class": str(action["view_class"]), } actions[name] = d diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py index 1a91a832..82a490ee 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ b/openflexure_microscope/api/v2/blueprints/actions/camera.py @@ -84,11 +84,11 @@ class CaptureAPI(MicroscopeView): # Inject system metadata system_metadata = { - "microscope_settings": self.microscope.read_settings(), - "microscope_state": self.microscope.state, - "microscope_id": self.microscope.id, - "microscope_name": self.microscope.name, - } + "microscope_settings": self.microscope.read_settings(), + "microscope_state": self.microscope.state, + "microscope_id": self.microscope.id, + "microscope_name": self.microscope.name, + } output.system_metadata.update(system_metadata) # Insert custom metadata diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/blueprints/actions/system.py index 86878026..ab36911b 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/blueprints/actions/system.py @@ -11,7 +11,7 @@ class ShutdownAPI(MicroscopeView): .. :quickref: Actions; Shutdown """ - subprocess.Popen(['shutdown', '-h', 'now']) + subprocess.Popen(["shutdown", "-h", "now"]) return "{}", 201 @@ -24,6 +24,6 @@ class RebootAPI(MicroscopeView): .. :quickref: Actions; Shutdown """ - subprocess.Popen(['sudo', 'shutdown', '-r', 'now']) + subprocess.Popen(["sudo", "shutdown", "-r", "now"]) return "{}", 201 diff --git a/openflexure_microscope/api/v2/blueprints/captures.py b/openflexure_microscope/api/v2/blueprints/captures.py index f9bae501..814445e6 100644 --- a/openflexure_microscope/api/v2/blueprints/captures.py +++ b/openflexure_microscope/api/v2/blueprints/captures.py @@ -20,14 +20,22 @@ def captures_representation(capture_list: list, include_unavailable: bool = Fals if include_unavailable: captures = {image.id: image.state for image in capture_list} else: - captures = {image.id: image.state for image in capture_list if image.state["available"]} + captures = { + image.id: image.state for image in capture_list if image.state["available"] + } for capture_key, capture_repr in captures.items(): # Add API routes to returned representations extra_state = { "links": { "self": "{}".format(url_for(".capture", capture_id=capture_key)), - "download": "{}".format(url_for(".capture_download", capture_id=capture_key, filename=capture_repr["filename"])), + "download": "{}".format( + url_for( + ".capture_download", + capture_id=capture_key, + filename=capture_repr["filename"], + ) + ), "tags": "{}".format(url_for(".capture_tags", capture_id=capture_key)), } } @@ -131,7 +139,9 @@ class CaptureAPI(MicroscopeView): """ try: - representation = captures_representation(self.microscope.camera.images)[capture_id] + representation = captures_representation(self.microscope.camera.images)[ + capture_id + ] except KeyError: return abort(404) # 404 Not Found @@ -379,9 +389,7 @@ def construct_blueprint(microscope_obj): # Capture routes blueprint.add_url_rule( "//download/", - view_func=DownloadAPI.as_view( - "capture_download", microscope=microscope_obj - ), + view_func=DownloadAPI.as_view("capture_download", microscope=microscope_obj), ) blueprint.add_url_rule( @@ -397,7 +405,6 @@ def construct_blueprint(microscope_obj): ) blueprint.add_url_rule( - "/", - view_func=ListAPI.as_view("capture_list", microscope=microscope_obj), + "/", view_func=ListAPI.as_view("capture_list", microscope=microscope_obj) ) return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 9d9a6868..2ac55c2b 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -25,7 +25,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): "name": plugin["name"], "plugin": str(plugin["plugin"]), "routes": plugin["routes"], - "form": plugin["form"] + "form": plugin["form"], } plugins.append(d) @@ -55,8 +55,7 @@ def construct_blueprint(microscope_obj): # Create a base route to return plugin API forms, if any exist blueprint.add_url_rule( - "/", - view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj), + "/", view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj) ) all_routes = [] diff --git a/openflexure_microscope/api/v2/blueprints/tasks.py b/openflexure_microscope/api/v2/blueprints/tasks.py index 327dfe02..fe6c2202 100644 --- a/openflexure_microscope/api/v2/blueprints/tasks.py +++ b/openflexure_microscope/api/v2/blueprints/tasks.py @@ -16,9 +16,7 @@ def tasks_representation(): 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)), - } + "links": {"self": "{}".format(url_for(".task", task_id=task_key))} } tasks_dict[task_key].update(extra_state) diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 9a66bb79..17e244d1 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -35,7 +35,9 @@ def pull_usercomment_dict(filepath): return json.loads(exif_dict["Exif"][37510].decode()) except json.decoder.JSONDecodeError: # TODO: Remove YAML support in a later version - logging.warning(f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON.") + logging.warning( + f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON." + ) return yaml.load(exif_dict["Exif"][37510].decode()) else: return None @@ -227,7 +229,6 @@ class CaptureObject(object): # Insert exif into file piexif.insert(exif_bytes, self.file) - @property def metadata(self) -> dict: """ diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index c45857a9..a3e1fc47 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -68,7 +68,7 @@ class PiCameraStreamer(BaseCamera): "sharpness", "annotate_text", "annotate_text_size", - "zoom" + "zoom", ] def __init__(self): @@ -93,13 +93,23 @@ class PiCameraStreamer(BaseCamera): self.set_zoom(1.0) # Set default settings - self.image_resolution = tuple(self.camera.MAX_RESOLUTION) #: tuple: Resolution for image captures - self.stream_resolution = (832, 624) #: tuple: Resolution for stream and video captures - self.numpy_resolution = (1312, 976) #: tuple: Resolution for numpy array captures + self.image_resolution = tuple( + self.camera.MAX_RESOLUTION + ) #: tuple: Resolution for image captures + self.stream_resolution = ( + 832, + 624, + ) #: tuple: Resolution for stream and video captures + self.numpy_resolution = ( + 1312, + 976, + ) #: tuple: Resolution for numpy array captures self.jpeg_quality = 75 #: int: JPEG quality # Set default lens shading table path - self.picamera_lst_path = settings_file_path("picamera_lst.npy") #: str: Path of .npy lens shading table file + self.picamera_lst_path = settings_file_path( + "picamera_lst.npy" + ) #: str: Path of .npy lens shading table file # Update board identifier self.status.update({}) @@ -138,7 +148,9 @@ class PiCameraStreamer(BaseCamera): "image_resolution": self.image_resolution, "numpy_resolution": self.numpy_resolution, "jpeg_quality": self.jpeg_quality, - "picamera_lst_path": self.picamera_lst_path if os.path.isfile(self.picamera_lst_path) else None, + "picamera_lst_path": self.picamera_lst_path + if os.path.isfile(self.picamera_lst_path) + else None, "picamera_settings": {}, } ) @@ -273,7 +285,7 @@ class PiCameraStreamer(BaseCamera): """ Read the current lens shading table as a numpy array, if it exists. Return None otherwise. """ - if hasattr(self.camera, 'lens_shading_table'): + if hasattr(self.camera, "lens_shading_table"): return self.camera.lens_shading_table else: return None @@ -297,13 +309,17 @@ class PiCameraStreamer(BaseCamera): elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path): self.camera.lens_shading_table = np.load(lst_array_or_path) else: - logging.error("Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping.") + logging.error( + "Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping." + ) def set_zoom(self, zoom_value: float = 1.0) -> None: """ Change the camera zoom, handling re-centering and scaling. """ - logging.warning("set_zoom is deprecated. Please use the 'zoom' property in picamera_settings.") + logging.warning( + "set_zoom is deprecated. Please use the 'zoom' property in picamera_settings." + ) with self.lock: self.status["zoom_value"] = float(zoom_value) if self.status["zoom_value"] < 1: diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 77ab3c48..4d81f98f 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -21,6 +21,7 @@ class JSONEncoder(json.JSONEncoder): """ A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays """ + def default(self, o, markers=None): # PiCamera fractions if isinstance(o, Fraction): @@ -39,6 +40,7 @@ class JSONEncoder(json.JSONEncoder): # MAIN CONFIG CLASS + class OpenflexureSettingsFile: """ An object to handle expansion, conversion, and saving of the microscope configuration. @@ -164,6 +166,7 @@ class OpenflexureSettingsFile: # HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES + def load_json_file(config_path) -> dict: """ Open a .json config file diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 820d5986..6be22766 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -160,7 +160,9 @@ class Microscope: and :py:attr:`openflexure_microscope.camera.base.BaseCamera.status` """ # DEPRECATED - logging.warning("Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version.") + logging.warning( + "Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version." + ) state = { "camera": self.camera.status, "stage": self.stage.status, diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index 5f646921..172391c8 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -32,7 +32,7 @@ class AutofocusAPI(MicroscopeViewPlugin): return jsonify(task.state), 202 else: - abort(503, 'No stage connected. Unable to autofocus.') + abort(503, "No stage connected. Unable to autofocus.") class FastAutofocusAPI(MicroscopeViewPlugin): @@ -53,4 +53,4 @@ class FastAutofocusAPI(MicroscopeViewPlugin): return jsonify(task.state), 202 else: - abort(503, 'No stage connected. Unable to autofocus.') + abort(503, "No stage connected. Unable to autofocus.") diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index 35c29e9c..b63abe0e 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -147,14 +147,16 @@ class ScanPlugin(MicroscopePlugin): if "time" not in metadata: metadata["time"] = generate_basename() - metadata.update({ - "scan_parameters": { - "step_size": step_size, - "grid": grid, - "style": style, - "autofocus_dz": autofocus_dz + metadata.update( + { + "scan_parameters": { + "step_size": step_size, + "grid": grid, + "style": style, + "autofocus_dz": autofocus_dz, + } } - }) + ) # Check if autofocus is enabled if ( diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 120c36ef..fc5e3f93 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -154,7 +154,9 @@ class PluginLoader(object): @property def state(self): # DEPRECATED - logging.warning("PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version.") + logging.warning( + "PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version." + ) return [m["python_name"] for m in self._plugins] @property @@ -195,13 +197,15 @@ class PluginLoader(object): setattr(self, pythonsafe_plugin_name, plugin_object) # Store the plugin object, and it's properties - self._plugins.append({ - "name": plugin_name, - "python_name": pythonsafe_plugin_name, - "plugin": plugin_object, - "routes": [], - "form": None - }) + self._plugins.append( + { + "name": plugin_name, + "python_name": pythonsafe_plugin_name, + "plugin": plugin_object, + "routes": [], + "form": None, + } + ) # Grant plugin access to the hardware plugin_object.microscope = self.parent From dac50f1f3abacf6f33f3b687b28d2dcae5283024 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Fri, 15 Nov 2019 16:53:50 +0000 Subject: [PATCH 05/27] Replaced 202s with more suitable 201s --- docs/source/plugins/example/plugin.py | 2 +- docs/source/plugins/hardware.rst | 4 ++-- openflexure_microscope/plugins/default/autofocus/api.py | 4 ++-- .../plugins/default/camera_calibration/plugin.py | 2 +- openflexure_microscope/plugins/default/scan/api.py | 2 +- openflexure_microscope/plugins/example/api.py | 4 ++-- openflexure_microscope/plugins/testing/form_example/api.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py index 2c29c39c..e99f9e85 100644 --- a/docs/source/plugins/example/plugin.py +++ b/docs/source/plugins/example/plugin.py @@ -142,4 +142,4 @@ class TimelapseAPI(MicroscopeViewPlugin): self.timelapse_task = taskify(self.plugin.timelapse)(n_images) # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 + return jsonify(self.timelapse_task.state), 201 diff --git a/docs/source/plugins/hardware.rst b/docs/source/plugins/hardware.rst index 3b4aa956..cd2f0a70 100644 --- a/docs/source/plugins/hardware.rst +++ b/docs/source/plugins/hardware.rst @@ -44,7 +44,7 @@ An example of a long running task may look like: def post(self): # Attach the long-running method as a microscope task self.my_task = taskify(self.plugin.long_running_function)() - return jsonify(self.my_task.state), 202 + return jsonify(self.my_task.state), 201 After some time, once the task has completed, it could be retreived using: @@ -132,7 +132,7 @@ For example, a timelapse plugin may look like: self.timelapse_task = taskify(self.plugin.timelapse)(n_images) # Return the state of the task (will show ID, start time, and status before the task has finished) - return jsonify(self.timelapse_task.state), 202 + return jsonify(self.timelapse_task.state), 201 Notice that even though we never use the stage here, we still acquire the lock. This means that during the timelapse, diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index 172391c8..0e7ed816 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -29,7 +29,7 @@ class AutofocusAPI(MicroscopeViewPlugin): task = taskify(self.plugin.autofocus)(dz) # return a handle on the autofocus task - return jsonify(task.state), 202 + return jsonify(task.state), 201 else: abort(503, "No stage connected. Unable to autofocus.") @@ -50,7 +50,7 @@ class FastAutofocusAPI(MicroscopeViewPlugin): task = taskify(self.plugin.fast_autofocus)(dz, backlash=backlash) # return a handle on the autofocus task - return jsonify(task.state), 202 + return jsonify(task.state), 201 else: abort(503, "No stage connected. Unable to autofocus.") diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py index 6708cbba..126bb86c 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ b/openflexure_microscope/plugins/default/camera_calibration/plugin.py @@ -18,7 +18,7 @@ class RecalibrateAPIView(MicroscopeViewPlugin): task = taskify(self.plugin.recalibrate)() # Return a handle on the autofocus task - return jsonify(task.state), 202 + return jsonify(task.state), 201 class Plugin(MicroscopePlugin): diff --git a/openflexure_microscope/plugins/default/scan/api.py b/openflexure_microscope/plugins/default/scan/api.py index d61ab01d..13a9a8f2 100644 --- a/openflexure_microscope/plugins/default/scan/api.py +++ b/openflexure_microscope/plugins/default/scan/api.py @@ -60,4 +60,4 @@ class TileScanAPI(MicroscopeViewPlugin): ) # return a handle on the scan task - return jsonify(task.state), 202 + return jsonify(task.state), 201 diff --git a/openflexure_microscope/plugins/example/api.py b/openflexure_microscope/plugins/example/api.py index 1417359a..ac818184 100644 --- a/openflexure_microscope/plugins/example/api.py +++ b/openflexure_microscope/plugins/example/api.py @@ -73,7 +73,7 @@ class LongRunningAPI(MicroscopeViewPlugin): # Attach the long-running method as a microscope task try: task = self.microscope.task.start(self.plugin.long_running, time_to_run) - return jsonify(task.state), 202 + return jsonify(task.state), 201 except TaskDeniedException: return abort(409) @@ -94,7 +94,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin): # Attach the long-running method as a microscope task try: task = self.microscope.task.start(self.plugin.some_exception) - return jsonify(task.state), 202 + return jsonify(task.state), 201 except TaskDeniedException: return abort(409) diff --git a/openflexure_microscope/plugins/testing/form_example/api.py b/openflexure_microscope/plugins/testing/form_example/api.py index f41ad4f5..d4fc40f6 100644 --- a/openflexure_microscope/plugins/testing/form_example/api.py +++ b/openflexure_microscope/plugins/testing/form_example/api.py @@ -57,4 +57,4 @@ class TaskAPI(MicroscopeViewPlugin): task = taskify(self.plugin.generate_random_numbers_for_a_while)(val_int) # return a handle on the autofocus task - return jsonify(task.state), 202 + return jsonify(task.state), 201 From edecc01ca1f918f06e89731adb741b1f0de568e0 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 19 Nov 2019 15:45:54 +0000 Subject: [PATCH 06/27] Added proper root representation --- openflexure_microscope/api/app.py | 3 ++ .../api/v2/blueprints/__init__.py | 2 +- .../api/v2/blueprints/actions/__init__.py | 13 +++-- .../api/v2/blueprints/captures.py | 4 +- .../api/v2/blueprints/plugins.py | 16 ++++-- .../api/v2/blueprints/root.py | 44 +++++++++++++++ openflexure_microscope/plugins/loader.py | 53 +++++++++++++++++-- 7 files changed, 122 insertions(+), 13 deletions(-) create mode 100644 openflexure_microscope/api/v2/blueprints/root.py diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 9e91b40a..33ea8ec1 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -144,6 +144,9 @@ app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) ### V2 # Root routes +v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope) +app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2")) + v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope) app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2")) diff --git a/openflexure_microscope/api/v2/blueprints/__init__.py b/openflexure_microscope/api/v2/blueprints/__init__.py index c29dd363..473540f2 100644 --- a/openflexure_microscope/api/v2/blueprints/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/__init__.py @@ -1 +1 @@ -from . import captures, settings, status, tasks, stream, plugins, actions +from . import root, captures, settings, status, tasks, stream, plugins, actions diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index 88285f12..f16a41e3 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -1,6 +1,8 @@ from flask import Blueprint, url_for, jsonify from sys import platform +from openflexure_microscope.api.views import MicroscopeView + from . import camera, stage, system _actions = { @@ -64,11 +66,14 @@ def actions_representation(): return actions +class ActionsAPI(MicroscopeView): + def get(self): + return jsonify(actions_representation()) def construct_blueprint(microscope_obj): global _actions - blueprint = Blueprint("actions_blueprint", __name__) + blueprint = Blueprint("v2_actions_blueprint", __name__) # For each enabled action route defined in our dictionary above for name, action in enabled_actions().items(): @@ -78,8 +83,8 @@ def construct_blueprint(microscope_obj): view_func=action["view_class"].as_view(name, microscope=microscope_obj), ) - @blueprint.route("/") - def representation(): - return jsonify(actions_representation()) + blueprint.add_url_rule( + "/", view_func=ActionsAPI.as_view("actions", microscope=microscope_obj) + ) return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/captures.py b/openflexure_microscope/api/v2/blueprints/captures.py index 814445e6..0a54e69a 100644 --- a/openflexure_microscope/api/v2/blueprints/captures.py +++ b/openflexure_microscope/api/v2/blueprints/captures.py @@ -378,7 +378,7 @@ class TagsAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("captures_blueprint", __name__) + blueprint = Blueprint("v2_captures_blueprint", __name__) # Tag routes blueprint.add_url_rule( @@ -405,6 +405,6 @@ def construct_blueprint(microscope_obj): ) blueprint.add_url_rule( - "/", view_func=ListAPI.as_view("capture_list", microscope=microscope_obj) + "/", view_func=ListAPI.as_view("captures", microscope=microscope_obj) ) return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 2ac55c2b..50f298f0 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -1,7 +1,7 @@ from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin from openflexure_microscope.api.views import MicroscopeViewPlugin -from flask import Blueprint, jsonify +from flask import Blueprint, jsonify, url_for from openflexure_microscope.api.views import MicroscopeView import copy @@ -27,6 +27,11 @@ def plugins_representation(plugin_loader_object: PluginLoader): "routes": plugin["routes"], "form": plugin["form"], } + + for route in d["routes"]: + route_id = route["id"] + uri = url_for(f"v2_plugins_blueprint.{route_id}") + route["links"]["self"] = uri plugins.append(d) return plugins @@ -51,7 +56,7 @@ class PluginFormAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("plugins_blueprint", __name__) + blueprint = Blueprint("v2_plugins_blueprint", __name__) # Create a base route to return plugin API forms, if any exist blueprint.add_url_rule( @@ -110,7 +115,12 @@ def construct_blueprint(microscope_obj): ) # Add route to the plugin representation dictionary - plugin_representation["routes"].append(full_view_route) + route_representation = { + "id": plugin_route_id, + "route": full_view_route, + "links": {}, + } + plugin_representation["routes"].append(route_representation) else: warnings.warn( diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py new file mode 100644 index 00000000..6106ad86 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/root.py @@ -0,0 +1,44 @@ +from flask import Blueprint, jsonify, url_for +from openflexure_microscope import Microscope +from openflexure_microscope.api.views import MicroscopeView + +def root_representation(microscope_obj: Microscope): + d = { + "settings": { + "description": "Writeable settings for the microscope, and attached hardware", + "links": {"self": url_for("v2_settings_blueprint.settings")} + }, + "status": { + "description": "Read-only status of the microscope, and attached hardware info", + "links": {"self": url_for("v2_status_blueprint.status")} + }, + "plugins": { + "description": "Top-level representation of attached and enabled plugins", + "links": {"self": url_for("v2_plugins_blueprint.plugins")} + }, + "captures": { + "description": "Top-level representation of all acquired captures", + "links": {"self": url_for("v2_captures_blueprint.captures")} + }, + "actions": { + "description": "Top-level representation of enabled actions", + "links": {"self": url_for("v2_actions_blueprint.actions")} + }, + } + + return d + + +class RootAPI(MicroscopeView): + def get(self): + + return jsonify(root_representation(self.microscope)) + + +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/plugins/loader.py b/openflexure_microscope/plugins/loader.py index fc5e3f93..4aaef28e 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -1,8 +1,11 @@ import importlib +import copy import os import inspect import logging +from openflexure_microscope.api.views import MicroscopeViewPlugin + class ConColors: HEADER = "\033[95m" @@ -137,6 +140,50 @@ def class_from_map(plugin_map): return load_plugin_class(*plugin_arr) +def expand_views(plugin_object, plugin_name): + routes = [] + + # If plugin contains valid endpoints + if hasattr(plugin_object, "api_views") and isinstance(plugin_object.api_views, dict): + + # We'll keep a record of how each route was expanded + expanded_routes = {} + + # For each defined endpoint + for view_route, view_class in plugin_object.api_views.items(): + + # Remove all leading slashes from view route + cleaned_route = view_route + while cleaned_route[0] == "/": + cleaned_route = cleaned_route[1:] + + # Construct a full view route from the plugin name + full_view_route = "/{}/{}".format(plugin_name, cleaned_route) + logging.debug(full_view_route) + + # Record how the view_route got expanded + expanded_routes[view_route] = full_view_route + + # Check if endpoint name clashes + if issubclass(view_class, MicroscopeViewPlugin): + + # Create a Python-safe name for the route + plugin_route_id = "plugin{}".format(full_view_route).replace( + "/", "_" + ) + + # Add route to the plugin representation dictionary + route_representation = { + "id": plugin_route_id, + "route": full_view_route, + "links": {}, + } + + routes.append(route_representation) + + return routes + + class PluginLoader(object): """ A mount-point for all loaded plugins. Attaches to a Microscope object. @@ -174,7 +221,7 @@ class PluginLoader(object): if plugin_class is not None: - pythonsafe_plugin_name = plugin_name.replace("/", "_") + plugin_name_python_safe = plugin_name.replace("/", "_") if plugin_class and plugin_name: plugin_object = plugin_class() @@ -194,13 +241,13 @@ class PluginLoader(object): plugin_object, MicroscopePlugin ): # If plugin_object is an instance of MicroscopePlugin # Attach plugin_object to the plugin mount - setattr(self, pythonsafe_plugin_name, plugin_object) + setattr(self, plugin_name_python_safe, plugin_object) # Store the plugin object, and it's properties self._plugins.append( { "name": plugin_name, - "python_name": pythonsafe_plugin_name, + "python_name": plugin_name_python_safe, "plugin": plugin_object, "routes": [], "form": None, From a3ddaa69d30bb4a434a29ea4394a483a21db9bbe Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 16:49:11 +0000 Subject: [PATCH 07/27] Fixed ESI never timing out --- .../extensible_serial_instrument.py | 33 +++++++++++-------- .../stage/sangaboard/sangaboard.py | 10 ++++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index efbf3f6c..2427dd06 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -65,6 +65,7 @@ class ExtensibleSerialInstrument(object): Set up the serial port and so on. """ logging.info("Updating ESI port settings") + logging.debug(kwargs) self.port_settings.update(kwargs) logging.info("Opening ESI connection to port {}".format(port)) self.open(port, False) # Eventually this shouldn't rely on init... @@ -86,7 +87,9 @@ class ExtensibleSerialInstrument(object): assert ( port is not None ), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?" + logging.info("Creating serial.Serial instance...") self._ser = serial.Serial(port, **self.port_settings) + logging.info(f"Created {self._ser}") # the block above wraps the serial IO layer with a text IO layer # this allows us to read/write in neat lines. NB the buffer size must # be set to 1 byte for maximum responsiveness. @@ -130,11 +133,6 @@ class ExtensibleSerialInstrument(object): assert ( self._ser.isOpen() ), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?" - # TODO: Check if this code is needed and if not kill it - # try: - # if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting - # except AttributeError: - # if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting data = query_string + self.termination_character data = data.encode() self._ser.write(data) @@ -145,7 +143,7 @@ class ExtensibleSerialInstrument(object): if self._ser.inWaiting() > 0: self._ser.flushInput() - def readline(self, timeout=None): + def readline(self): """Read one line from the serial port.""" with self.communications_lock: return ( @@ -165,7 +163,7 @@ class ExtensibleSerialInstrument(object): self._communications_lock = threading.RLock() return self._communications_lock - def read_multiline(self, termination_line=None, timeout=None): + def read_multiline(self, termination_line=None): """Read one line from the underlying bus. Must be overriden. This should not need to be reimplemented unless there's a more efficient @@ -181,11 +179,11 @@ class ExtensibleSerialInstrument(object): while ( termination_line not in last_line and len(last_line) > 0 ): # read until we get the termination line. - last_line = self.readline(timeout) + last_line = self.readline() response += last_line return response - def query(self, queryString, multiline=False, termination_line=None, timeout=None): + def query(self, queryString, multiline=False, termination_line=None): """ Write a string to the stage controller and return its response. @@ -193,12 +191,17 @@ class ExtensibleSerialInstrument(object): will keep reading until a termination phrase is reached. """ with self.communications_lock: + logging.debug("Flushing input buffer...") self.flush_input_buffer() + logging.debug(f"Writing query: {queryString}") self.write(queryString) + logging.debug("Query written") if self.ignore_echo == True: # Needs Implementing for a multiline read! - first_line = self.readline(timeout).strip() + logging.debug("Reading first line...") + first_line = self.readline().strip() + logging.debug(f"Read finished. Got {first_line}") if first_line == queryString: - return self.readline(timeout).strip() + return self.readline().strip() else: logging.info("This command did not echo!!!") return first_line @@ -206,11 +209,13 @@ class ExtensibleSerialInstrument(object): if termination_line is not None: multiline = True if multiline: + logging.debug("Reading multiline...") return self.read_multiline(termination_line) else: - return self.readline( - timeout - ).strip() # question: should we strip the final newline? + logging.debug("Reading response...") + line = self.readline().strip() # question: should we strip the final newline? + logging.debug(f"Read finished. Got {line}") + return line def parsed_query( self, diff --git a/openflexure_microscope/stage/sangaboard/sangaboard.py b/openflexure_microscope/stage/sangaboard/sangaboard.py index 5a0004e6..9002144c 100644 --- a/openflexure_microscope/stage/sangaboard/sangaboard.py +++ b/openflexure_microscope/stage/sangaboard/sangaboard.py @@ -90,7 +90,7 @@ class Sangaboard(ExtensibleSerialInstrument): # Once initialised, `firmware` is a string that identifies the firmware version firmware = None - def __init__(self, port=None, **kwargs): + def __init__(self, port=None, timeout: int = 2, **kwargs): """Create a sangaboard object. Arguments are passed to the constructor of @@ -101,13 +101,16 @@ class Sangaboard(ExtensibleSerialInstrument): """ # Initialise basic serial instrument with specified - ExtensibleSerialInstrument.__init__(self, port, **kwargs) + logging.info(f"Initialising ExtensibleSerialInstrument on port {port}") + ExtensibleSerialInstrument.__init__(self, port, timeout=timeout, **kwargs) try: # Make absolutely sure that whatever port we're on is valid + logging.info("Checking valid firmware...") self.check_valid_firmware() # Bit messy: Defining all valid modules as not available, then overwriting with available information if available. + logging.info("Loading modules...") self.light_sensor = LightSensor(False) for module in self.list_modules(): @@ -145,10 +148,11 @@ class Sangaboard(ExtensibleSerialInstrument): """ Overrides superclass, used in self.open(), and port scanning """ + logging.info("Testing communication to SangaBoard") return self.check_valid_firmware() def check_valid_firmware(self): - logging.debug("Running firmware checks") + logging.info("Running firmware checks...") # Request firmware version from the board self.firmware = self.query("version", timeout=2).rstrip() From b8f6193f7bfa15f2f0c2914959a4ec4ee85fb6e7 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 19:19:39 +0000 Subject: [PATCH 08/27] Fixed debug logging --- openflexure_microscope/api/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 33ea8ec1..5c558afe 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -44,7 +44,8 @@ DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log") if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn print("Letting sys handle logs") - logging.basicConfig(level=logging.DEBUG) + logger = logging.getLogger() + logger.setLevel(logging.INFO) else: # Direct standard Python logging to file and console root = logging.getLogger() From 3c096e9b53c109f564b306821438824ca65edfc2 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 19:19:54 +0000 Subject: [PATCH 09/27] Overhauled v2 plugin attaching --- .../api/v1/blueprints/plugins.py | 5 +- .../api/v2/blueprints/plugins.py | 108 ++++------- .../plugins/default/scan/plugin.py | 2 + openflexure_microscope/plugins/loader.py | 176 ++++++++++++------ .../plugins/testing/form_example/plugin.py | 2 + openflexure_microscope/utilities.py | 9 + 6 files changed, 167 insertions(+), 135 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/plugins.py b/openflexure_microscope/api/v1/blueprints/plugins.py index c1d8fc97..f172b79b 100644 --- a/openflexure_microscope/api/v1/blueprints/plugins.py +++ b/openflexure_microscope/api/v1/blueprints/plugins.py @@ -39,10 +39,7 @@ def construct_blueprint(microscope_obj): all_routes = [] # For each plugin attached to the microscope object - for plugin_representation in microscope_obj.plugins.active: - - plugin_obj = plugin_representation["plugin"] - plugin_name = plugin_representation["name"] + for plugin_name, plugin_obj in microscope_obj.plugins._legacy_plugins.items(): # If plugin contains valid endpoints if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 50f298f0..b4709ce7 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -20,20 +20,34 @@ def plugins_representation(plugin_loader_object: PluginLoader): dict: Dictionary representation of all plugins """ plugins = [] + + print(plugin_loader_object.active) + for plugin in plugin_loader_object.active: + logging.info(f"Representing plugin {plugin._name}") d = { - "name": plugin["name"], - "plugin": str(plugin["plugin"]), - "routes": plugin["routes"], - "form": plugin["form"], + "name": plugin._name, + "plugin": str(plugin), + "views": {}, + "form": plugin.form, } - for route in d["routes"]: - route_id = route["id"] - uri = url_for(f"v2_plugins_blueprint.{route_id}") - route["links"]["self"] = uri + for view_id, view in plugin.views.items(): + logging.info(f"Representing view {view_id}") + uri = url_for(f"v2_plugins_blueprint.{view_id}") + # Make links dictionary if it doesn't yet exist + view_d = { + "links": {"self": uri} + } + + d["views"][view_id] = view_d + + print("\n") + print(d) + print("\n") plugins.append(d) + print(plugins) return plugins @@ -66,69 +80,20 @@ def construct_blueprint(microscope_obj): all_routes = [] # For each plugin attached to the microscope object - for plugin_representation in microscope_obj.plugins.active: + for plugin in microscope_obj.plugins.active: - plugin_obj = plugin_representation["plugin"] - plugin_name = plugin_representation["name"] - - # If plugin contains valid endpoints - if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): - - # We'll keep a record of how each route was expanded - expanded_routes = {} - - # For each defined endpoint - for view_route, view_class in plugin_obj.api_views.items(): - - # Remove all leading slashes from view route - cleaned_route = view_route - while cleaned_route[0] == "/": - cleaned_route = cleaned_route[1:] - - # Construct a full view route from the plugin name - full_view_route = "/{}/{}".format(plugin_name, cleaned_route) - logging.debug(full_view_route) - - # Record how the view_route got expanded - expanded_routes[view_route] = full_view_route - - # Check if endpoint name clashes - if full_view_route not in all_routes and issubclass( - view_class, MicroscopeViewPlugin - ): - # Add route to main route dictionary - all_routes.append(full_view_route) - - # Create a Python-safe name for the route - plugin_route_id = "plugin{}".format(full_view_route).replace( - "/", "_" - ) - - # Add route to the plugins blueprint - blueprint.add_url_rule( - full_view_route, - view_func=view_class.as_view( - plugin_route_id, - microscope=microscope_obj, - plugin=plugin_obj, - ), - ) - - # Add route to the plugin representation dictionary - route_representation = { - "id": plugin_route_id, - "route": full_view_route, - "links": {}, - } - plugin_representation["routes"].append(route_representation) - - else: - warnings.warn( - "An endpoint /{} has already been loaded. Skipping {}.".format( - full_view_route, view_class - ) - ) + for plugin_view_id, plugin_view in plugin.views.items(): + # Add route to the plugins blueprint + blueprint.add_url_rule( + plugin_view["rule"], + view_func=plugin_view["view"].as_view( + plugin_view_id, + microscope=microscope_obj, + plugin=plugin, + ), + ) + """ # If plugin includes an API form if hasattr(plugin_obj, "api_form") and isinstance( plugin_obj.api_form, dict @@ -152,9 +117,6 @@ def construct_blueprint(microscope_obj): # Store the complete form in Microscope().plugin.form plugin_representation["form"] = api_form_info print(microscope_obj.plugins.forms) + """ - else: - warnings.warn( - "No valid 'api_views' dictionary found in {}".format(plugin_obj) - ) return blueprint diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index b63abe0e..9d1d02d3 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -58,6 +58,8 @@ class ScanPlugin(MicroscopePlugin): api_views = {"/tile": TileScanAPI} def __init__(self): + MicroscopePlugin.__init__(self) + self.images_to_be_captured: int = 1 update_task_data({"images_to_be_captured": self.images_to_be_captured}) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 4aaef28e..2a2eac05 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -4,6 +4,7 @@ import os import inspect import logging +from openflexure_microscope.utilities import camel_to_snake, camel_to_spine from openflexure_microscope.api.views import MicroscopeViewPlugin @@ -83,7 +84,6 @@ def name_from_module(plugin_path): def load_plugin_module(plugin_path): - # If the loader was found (i.e. plugin probably exists) if check_module(plugin_path): try: @@ -140,50 +140,6 @@ def class_from_map(plugin_map): return load_plugin_class(*plugin_arr) -def expand_views(plugin_object, plugin_name): - routes = [] - - # If plugin contains valid endpoints - if hasattr(plugin_object, "api_views") and isinstance(plugin_object.api_views, dict): - - # We'll keep a record of how each route was expanded - expanded_routes = {} - - # For each defined endpoint - for view_route, view_class in plugin_object.api_views.items(): - - # Remove all leading slashes from view route - cleaned_route = view_route - while cleaned_route[0] == "/": - cleaned_route = cleaned_route[1:] - - # Construct a full view route from the plugin name - full_view_route = "/{}/{}".format(plugin_name, cleaned_route) - logging.debug(full_view_route) - - # Record how the view_route got expanded - expanded_routes[view_route] = full_view_route - - # Check if endpoint name clashes - if issubclass(view_class, MicroscopeViewPlugin): - - # Create a Python-safe name for the route - plugin_route_id = "plugin{}".format(full_view_route).replace( - "/", "_" - ) - - # Add route to the plugin representation dictionary - route_representation = { - "id": plugin_route_id, - "route": full_view_route, - "links": {}, - } - - routes.append(route_representation) - - return routes - - class PluginLoader(object): """ A mount-point for all loaded plugins. Attaches to a Microscope object. @@ -195,6 +151,7 @@ class PluginLoader(object): def __init__(self, parent): self.parent = parent self._plugins = [] # List of plugin objects + self._legacy_plugins = {} # DEPRECATED: Dictionary of plugins with old names self.forms = [] # List of plugin forms logging.info("Creating plugin mount") @@ -221,13 +178,15 @@ class PluginLoader(object): if plugin_class is not None: + logging.debug(f"Loading plugin class {plugin_class}, {plugin_name}") + plugin_name_python_safe = plugin_name.replace("/", "_") if plugin_class and plugin_name: plugin_object = plugin_class() if hasattr( - self, plugin_name + self, plugin_name ): # If a plugin with the same name is already attached. logging.warning( ConColors.WARNING @@ -238,24 +197,19 @@ class PluginLoader(object): ) elif isinstance( - plugin_object, MicroscopePlugin + plugin_object, BasePlugin ): # If plugin_object is an instance of MicroscopePlugin # Attach plugin_object to the plugin mount setattr(self, plugin_name_python_safe, plugin_object) # Store the plugin object, and it's properties - self._plugins.append( - { - "name": plugin_name, - "python_name": plugin_name_python_safe, - "plugin": plugin_object, - "routes": [], - "form": None, - } - ) + self._plugins.append(plugin_object) + # DEPRECATED: Store the plugin with it's old name + self._legacy_plugins[plugin_name_python_safe] = plugin_object # Grant plugin access to the hardware - plugin_object.microscope = self.parent + if isinstance(plugin_object, MicroscopePlugin): + plugin_object.microscope = self.parent logging.info( ConColors.OKGREEN @@ -267,7 +221,112 @@ class PluginLoader(object): logging.warning(f"Error loading plugin {plugin_map}. Moving on.") -class MicroscopePlugin: +class BasePlugin: + """ + Parent class for all plugins. + + Handles binding route views and forms. + """ + + def __init__(self): + self._views = {} + self._rules = {} + self._form = None + + # If old api_views dictionary is found + if hasattr(self, "api_views"): + # Convert to new format + self._convert_old_api_views() + # If old api_form dictionary is found + if hasattr(self, "api_form"): + # Convert to new format + self._convert_old_api_form() + + print("\n\n NAME: " + self._name + "\n\n") + print(self.views) + print(self.form) + + @property + def views(self): + return self._views + + def add_view(self, rule, view_class): + # Remove all leading slashes from view route + cleaned_rule = rule + while cleaned_rule[0] == "/": + cleaned_rule = cleaned_rule[1:] + + # Expand the rule to include plugin name + full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule) + + # Create a Python-safe route ID + view_id = cleaned_rule.replace("/", "_") + full_view_id = f"{self._name_python_safe}_{view_id}" + + # Store route information in a dictionary + d = { + "rule": full_rule, + "view": view_class + } + + # Add view to private views dictionary + self._views[full_view_id] = d + # Store the rule expansion information + self._rules[rule] = self._views[full_view_id] + + @property + def form(self): + if not self._form: + return None + + api_form_info = copy.deepcopy(self._form) + api_form_info["id"] = self._name + + if "forms" in api_form_info and isinstance( + api_form_info["forms"], list + ): + for form in api_form_info["forms"]: + if "route" in form and form["route"] in self._rules.keys(): + form["route"] = self._rules[form["route"]]["rule"] + else: + logging.warn( + "No valid expandable route found for {}".format( + form["route"] + ) + ) + return api_form_info + + def set_form(self, form_dictionary: dict): + self._form = form_dictionary + + def _convert_old_api_views(self): + for view_route, view_class in self.api_views.items(): + self.add_view(view_route, view_class) + + def _convert_old_api_form(self): + self.set_form(self.api_form) + + @property + def _name(self): + return self.__class__.__name__ + + @property + def _name_python_safe(self): + return camel_to_snake(self._name) + + @property + def _name_uri_safe(self): + return camel_to_spine(self._name) + + def _full_name(self): + module = self.__class__.__module__ + if module is None or module == str.__class__.__module__: + return self.__class__.__name__ # Avoid reporting __builtin__ + else: + return module + '.' + self.__class__.__name__ + + +class MicroscopePlugin(BasePlugin): """ Parent class for all microscope plugins. @@ -278,6 +337,7 @@ class MicroscopePlugin: api_views = {} # Initially empty dictionary of API views associated with the plugin def __init__(self): + BasePlugin.__init__(self) self.microscope = ( None ) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object diff --git a/openflexure_microscope/plugins/testing/form_example/plugin.py b/openflexure_microscope/plugins/testing/form_example/plugin.py index 42acb932..aabf6880 100644 --- a/openflexure_microscope/plugins/testing/form_example/plugin.py +++ b/openflexure_microscope/plugins/testing/form_example/plugin.py @@ -23,6 +23,8 @@ class ExamplePlugin(MicroscopePlugin): api_views = {"/do": DoAPI, "/task": TaskAPI} def __init__(self): + MicroscopePlugin.__init__(self) + self.val_int = 10 self.val_str = "Hello" self.val_radio = "First" diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 69396fc5..943e54c1 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -1,3 +1,4 @@ +import re import copy import operator from collections import abc @@ -5,6 +6,14 @@ from functools import reduce from contextlib import contextmanager +def camel_to_snake(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +def camel_to_spine(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower() + @contextmanager def set_properties(obj, **kwargs): """A context manager to set, then reset, certain properties of an object. From a3716cf8172e95cceed7f08095e600c838a62a63 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 21:35:32 +0000 Subject: [PATCH 10/27] Tidied up debug statements --- .../api/v1/blueprints/plugins.py | 1 - .../api/v2/blueprints/plugins.py | 32 ------------------- openflexure_microscope/plugins/loader.py | 8 ++--- 3 files changed, 2 insertions(+), 39 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/plugins.py b/openflexure_microscope/api/v1/blueprints/plugins.py index f172b79b..86be7241 100644 --- a/openflexure_microscope/api/v1/blueprints/plugins.py +++ b/openflexure_microscope/api/v1/blueprints/plugins.py @@ -112,7 +112,6 @@ def construct_blueprint(microscope_obj): # Store the complete form in Microscope().plugin.form microscope_obj.plugins.forms.append(api_form_info) - print(microscope_obj.plugins.forms) else: warnings.warn( diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index b4709ce7..cfc94d16 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -21,8 +21,6 @@ def plugins_representation(plugin_loader_object: PluginLoader): """ plugins = [] - print(plugin_loader_object.active) - for plugin in plugin_loader_object.active: logging.info(f"Representing plugin {plugin._name}") d = { @@ -42,12 +40,8 @@ def plugins_representation(plugin_loader_object: PluginLoader): d["views"][view_id] = view_d - print("\n") - print(d) - print("\n") plugins.append(d) - print(plugins) return plugins @@ -93,30 +87,4 @@ def construct_blueprint(microscope_obj): ), ) - """ - # If plugin includes an API form - if hasattr(plugin_obj, "api_form") and isinstance( - plugin_obj.api_form, dict - ): - # TODO: We deep copy this to avoid clashing between API versions. Can be removed when v1 is removed. - api_form_info = copy.deepcopy(plugin_obj.api_form) - api_form_info["id"] = plugin_name - if "forms" in api_form_info and isinstance( - api_form_info["forms"], list - ): - for form in api_form_info["forms"]: - if "route" in form and form["route"] in expanded_routes.keys(): - form["route"] = expanded_routes[form["route"]] - else: - logging.warn( - "No valid expandable route found for {}".format( - form["route"] - ) - ) - - # Store the complete form in Microscope().plugin.form - plugin_representation["form"] = api_form_info - print(microscope_obj.plugins.forms) - """ - return blueprint diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 2a2eac05..b09ba839 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -229,8 +229,8 @@ class BasePlugin: """ def __init__(self): - self._views = {} - self._rules = {} + self._views = {} # Key: Full, Python-safe ID. Val: Original rule, and view class + self._rules = {} # Key: Original rule. Val: View class self._form = None # If old api_views dictionary is found @@ -242,10 +242,6 @@ class BasePlugin: # Convert to new format self._convert_old_api_form() - print("\n\n NAME: " + self._name + "\n\n") - print(self.views) - print(self.form) - @property def views(self): return self._views From 83391f7edf12c73695fbf743ffdc1898741a0c09 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 21:40:20 +0000 Subject: [PATCH 11/27] Fixed PIL debug spam --- openflexure_microscope/api/app.py | 2 +- openflexure_microscope/api/v2/blueprints/plugins.py | 4 ++-- openflexure_microscope/camera/mock.py | 7 +++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 5c558afe..83264dba 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -45,7 +45,7 @@ if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn print("Letting sys handle logs") logger = logging.getLogger() - logger.setLevel(logging.INFO) + logger.setLevel(logging.DEBUG) else: # Direct standard Python logging to file and console root = logging.getLogger() diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index cfc94d16..f6bb1db2 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -22,7 +22,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): plugins = [] for plugin in plugin_loader_object.active: - logging.info(f"Representing plugin {plugin._name}") + logging.debug(f"Representing plugin {plugin._name}") d = { "name": plugin._name, "plugin": str(plugin), @@ -31,7 +31,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): } for view_id, view in plugin.views.items(): - logging.info(f"Representing view {view_id}") + logging.debug(f"Representing view {view_id}") uri = url_for(f"v2_plugins_blueprint.{view_id}") # Make links dictionary if it doesn't yet exist view_d = { diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index 1d252cdf..2769f6a6 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -19,6 +19,13 @@ from typing import Tuple from openflexure_microscope.camera.base import BaseCamera +""" +PIL spams the logger with debug-level information. This is a pain when debugging api.app. +We override the logging settings in api.app by setting a level for PIL here. +""" +pil_logger = logging.getLogger('PIL') +pil_logger.setLevel(logging.INFO) + # MAIN CLASS class MockStreamer(BaseCamera): def __init__(self): From 0cb678beedafcc07f8b9b23b1cfd1461ca21c333 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 21:47:11 +0000 Subject: [PATCH 12/27] Use plugin name as JSON object keys --- openflexure_microscope/api/v2/blueprints/plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index f6bb1db2..490619bf 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -19,12 +19,12 @@ def plugins_representation(plugin_loader_object: PluginLoader): Returns: dict: Dictionary representation of all plugins """ - plugins = [] + plugins = {} for plugin in plugin_loader_object.active: logging.debug(f"Representing plugin {plugin._name}") d = { - "name": plugin._name, + "python_name": plugin._name_python_safe, "plugin": str(plugin), "views": {}, "form": plugin.form, @@ -40,7 +40,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): d["views"][view_id] = view_d - plugins.append(d) + plugins[plugin._name] = d return plugins From eb321220c66a2e5000da55cfdf4a26c4b6c8d379 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 21:47:23 +0000 Subject: [PATCH 13/27] Don't prefix view IDs with plugin name --- openflexure_microscope/plugins/loader.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index b09ba839..b1d12c89 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -201,7 +201,6 @@ class PluginLoader(object): ): # If plugin_object is an instance of MicroscopePlugin # Attach plugin_object to the plugin mount setattr(self, plugin_name_python_safe, plugin_object) - # Store the plugin object, and it's properties self._plugins.append(plugin_object) # DEPRECATED: Store the plugin with it's old name @@ -213,7 +212,7 @@ class PluginLoader(object): logging.info( ConColors.OKGREEN - + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + + "Plugin {} loaded as {}.".format(plugin_map, plugin_object._name) + ConColors.ENDC ) @@ -257,7 +256,6 @@ class BasePlugin: # Create a Python-safe route ID view_id = cleaned_rule.replace("/", "_") - full_view_id = f"{self._name_python_safe}_{view_id}" # Store route information in a dictionary d = { @@ -266,9 +264,9 @@ class BasePlugin: } # Add view to private views dictionary - self._views[full_view_id] = d + self._views[view_id] = d # Store the rule expansion information - self._rules[rule] = self._views[full_view_id] + self._rules[rule] = self._views[view_id] @property def form(self): From c2d27e8f565a471dfe00e3c6365f640ebd889eab Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 22:31:57 +0000 Subject: [PATCH 14/27] Added a description field using docstrings --- openflexure_microscope/api/v2/blueprints/plugins.py | 4 +++- openflexure_microscope/plugins/default/autofocus/api.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 490619bf..d15c7376 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -28,13 +28,15 @@ def plugins_representation(plugin_loader_object: PluginLoader): "plugin": str(plugin), "views": {}, "form": plugin.form, + "description": plugin.__doc__.strip() if plugin.__doc__ else "" } - for view_id, view in plugin.views.items(): + for view_id, view_data in plugin.views.items(): logging.debug(f"Representing view {view_id}") uri = url_for(f"v2_plugins_blueprint.{view_id}") # Make links dictionary if it doesn't yet exist view_d = { + "description": view_data["view"].__doc__.strip() if view_data["view"].__doc__ else "", "links": {"self": uri} } diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index 0e7ed816..ee28abb1 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -18,6 +18,9 @@ class MeasureSharpnessAPI(MicroscopeViewPlugin): class AutofocusAPI(MicroscopeViewPlugin): + """ + Run a standard autofocus + """ def post(self): payload = JsonResponse(request) @@ -36,6 +39,9 @@ class AutofocusAPI(MicroscopeViewPlugin): class FastAutofocusAPI(MicroscopeViewPlugin): + """ + Run a fast autofocus + """ def post(self): payload = JsonResponse(request) From 347bd2207add358db8373ddb7750e4522690da31 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 22:46:50 +0000 Subject: [PATCH 15/27] Get action descriptions from docstrings --- .../api/v2/blueprints/actions/__init__.py | 10 +++------- .../api/v2/blueprints/actions/camera.py | 18 ++++++++++++++++++ .../api/v2/blueprints/actions/stage.py | 6 ++++++ .../api/v2/blueprints/actions/system.py | 12 ++++++++++++ .../api/v2/blueprints/plugins.py | 5 +++-- openflexure_microscope/utilities.py | 6 ++++++ 6 files changed, 48 insertions(+), 9 deletions(-) diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index f16a41e3..47b386d9 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -1,45 +1,41 @@ from flask import Blueprint, url_for, jsonify from sys import platform +from openflexure_microscope.utilities import get_docstring from openflexure_microscope.api.views import MicroscopeView from . import camera, stage, system + _actions = { "capture": { "rule": "/camera/capture/", "view_class": camera.CaptureAPI, - "description": "Take a single still capture", "conditions": True, }, "preview_start": { "rule": "/camera/preview/start", "view_class": camera.GPUPreviewStartAPI, - "description": "Start the on-board GPU preview", "conditions": True, }, "preview_stop": { "rule": "/camera/preview/stop", "view_class": camera.GPUPreviewStopAPI, - "description": "Stop the on-board GPU preview", "conditions": True, }, "move": { "rule": "/stage/move/", "view_class": stage.MoveStageAPI, - "description": "Move the microscope stage", "conditions": True, }, "shutdown": { "rule": "/system/shutdown/", "view_class": system.ShutdownAPI, - "description": "Shutdown the device", "conditions": (platform == "linux"), }, "reboot": { "rule": "/system/reboot/", "view_class": system.RebootAPI, - "description": "Reboot the device", "conditions": (platform == "linux"), }, } @@ -57,7 +53,7 @@ def actions_representation(): for name, action in enabled_actions().items(): d = { "links": {"self": url_for(f".{name}")}, - "description": action["description"], + "description": get_docstring(action["view_class"]), "rule": action["rule"], "view_class": str(action["view_class"]), } diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py index 82a490ee..80440e39 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ b/openflexure_microscope/api/v2/blueprints/actions/camera.py @@ -7,6 +7,12 @@ from flask import jsonify, request, abort, url_for, redirect, send_file class CaptureAPI(MicroscopeView): + """ + Create a new image capture. + + Allowed methods: + POST + """ def post(self): """ Create a new image capture. @@ -101,6 +107,12 @@ class CaptureAPI(MicroscopeView): class GPUPreviewStartAPI(MicroscopeView): + """ + Start the onboard GPU preview. + + Allowed methods: + POST + """ def post(self, operation): """ Start the onboard GPU preview. @@ -143,6 +155,12 @@ class GPUPreviewStartAPI(MicroscopeView): class GPUPreviewStopAPI(MicroscopeView): + """ + Stop the onboard GPU preview. + + Allowed methods: + POST + """ def post(self, operation): """ Stop the onboard GPU preview. diff --git a/openflexure_microscope/api/v2/blueprints/actions/stage.py b/openflexure_microscope/api/v2/blueprints/actions/stage.py index c020091b..2afdd6b7 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/stage.py +++ b/openflexure_microscope/api/v2/blueprints/actions/stage.py @@ -8,6 +8,12 @@ import logging class MoveStageAPI(MicroscopeView): + """ + Handle stage movements. + + Allowed methods: + POST + """ def post(self): """ Set x, y and z positions of the stage. diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/blueprints/actions/system.py index ab36911b..a82aeaec 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/blueprints/actions/system.py @@ -4,6 +4,12 @@ import subprocess class ShutdownAPI(MicroscopeView): + """ + Attempt to shutdown the device + + Allowed methods: + POST + """ def post(self): """ Attempt to shutdown the device @@ -17,6 +23,12 @@ class ShutdownAPI(MicroscopeView): class RebootAPI(MicroscopeView): + """ + Attempt to reboot the device + + Allowed methods: + POST + """ def post(self): """ Attempt to shutdown the device diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index d15c7376..70e80e49 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -1,5 +1,6 @@ from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin from openflexure_microscope.api.views import MicroscopeViewPlugin +from openflexure_microscope.utilities import get_docstring from flask import Blueprint, jsonify, url_for from openflexure_microscope.api.views import MicroscopeView @@ -28,7 +29,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): "plugin": str(plugin), "views": {}, "form": plugin.form, - "description": plugin.__doc__.strip() if plugin.__doc__ else "" + "description": get_docstring(plugin) } for view_id, view_data in plugin.views.items(): @@ -36,7 +37,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): uri = url_for(f"v2_plugins_blueprint.{view_id}") # Make links dictionary if it doesn't yet exist view_d = { - "description": view_data["view"].__doc__.strip() if view_data["view"].__doc__ else "", + "description": get_docstring(view_data["view"]), "links": {"self": uri} } diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 943e54c1..c46494c4 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -5,6 +5,12 @@ from collections import abc from functools import reduce from contextlib import contextmanager +def get_docstring(obj): + ds = obj.__doc__ + if ds: + return ds.strip() + else: + return "" def camel_to_snake(name): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) From 21451b14b3e7f085eeab518cf38394cb93c87941 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 22:48:24 +0000 Subject: [PATCH 16/27] Todo note --- openflexure_microscope/api/v2/blueprints/actions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index 47b386d9..3d275734 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -6,7 +6,7 @@ from openflexure_microscope.api.views import MicroscopeView from . import camera, stage, system - +# TODO: Could allowed methods be calculated automatically by looking at what methods exist? _actions = { "capture": { "rule": "/camera/capture/", From d3f2807bb974524eab10ab6be6e107acde2593c0 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 12:08:13 +0000 Subject: [PATCH 17/27] Renamed form to GUI --- openflexure_microscope/plugins/loader.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index b1d12c89..5e7da427 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -230,7 +230,7 @@ class BasePlugin: def __init__(self): self._views = {} # Key: Full, Python-safe ID. Val: Original rule, and view class self._rules = {} # Key: Original rule. Val: View class - self._form = None + self._gui = None # If old api_views dictionary is found if hasattr(self, "api_views"): @@ -269,17 +269,17 @@ class BasePlugin: self._rules[rule] = self._views[view_id] @property - def form(self): - if not self._form: + def gui(self): + if not self._gui: return None - api_form_info = copy.deepcopy(self._form) - api_form_info["id"] = self._name + api_gui = copy.deepcopy(self._gui) + api_gui["id"] = self._name - if "forms" in api_form_info and isinstance( - api_form_info["forms"], list + if "forms" in api_gui and isinstance( + api_gui["forms"], list ): - for form in api_form_info["forms"]: + for form in api_gui["forms"]: if "route" in form and form["route"] in self._rules.keys(): form["route"] = self._rules[form["route"]]["rule"] else: @@ -288,17 +288,17 @@ class BasePlugin: form["route"] ) ) - return api_form_info + return api_gui - def set_form(self, form_dictionary: dict): - self._form = form_dictionary + def set_gui(self, form_dictionary: dict): + self._gui = form_dictionary def _convert_old_api_views(self): for view_route, view_class in self.api_views.items(): self.add_view(view_route, view_class) def _convert_old_api_form(self): - self.set_form(self.api_form) + self.set_gui(self.api_form) @property def _name(self): From 9b387985e3b3a4af43b2c27066d3cc533db00ad2 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 12:08:25 +0000 Subject: [PATCH 18/27] Use more docstrings as descriptions --- openflexure_microscope/api/utilities.py | 8 +++- .../api/v2/blueprints/actions/__init__.py | 13 +++-- .../api/v2/blueprints/actions/camera.py | 9 ---- .../api/v2/blueprints/actions/stage.py | 3 -- .../api/v2/blueprints/actions/system.py | 6 --- .../api/v2/blueprints/captures.py | 8 +++- .../api/v2/blueprints/plugins.py | 17 ++++--- .../api/v2/blueprints/root.py | 48 +++++++++---------- .../api/v2/blueprints/settings.py | 7 ++- .../api/v2/blueprints/status.py | 8 +++- .../api/v2/blueprints/stream.py | 3 +- openflexure_microscope/utilities.py | 17 +++++++ 12 files changed, 88 insertions(+), 59 deletions(-) diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index 60d490db..ce8c1b1d 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -1,7 +1,13 @@ import logging from werkzeug.exceptions import BadRequest -from flask import url_for +from flask import url_for, Blueprint +def blueprint_for_module(module_name, api_ver=2, suffix=""): + return Blueprint(blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix), module_name) + +def blueprint_name_for_module(module_name, api_ver=2, suffix=""): + bp_name = module_name.split('.')[-1] + return f"v{api_ver}_{bp_name}_blueprint{suffix}" class JsonResponse: def __init__(self, request): diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index 3d275734..e78f9d85 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -1,12 +1,16 @@ +""" +Top-level representation of enabled actions +""" + from flask import Blueprint, url_for, jsonify from sys import platform -from openflexure_microscope.utilities import get_docstring +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 -# TODO: Could allowed methods be calculated automatically by looking at what methods exist? _actions = { "capture": { "rule": "/camera/capture/", @@ -53,11 +57,12 @@ def actions_representation(): for name, action in enabled_actions().items(): d = { "links": {"self": url_for(f".{name}")}, - "description": get_docstring(action["view_class"]), "rule": action["rule"], "view_class": str(action["view_class"]), } + d.update(description_from_view(action["view_class"])) + actions[name] = d return actions @@ -69,7 +74,7 @@ class ActionsAPI(MicroscopeView): def construct_blueprint(microscope_obj): global _actions - blueprint = Blueprint("v2_actions_blueprint", __name__) + blueprint = blueprint_for_module(__name__) # For each enabled action route defined in our dictionary above for name, action in enabled_actions().items(): diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py index 80440e39..5e199eb1 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ b/openflexure_microscope/api/v2/blueprints/actions/camera.py @@ -9,9 +9,6 @@ from flask import jsonify, request, abort, url_for, redirect, send_file class CaptureAPI(MicroscopeView): """ Create a new image capture. - - Allowed methods: - POST """ def post(self): """ @@ -109,9 +106,6 @@ class CaptureAPI(MicroscopeView): class GPUPreviewStartAPI(MicroscopeView): """ Start the onboard GPU preview. - - Allowed methods: - POST """ def post(self, operation): """ @@ -157,9 +151,6 @@ class GPUPreviewStartAPI(MicroscopeView): class GPUPreviewStopAPI(MicroscopeView): """ Stop the onboard GPU preview. - - Allowed methods: - POST """ def post(self, operation): """ diff --git a/openflexure_microscope/api/v2/blueprints/actions/stage.py b/openflexure_microscope/api/v2/blueprints/actions/stage.py index 2afdd6b7..3294011d 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/stage.py +++ b/openflexure_microscope/api/v2/blueprints/actions/stage.py @@ -10,9 +10,6 @@ import logging class MoveStageAPI(MicroscopeView): """ Handle stage movements. - - Allowed methods: - POST """ def post(self): """ diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/blueprints/actions/system.py index a82aeaec..596144d6 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/blueprints/actions/system.py @@ -6,9 +6,6 @@ import subprocess class ShutdownAPI(MicroscopeView): """ Attempt to shutdown the device - - Allowed methods: - POST """ def post(self): """ @@ -25,9 +22,6 @@ class ShutdownAPI(MicroscopeView): class RebootAPI(MicroscopeView): """ Attempt to reboot the device - - Allowed methods: - POST """ def post(self): """ diff --git a/openflexure_microscope/api/v2/blueprints/captures.py b/openflexure_microscope/api/v2/blueprints/captures.py index 0a54e69a..e50ce2e2 100644 --- a/openflexure_microscope/api/v2/blueprints/captures.py +++ b/openflexure_microscope/api/v2/blueprints/captures.py @@ -1,6 +1,11 @@ +""" +Top-level representation of all acquired captures +""" + from openflexure_microscope.api.utilities import get_bool, JsonResponse from openflexure_microscope.api.views import MicroscopeView from openflexure_microscope.utilities import filter_dict +from openflexure_microscope.api.utilities import blueprint_for_module from flask import jsonify, request, abort, url_for, redirect, send_file, Blueprint @@ -378,7 +383,8 @@ class TagsAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("v2_captures_blueprint", __name__) + + blueprint = blueprint_for_module(__name__) # Tag routes blueprint.add_url_rule( diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 70e80e49..5b5b8ff6 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -1,6 +1,11 @@ +""" +Top-level representation of attached and enabled plugins +""" + from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin from openflexure_microscope.api.views import MicroscopeViewPlugin -from openflexure_microscope.utilities import get_docstring +from openflexure_microscope.utilities import get_docstring, description_from_view +from openflexure_microscope.api.utilities import blueprint_for_module from flask import Blueprint, jsonify, url_for from openflexure_microscope.api.views import MicroscopeView @@ -9,7 +14,6 @@ import copy import logging import warnings - def plugins_representation(plugin_loader_object: PluginLoader): """ Generate a dictionary representation of all plugins, including Flask route URLs @@ -28,7 +32,7 @@ def plugins_representation(plugin_loader_object: PluginLoader): "python_name": plugin._name_python_safe, "plugin": str(plugin), "views": {}, - "form": plugin.form, + "gui": plugin.gui, "description": get_docstring(plugin) } @@ -37,10 +41,11 @@ def plugins_representation(plugin_loader_object: PluginLoader): uri = url_for(f"v2_plugins_blueprint.{view_id}") # Make links dictionary if it doesn't yet exist view_d = { - "description": get_docstring(view_data["view"]), "links": {"self": uri} } + view_d.update(description_from_view(view_data["view"])) + d["views"][view_id] = view_d plugins[plugin._name] = d @@ -67,15 +72,13 @@ class PluginFormAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("v2_plugins_blueprint", __name__) + blueprint = blueprint_for_module(__name__) # Create a base route to return plugin API forms, if any exist blueprint.add_url_rule( "/", view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj) ) - all_routes = [] - # For each plugin attached to the microscope object for plugin in microscope_obj.plugins.active: diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py index 6106ad86..17b2773c 100644 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ b/openflexure_microscope/api/v2/blueprints/root.py @@ -2,29 +2,29 @@ from flask import Blueprint, jsonify, url_for from openflexure_microscope import Microscope from openflexure_microscope.api.views import MicroscopeView -def root_representation(microscope_obj: Microscope): - d = { - "settings": { - "description": "Writeable settings for the microscope, and attached hardware", - "links": {"self": url_for("v2_settings_blueprint.settings")} - }, - "status": { - "description": "Read-only status of the microscope, and attached hardware info", - "links": {"self": url_for("v2_status_blueprint.status")} - }, - "plugins": { - "description": "Top-level representation of attached and enabled plugins", - "links": {"self": url_for("v2_plugins_blueprint.plugins")} - }, - "captures": { - "description": "Top-level representation of all acquired captures", - "links": {"self": url_for("v2_captures_blueprint.captures")} - }, - "actions": { - "description": "Top-level representation of enabled actions", - "links": {"self": url_for("v2_actions_blueprint.actions")} - }, - } +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, plugins, captures, actions, stream + +# List of submodules containing create_blueprint methods using standard blueprint_for_module naming +_root_blueprint_modules = [settings, status, plugins, captures, actions, stream] + +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 @@ -32,7 +32,7 @@ def root_representation(microscope_obj: Microscope): class RootAPI(MicroscopeView): def get(self): - return jsonify(root_representation(self.microscope)) + return jsonify(root_representation()) def construct_blueprint(microscope_obj): diff --git a/openflexure_microscope/api/v2/blueprints/settings.py b/openflexure_microscope/api/v2/blueprints/settings.py index 972785d3..4d14fd13 100644 --- a/openflexure_microscope/api/v2/blueprints/settings.py +++ b/openflexure_microscope/api/v2/blueprints/settings.py @@ -1,5 +1,10 @@ +""" +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 flask import Blueprint, jsonify, request import logging @@ -144,7 +149,7 @@ class SettingsAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("v2_settings_blueprint", __name__) + blueprint = blueprint_for_module(__name__) blueprint.add_url_rule( "/", view_func=SettingsAPI.as_view("settings", microscope=microscope_obj) diff --git a/openflexure_microscope/api/v2/blueprints/status.py b/openflexure_microscope/api/v2/blueprints/status.py index 48e59602..ac63d35a 100644 --- a/openflexure_microscope/api/v2/blueprints/status.py +++ b/openflexure_microscope/api/v2/blueprints/status.py @@ -1,8 +1,12 @@ +""" +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 flask import Blueprint, jsonify - class StatusAPI(MicroscopeView): def get(self): """ @@ -55,7 +59,7 @@ class StatusAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("v2_status_blueprint", __name__) + blueprint = blueprint_for_module(__name__) blueprint.add_url_rule( "/", view_func=StatusAPI.as_view("status", microscope=microscope_obj) diff --git a/openflexure_microscope/api/v2/blueprints/stream.py b/openflexure_microscope/api/v2/blueprints/stream.py index a6f40a42..a79d2419 100644 --- a/openflexure_microscope/api/v2/blueprints/stream.py +++ b/openflexure_microscope/api/v2/blueprints/stream.py @@ -1,5 +1,6 @@ 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 flask import Response, Blueprint, jsonify, request @@ -43,7 +44,7 @@ class SnapshotAPI(MicroscopeView): def construct_blueprint(microscope_obj): - blueprint = Blueprint("v2_stream_blueprint", __name__) + blueprint = blueprint_for_module(__name__) blueprint.add_url_rule( "/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj) diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index c46494c4..fa194fc1 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -5,6 +5,23 @@ from collections import abc from functools import reduce from contextlib import contextmanager +def bottom_level_name(obj): + return obj.__name__.split('.')[-1] + +def description_from_view(view_class): + methods = [] + for method_key in ["get", "post", "put", "delete"]: + if hasattr(view_class, method_key): + methods.append(method_key.upper()) + brief_description = get_docstring(view_class).partition('\n')[0].strip() + + d = { + "methods": methods, + "description": brief_description + } + + return d + def get_docstring(obj): ds = obj.__doc__ if ds: From 8c5af3486cd80deea06d174a203f1e3c39291d78 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 13:15:25 +0000 Subject: [PATCH 19/27] Changed keys to be more JavaScript friendly --- openflexure_microscope/api/v2/blueprints/actions/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index e78f9d85..0f3faa19 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -17,12 +17,12 @@ _actions = { "view_class": camera.CaptureAPI, "conditions": True, }, - "preview_start": { + "previewStart": { "rule": "/camera/preview/start", "view_class": camera.GPUPreviewStartAPI, "conditions": True, }, - "preview_stop": { + "previewStop": { "rule": "/camera/preview/stop", "view_class": camera.GPUPreviewStopAPI, "conditions": True, From 0b2536a79bd453fa420cdb29815b305fc91c96df Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 13:48:27 +0000 Subject: [PATCH 20/27] Reorganised stream resource --- openflexure_microscope/api/app.py | 4 +- .../api/v2/blueprints/__init__.py | 2 +- .../api/v2/blueprints/actions/__init__.py | 8 +- .../api/v2/blueprints/root.py | 4 +- .../api/v2/blueprints/stream.py | 58 --------- .../api/v2/blueprints/streams.py | 112 ++++++++++++++++++ 6 files changed, 122 insertions(+), 66 deletions(-) delete mode 100644 openflexure_microscope/api/v2/blueprints/stream.py create mode 100644 openflexure_microscope/api/v2/blueprints/streams.py diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 83264dba..c3db5f89 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -148,8 +148,8 @@ app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope) app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2")) -v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope) -app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2")) +v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope) +app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2")) # Captures routes v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope) diff --git a/openflexure_microscope/api/v2/blueprints/__init__.py b/openflexure_microscope/api/v2/blueprints/__init__.py index 473540f2..0ce1ebae 100644 --- a/openflexure_microscope/api/v2/blueprints/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/__init__.py @@ -1 +1 @@ -from . import root, captures, settings, status, tasks, stream, plugins, actions +from . import root, captures, settings, status, tasks, streams, plugins, actions diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index 0f3faa19..e7cd7c67 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -11,6 +11,11 @@ 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/", @@ -67,9 +72,6 @@ def actions_representation(): return actions -class ActionsAPI(MicroscopeView): - def get(self): - return jsonify(actions_representation()) def construct_blueprint(microscope_obj): global _actions diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py index 17b2773c..e005e7e7 100644 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ b/openflexure_microscope/api/v2/blueprints/root.py @@ -4,10 +4,10 @@ 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, plugins, captures, actions, stream +from openflexure_microscope.api.v2.blueprints import settings, status, plugins, captures, actions, streams # List of submodules containing create_blueprint methods using standard blueprint_for_module naming -_root_blueprint_modules = [settings, status, plugins, captures, actions, stream] +_root_blueprint_modules = [settings, status, plugins, captures, actions, streams] def root_representation(): """ diff --git a/openflexure_microscope/api/v2/blueprints/stream.py b/openflexure_microscope/api/v2/blueprints/stream.py deleted file mode 100644 index a79d2419..00000000 --- a/openflexure_microscope/api/v2/blueprints/stream.py +++ /dev/null @@ -1,58 +0,0 @@ -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 flask import Response, Blueprint, jsonify, request - - -class StreamAPI(MicroscopeView): - def get(self): - """ - Real-time MJPEG stream from the microscope camera - - .. :quickref: Stream; 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): - def get(self): - """ - Single snapshot from the camera stream - - .. :quickref: Stream; 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") - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - blueprint.add_url_rule( - "/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj) - ) - - blueprint.add_url_rule( - "/snapshot", - view_func=SnapshotAPI.as_view("snapshot", microscope=microscope_obj), - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/streams.py b/openflexure_microscope/api/v2/blueprints/streams.py new file mode 100644 index 00000000..e727c6e3 --- /dev/null +++ b/openflexure_microscope/api/v2/blueprints/streams.py @@ -0,0 +1,112 @@ +""" +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: Stream; 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: Stream; 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 From 36f195d6e74498fe4eae74c23c3053e2b8d378e6 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 14:11:28 +0000 Subject: [PATCH 21/27] Fixed legacy plugin state --- openflexure_microscope/plugins/loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 5e7da427..ad1e4207 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -161,7 +161,7 @@ class PluginLoader(object): logging.warning( "PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version." ) - return [m["python_name"] for m in self._plugins] + return list(self._legacy_plugins.keys()) @property def active(self): From b430a2d34a5bee48b202c8cbe3e4e400fc7c710a Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 14:13:01 +0000 Subject: [PATCH 22/27] Blackened --- openflexure_microscope/api/utilities.py | 10 +++++-- .../api/v2/blueprints/actions/__init__.py | 1 + .../api/v2/blueprints/actions/camera.py | 3 +++ .../api/v2/blueprints/actions/stage.py | 1 + .../api/v2/blueprints/actions/system.py | 2 ++ .../api/v2/blueprints/captures.py | 2 +- .../api/v2/blueprints/plugins.py | 11 +++----- .../api/v2/blueprints/root.py | 12 +++++++-- .../api/v2/blueprints/status.py | 1 + .../api/v2/blueprints/streams.py | 23 +++++++--------- openflexure_microscope/camera/mock.py | 2 +- .../plugins/default/autofocus/api.py | 2 ++ openflexure_microscope/plugins/loader.py | 27 +++++++++---------- .../extensible_serial_instrument.py | 6 +++-- openflexure_microscope/utilities.py | 23 +++++++++------- 15 files changed, 72 insertions(+), 54 deletions(-) diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index ce8c1b1d..32a854e5 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -2,13 +2,19 @@ import logging from werkzeug.exceptions import BadRequest from flask import url_for, Blueprint + def blueprint_for_module(module_name, api_ver=2, suffix=""): - return Blueprint(blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix), module_name) + return Blueprint( + blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix), + module_name, + ) + def blueprint_name_for_module(module_name, api_ver=2, suffix=""): - bp_name = module_name.split('.')[-1] + bp_name = module_name.split(".")[-1] return f"v{api_ver}_{bp_name}_blueprint{suffix}" + class JsonResponse: def __init__(self, request): """ diff --git a/openflexure_microscope/api/v2/blueprints/actions/__init__.py b/openflexure_microscope/api/v2/blueprints/actions/__init__.py index e7cd7c67..32f2c061 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/__init__.py +++ b/openflexure_microscope/api/v2/blueprints/actions/__init__.py @@ -11,6 +11,7 @@ from openflexure_microscope.api.views import MicroscopeView from . import camera, stage, system + class ActionsAPI(MicroscopeView): def get(self): return jsonify(actions_representation()) diff --git a/openflexure_microscope/api/v2/blueprints/actions/camera.py b/openflexure_microscope/api/v2/blueprints/actions/camera.py index 5e199eb1..198dc1ba 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/camera.py +++ b/openflexure_microscope/api/v2/blueprints/actions/camera.py @@ -10,6 +10,7 @@ class CaptureAPI(MicroscopeView): """ Create a new image capture. """ + def post(self): """ Create a new image capture. @@ -107,6 +108,7 @@ class GPUPreviewStartAPI(MicroscopeView): """ Start the onboard GPU preview. """ + def post(self, operation): """ Start the onboard GPU preview. @@ -152,6 +154,7 @@ class GPUPreviewStopAPI(MicroscopeView): """ Stop the onboard GPU preview. """ + def post(self, operation): """ Stop the onboard GPU preview. diff --git a/openflexure_microscope/api/v2/blueprints/actions/stage.py b/openflexure_microscope/api/v2/blueprints/actions/stage.py index 3294011d..7e8af1d3 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/stage.py +++ b/openflexure_microscope/api/v2/blueprints/actions/stage.py @@ -11,6 +11,7 @@ class MoveStageAPI(MicroscopeView): """ Handle stage movements. """ + def post(self): """ Set x, y and z positions of the stage. diff --git a/openflexure_microscope/api/v2/blueprints/actions/system.py b/openflexure_microscope/api/v2/blueprints/actions/system.py index 596144d6..1142b700 100644 --- a/openflexure_microscope/api/v2/blueprints/actions/system.py +++ b/openflexure_microscope/api/v2/blueprints/actions/system.py @@ -7,6 +7,7 @@ class ShutdownAPI(MicroscopeView): """ Attempt to shutdown the device """ + def post(self): """ Attempt to shutdown the device @@ -23,6 +24,7 @@ class RebootAPI(MicroscopeView): """ Attempt to reboot the device """ + def post(self): """ Attempt to shutdown the device diff --git a/openflexure_microscope/api/v2/blueprints/captures.py b/openflexure_microscope/api/v2/blueprints/captures.py index e50ce2e2..4fac46f2 100644 --- a/openflexure_microscope/api/v2/blueprints/captures.py +++ b/openflexure_microscope/api/v2/blueprints/captures.py @@ -383,7 +383,7 @@ class TagsAPI(MicroscopeView): def construct_blueprint(microscope_obj): - + blueprint = blueprint_for_module(__name__) # Tag routes diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 5b5b8ff6..644a33aa 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -14,6 +14,7 @@ import copy import logging import warnings + def plugins_representation(plugin_loader_object: PluginLoader): """ Generate a dictionary representation of all plugins, including Flask route URLs @@ -33,16 +34,14 @@ def plugins_representation(plugin_loader_object: PluginLoader): "plugin": str(plugin), "views": {}, "gui": plugin.gui, - "description": get_docstring(plugin) + "description": get_docstring(plugin), } for view_id, view_data in plugin.views.items(): logging.debug(f"Representing view {view_id}") uri = url_for(f"v2_plugins_blueprint.{view_id}") # Make links dictionary if it doesn't yet exist - view_d = { - "links": {"self": uri} - } + view_d = {"links": {"self": uri}} view_d.update(description_from_view(view_data["view"])) @@ -87,9 +86,7 @@ def construct_blueprint(microscope_obj): blueprint.add_url_rule( plugin_view["rule"], view_func=plugin_view["view"].as_view( - plugin_view_id, - microscope=microscope_obj, - plugin=plugin, + plugin_view_id, microscope=microscope_obj, plugin=plugin ), ) diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py index e005e7e7..bdaf348b 100644 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ b/openflexure_microscope/api/v2/blueprints/root.py @@ -4,11 +4,19 @@ 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, plugins, captures, actions, streams +from openflexure_microscope.api.v2.blueprints import ( + settings, + status, + plugins, + captures, + actions, + streams, +) # List of submodules containing create_blueprint methods using standard blueprint_for_module naming _root_blueprint_modules = [settings, status, plugins, captures, actions, streams] + def root_representation(): """ Generate a dictionar representation of all top-level blueprint rules @@ -23,7 +31,7 @@ def root_representation(): d[module_short_name] = { "name": blueprint_module.__name__, "description": get_docstring(blueprint_module), - "links": {"self": url_for(f"{blueprint_name}.{module_short_name}")} + "links": {"self": url_for(f"{blueprint_name}.{module_short_name}")}, } return d diff --git a/openflexure_microscope/api/v2/blueprints/status.py b/openflexure_microscope/api/v2/blueprints/status.py index ac63d35a..6344cb42 100644 --- a/openflexure_microscope/api/v2/blueprints/status.py +++ b/openflexure_microscope/api/v2/blueprints/status.py @@ -7,6 +7,7 @@ from openflexure_microscope.api.utilities import blueprint_for_module from flask import Blueprint, jsonify + class StatusAPI(MicroscopeView): def get(self): """ diff --git a/openflexure_microscope/api/v2/blueprints/streams.py b/openflexure_microscope/api/v2/blueprints/streams.py index e727c6e3..e11bb3df 100644 --- a/openflexure_microscope/api/v2/blueprints/streams.py +++ b/openflexure_microscope/api/v2/blueprints/streams.py @@ -4,7 +4,10 @@ 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.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 @@ -19,6 +22,7 @@ class MjpegAPI(MicroscopeView): """ Real-time MJPEG stream from the microscope camera """ + def get(self): """ Real-time MJPEG stream from the microscope camera @@ -42,6 +46,7 @@ class SnapshotAPI(MicroscopeView): """ Single JPEG snapshot from the camera stream """ + def get(self): """ Single snapshot from the camera stream @@ -59,16 +64,8 @@ class SnapshotAPI(MicroscopeView): _streams = { - "mjpeg": { - "rule": "/mjpeg", - "view_class": MjpegAPI, - "conditions": True, - }, - "snapshot": { - "rule": "/snapshot", - "view_class": SnapshotAPI, - "conditions": True, - } + "mjpeg": {"rule": "/mjpeg", "view_class": MjpegAPI, "conditions": True}, + "snapshot": {"rule": "/snapshot", "view_class": SnapshotAPI, "conditions": True}, } @@ -82,9 +79,7 @@ def streams_representation(): streams = {} for name, stream in enabled_streams().items(): - d = { - "links": {"self": url_for(f".{name}")}, - } + d = {"links": {"self": url_for(f".{name}")}} d.update(description_from_view(stream["view_class"])) diff --git a/openflexure_microscope/camera/mock.py b/openflexure_microscope/camera/mock.py index 2769f6a6..7aca48d8 100644 --- a/openflexure_microscope/camera/mock.py +++ b/openflexure_microscope/camera/mock.py @@ -23,7 +23,7 @@ from openflexure_microscope.camera.base import BaseCamera PIL spams the logger with debug-level information. This is a pain when debugging api.app. We override the logging settings in api.app by setting a level for PIL here. """ -pil_logger = logging.getLogger('PIL') +pil_logger = logging.getLogger("PIL") pil_logger.setLevel(logging.INFO) # MAIN CLASS diff --git a/openflexure_microscope/plugins/default/autofocus/api.py b/openflexure_microscope/plugins/default/autofocus/api.py index ee28abb1..e04bd529 100644 --- a/openflexure_microscope/plugins/default/autofocus/api.py +++ b/openflexure_microscope/plugins/default/autofocus/api.py @@ -21,6 +21,7 @@ class AutofocusAPI(MicroscopeViewPlugin): """ Run a standard autofocus """ + def post(self): payload = JsonResponse(request) @@ -42,6 +43,7 @@ class FastAutofocusAPI(MicroscopeViewPlugin): """ Run a fast autofocus """ + def post(self): payload = JsonResponse(request) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index ad1e4207..0981d822 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -186,7 +186,7 @@ class PluginLoader(object): plugin_object = plugin_class() if hasattr( - self, plugin_name + self, plugin_name ): # If a plugin with the same name is already attached. logging.warning( ConColors.WARNING @@ -197,7 +197,7 @@ class PluginLoader(object): ) elif isinstance( - plugin_object, BasePlugin + plugin_object, BasePlugin ): # If plugin_object is an instance of MicroscopePlugin # Attach plugin_object to the plugin mount setattr(self, plugin_name_python_safe, plugin_object) @@ -212,7 +212,9 @@ class PluginLoader(object): logging.info( ConColors.OKGREEN - + "Plugin {} loaded as {}.".format(plugin_map, plugin_object._name) + + "Plugin {} loaded as {}.".format( + plugin_map, plugin_object._name + ) + ConColors.ENDC ) @@ -228,7 +230,9 @@ class BasePlugin: """ def __init__(self): - self._views = {} # Key: Full, Python-safe ID. Val: Original rule, and view class + self._views = ( + {} + ) # Key: Full, Python-safe ID. Val: Original rule, and view class self._rules = {} # Key: Original rule. Val: View class self._gui = None @@ -258,10 +262,7 @@ class BasePlugin: view_id = cleaned_rule.replace("/", "_") # Store route information in a dictionary - d = { - "rule": full_rule, - "view": view_class - } + d = {"rule": full_rule, "view": view_class} # Add view to private views dictionary self._views[view_id] = d @@ -276,17 +277,13 @@ class BasePlugin: api_gui = copy.deepcopy(self._gui) api_gui["id"] = self._name - if "forms" in api_gui and isinstance( - api_gui["forms"], list - ): + if "forms" in api_gui and isinstance(api_gui["forms"], list): for form in api_gui["forms"]: if "route" in form and form["route"] in self._rules.keys(): form["route"] = self._rules[form["route"]]["rule"] else: logging.warn( - "No valid expandable route found for {}".format( - form["route"] - ) + "No valid expandable route found for {}".format(form["route"]) ) return api_gui @@ -317,7 +314,7 @@ class BasePlugin: if module is None or module == str.__class__.__module__: return self.__class__.__name__ # Avoid reporting __builtin__ else: - return module + '.' + self.__class__.__name__ + return module + "." + self.__class__.__name__ class MicroscopePlugin(BasePlugin): diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index 2427dd06..7524932e 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -213,7 +213,9 @@ class ExtensibleSerialInstrument(object): return self.read_multiline(termination_line) else: logging.debug("Reading response...") - line = self.readline().strip() # question: should we strip the final newline? + line = ( + self.readline().strip() + ) # question: should we strip the final newline? logging.debug(f"Read finished. Got {line}") return line @@ -223,7 +225,7 @@ class ExtensibleSerialInstrument(object): response_string=r"%d", re_flags=0, parse_function=None, - **kwargs + **kwargs, ): """ Perform a query, returning a parsed form of the response. diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index fa194fc1..27e5734d 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -5,23 +5,23 @@ from collections import abc from functools import reduce from contextlib import contextmanager + def bottom_level_name(obj): - return obj.__name__.split('.')[-1] + return obj.__name__.split(".")[-1] + def description_from_view(view_class): methods = [] for method_key in ["get", "post", "put", "delete"]: if hasattr(view_class, method_key): methods.append(method_key.upper()) - brief_description = get_docstring(view_class).partition('\n')[0].strip() + brief_description = get_docstring(view_class).partition("\n")[0].strip() - d = { - "methods": methods, - "description": brief_description - } + d = {"methods": methods, "description": brief_description} return d + def get_docstring(obj): ds = obj.__doc__ if ds: @@ -29,13 +29,16 @@ def get_docstring(obj): else: return "" + def camel_to_snake(name): - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() + def camel_to_spine(name): - s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', name) - return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower() + s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name) + return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower() + @contextmanager def set_properties(obj, **kwargs): From 52fb13037971f7a169abbedf3597bb4042a37b25 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 16:06:34 +0000 Subject: [PATCH 23/27] Re-added timeout arguments --- .../sangaboard/extensible_serial_instrument.py | 15 ++++++++------- .../stage/sangaboard/sangaboard.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py index 7524932e..91c53a7b 100644 --- a/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py +++ b/openflexure_microscope/stage/sangaboard/extensible_serial_instrument.py @@ -143,7 +143,7 @@ class ExtensibleSerialInstrument(object): if self._ser.inWaiting() > 0: self._ser.flushInput() - def readline(self): + def readline(self, timeout=None): """Read one line from the serial port.""" with self.communications_lock: return ( @@ -163,7 +163,7 @@ class ExtensibleSerialInstrument(object): self._communications_lock = threading.RLock() return self._communications_lock - def read_multiline(self, termination_line=None): + def read_multiline(self, termination_line=None, timeout=None): """Read one line from the underlying bus. Must be overriden. This should not need to be reimplemented unless there's a more efficient @@ -179,11 +179,11 @@ class ExtensibleSerialInstrument(object): while ( termination_line not in last_line and len(last_line) > 0 ): # read until we get the termination line. - last_line = self.readline() + last_line = self.readline(timeout) response += last_line return response - def query(self, queryString, multiline=False, termination_line=None): + def query(self, queryString, multiline=False, termination_line=None, timeout=None): """ Write a string to the stage controller and return its response. @@ -198,10 +198,10 @@ class ExtensibleSerialInstrument(object): logging.debug("Query written") if self.ignore_echo == True: # Needs Implementing for a multiline read! logging.debug("Reading first line...") - first_line = self.readline().strip() + first_line = self.readline(timeout).strip() logging.debug(f"Read finished. Got {first_line}") if first_line == queryString: - return self.readline().strip() + return self.readline(timeout).strip() else: logging.info("This command did not echo!!!") return first_line @@ -214,7 +214,7 @@ class ExtensibleSerialInstrument(object): else: logging.debug("Reading response...") line = ( - self.readline().strip() + self.readline(timeout).strip() ) # question: should we strip the final newline? logging.debug(f"Read finished. Got {line}") return line @@ -352,6 +352,7 @@ class ExtensibleSerialInstrument(object): def find_port(self): """Iterate through the available serial ports and query them to see if our instrument is there.""" + print("Auto-scanning ports") with self.communications_lock: success = False for ( diff --git a/openflexure_microscope/stage/sangaboard/sangaboard.py b/openflexure_microscope/stage/sangaboard/sangaboard.py index 9002144c..bd97adbb 100644 --- a/openflexure_microscope/stage/sangaboard/sangaboard.py +++ b/openflexure_microscope/stage/sangaboard/sangaboard.py @@ -102,7 +102,7 @@ class Sangaboard(ExtensibleSerialInstrument): # Initialise basic serial instrument with specified logging.info(f"Initialising ExtensibleSerialInstrument on port {port}") - ExtensibleSerialInstrument.__init__(self, port, timeout=timeout, **kwargs) + ExtensibleSerialInstrument.__init__(self, port, **kwargs) try: # Make absolutely sure that whatever port we're on is valid From a17bf7283c2ebb65f631548b1d3e79e336260a5d Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 20 Nov 2019 17:17:33 +0000 Subject: [PATCH 24/27] Fixed AutocalibrationPlugin plugin name --- openflexure_microscope/microscope_settings.default.json | 2 +- .../plugins/default/camera_calibration/__init__.py | 2 +- .../plugins/default/camera_calibration/plugin.py | 4 ++-- openflexure_microscope/plugins/loader.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json index 64d0bc0d..40ea86e8 100644 --- a/openflexure_microscope/microscope_settings.default.json +++ b/openflexure_microscope/microscope_settings.default.json @@ -9,6 +9,6 @@ "plugins": [ "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:Plugin" + "openflexure_microscope.plugins.default.camera_calibration:AutocalibrationPlugin" ] } \ No newline at end of file diff --git a/openflexure_microscope/plugins/default/camera_calibration/__init__.py b/openflexure_microscope/plugins/default/camera_calibration/__init__.py index 0a7ba613..46032671 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/__init__.py +++ b/openflexure_microscope/plugins/default/camera_calibration/__init__.py @@ -1,2 +1,2 @@ __all__ = ["Plugin"] -from .plugin import Plugin +from .plugin import AutocalibrationPlugin diff --git a/openflexure_microscope/plugins/default/camera_calibration/plugin.py b/openflexure_microscope/plugins/default/camera_calibration/plugin.py index 126bb86c..b682be02 100644 --- a/openflexure_microscope/plugins/default/camera_calibration/plugin.py +++ b/openflexure_microscope/plugins/default/camera_calibration/plugin.py @@ -21,9 +21,9 @@ class RecalibrateAPIView(MicroscopeViewPlugin): return jsonify(task.state), 201 -class Plugin(MicroscopePlugin): +class AutocalibrationPlugin(MicroscopePlugin): """ - A set of default plugins + Auto-calibration plugin """ api_views = {"/recalibrate": RecalibrateAPIView} diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 0981d822..503bb2d0 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -204,7 +204,7 @@ class PluginLoader(object): # Store the plugin object, and it's properties self._plugins.append(plugin_object) # DEPRECATED: Store the plugin with it's old name - self._legacy_plugins[plugin_name_python_safe] = plugin_object + self._legacy_plugins[plugin_name] = plugin_object # Grant plugin access to the hardware if isinstance(plugin_object, MicroscopePlugin): From 97f718ccbdf5d6e427100f6b0c3637c6e63646e4 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 21 Nov 2019 12:39:39 +0000 Subject: [PATCH 25/27] Added example of static default values to val_disposable --- .../plugins/testing/form_example/forms.json | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/openflexure_microscope/plugins/testing/form_example/forms.json b/openflexure_microscope/plugins/testing/form_example/forms.json index 4aee4dcd..0739ea28 100644 --- a/openflexure_microscope/plugins/testing/form_example/forms.json +++ b/openflexure_microscope/plugins/testing/form_example/forms.json @@ -1,8 +1,7 @@ { "id": "test-plugin", "icon": "pets", - "forms": [ - { + "forms": [{ "name": "Simple request", "isCollapsible": false, "isTask": false, @@ -10,8 +9,7 @@ "route": "/do", "submitLabel": "Do things", "schema": [ - [ - { + [{ "fieldType": "numberInput", "placeholder": "Some integer", "name": "val_int", @@ -25,8 +23,7 @@ "name": "val_str" } ], - [ - { + [{ "fieldType": "radioList", "name": "val_radio", "label": "Radio value", @@ -54,8 +51,8 @@ { "fieldType": "textInput", - "placeholder": "Some string", "label": "Non-persistent string", + "default": "A default value", "name": "val_disposable" } ] @@ -66,15 +63,13 @@ "selfUpdate": true, "route": "/task", "submitLabel": "Start task", - "schema": [ - { - "fieldType": "numberInput", - "placeholder": "", - "name": "run_time", - "label": "Run time (seconds)", - "minValue": 1 - } - ] + "schema": [{ + "fieldType": "numberInput", + "placeholder": "", + "name": "run_time", + "label": "Run time (seconds)", + "minValue": 1 + }] } - ] + ] } \ No newline at end of file From 99a29eb393f1d8e6ea07b289d096e0556f8f74cf Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 21 Nov 2019 14:02:00 +0000 Subject: [PATCH 26/27] Fixed incorrect placeholder type --- openflexure_microscope/plugins/testing/form_example/forms.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/openflexure_microscope/plugins/testing/form_example/forms.json b/openflexure_microscope/plugins/testing/form_example/forms.json index 0739ea28..6bc16dc0 100644 --- a/openflexure_microscope/plugins/testing/form_example/forms.json +++ b/openflexure_microscope/plugins/testing/form_example/forms.json @@ -11,7 +11,6 @@ "schema": [ [{ "fieldType": "numberInput", - "placeholder": "Some integer", "name": "val_int", "label": "Number value", "minValue": 0 @@ -65,7 +64,6 @@ "submitLabel": "Start task", "schema": [{ "fieldType": "numberInput", - "placeholder": "", "name": "run_time", "label": "Run time (seconds)", "minValue": 1 From 659f58b254b72542ebad51f4bd18c0fe6f89eb20 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 21 Nov 2019 14:44:10 +0000 Subject: [PATCH 27/27] Fixed missing scan metadata --- openflexure_microscope/plugins/default/scan/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index 9d1d02d3..0815ecae 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -151,12 +151,14 @@ class ScanPlugin(MicroscopePlugin): metadata.update( { + "scan_id": scan_id, + "basename": basename, "scan_parameters": { "step_size": step_size, "grid": grid, "style": style, "autofocus_dz": autofocus_dz, - } + }, } )