From f2af359b8b1b88efa1dc72e369734030c48d2a11 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Wed, 18 Dec 2019 14:30:33 +0000 Subject: [PATCH] Added basic thing description to root --- openflexure_microscope/api/app.py | 60 ++++----- .../api/v2/blueprints/__init__.py | 2 +- .../api/v2/blueprints/plugins.py | 114 ------------------ .../api/v2/blueprints/root.py | 1 - .../api/v2/views/actions/__init__.py | 65 ++++++++++ .../api/v2/views/actions/camera.py | 82 +++++++++++++ .../api/v2/views/actions/stage.py | 55 +++++++++ .../api/v2/views/actions/system.py | 46 +++++++ .../api/v2/views/captures.py | 11 +- openflexure_microscope/api/v2/views/state.py | 94 +++++++++++++++ .../api/v2/views/streams.py | 62 ++++++++++ openflexure_microscope/api/v2/views/tasks.py | 71 +++++++++++ .../common/labthings/fields.py | 2 +- .../common/labthings/find.py | 13 +- .../common/labthings/labthing.py | 89 ++++++++++++-- .../common/labthings/plugins.py | 18 ++- .../common/tasks/__init__.py | 2 + openflexure_microscope/common/tasks/pool.py | 25 +++- openflexure_microscope/common/utilities.py | 6 + openflexure_microscope/microscope.py | 3 +- .../plugins/v2/autofocus.py | 11 +- openflexure_microscope/plugins/v2/scan.py | 13 +- poetry.lock | 14 ++- pyproject.toml | 1 + 24 files changed, 677 insertions(+), 183 deletions(-) delete mode 100644 openflexure_microscope/api/v2/blueprints/plugins.py create mode 100644 openflexure_microscope/api/v2/views/actions/__init__.py create mode 100644 openflexure_microscope/api/v2/views/actions/camera.py create mode 100644 openflexure_microscope/api/v2/views/actions/stage.py create mode 100644 openflexure_microscope/api/v2/views/actions/system.py create mode 100644 openflexure_microscope/api/v2/views/state.py create mode 100644 openflexure_microscope/api/v2/views/streams.py create mode 100644 openflexure_microscope/api/v2/views/tasks.py create mode 100644 openflexure_microscope/common/utilities.py diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 9ce27a69..528800d8 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -6,7 +6,7 @@ import logging import sys import os -from flask import Flask, jsonify, send_file +from flask import Flask, jsonify, send_file, url_for from serial import SerialException from datetime import datetime @@ -79,7 +79,7 @@ CORS(app, resources=r"*") handler = JSONExceptionHandler(app) # Attach lab devices -labthing = LabThing(app, prefix="/api/v2b") +labthing = LabThing(app, prefix="/api/v2") labthing.description = "Test LabThing-based API for OpenFlexure Microscope" labthing.register_device(api_microscope, "openflexure_microscope") @@ -88,41 +88,23 @@ for _plugin in find_plugins(USER_PLUGINS_PATH): labthing.register_plugin(_plugin) from openflexure_microscope.api.v2.views.captures import add_captures_to_labthing + add_captures_to_labthing(labthing, prefix="") +from openflexure_microscope.api.v2.views.state import add_states_to_labthing -# WEBAPP ROUTES +add_states_to_labthing(labthing, prefix="") -### V2 -# Root routes -v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope) -app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2")) +from openflexure_microscope.api.v2.views.tasks import add_tasks_to_labthing -v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope) -app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2")) +add_tasks_to_labthing(labthing, prefix="") -# Captures routes -v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope) -app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2")) +from openflexure_microscope.api.v2.views.streams import add_streams_to_labthing -# Settings routes -v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope) -app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2")) +add_streams_to_labthing(labthing, prefix="") -# Status routes -v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope) -app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2")) +from openflexure_microscope.api.v2.views.actions import add_actions_to_labthing -# 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")) - -# Actions routes -v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope) -app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2")) +add_actions_to_labthing(labthing) @app.route("/routes") @@ -139,6 +121,26 @@ def routes(): return jsonify(list_routes(app)) +@app.route("/props") +def props(): + p = {} + for key, prop in labthing.properties.items(): + p[key] = {} + p[key]["view"] = prop + p[key]["url"] = labthing.url_for(prop, _external=True) + return jsonify(p) + + +@app.route("/ac") +def actions(): + p = {} + for key, prop in labthing.actions.items(): + p[key] = {} + p[key]["view"] = prop + p[key]["url"] = labthing.url_for(prop, _external=True) + return jsonify(p) + + @app.route("/log") def err_log(): """ diff --git a/openflexure_microscope/api/v2/blueprints/__init__.py b/openflexure_microscope/api/v2/blueprints/__init__.py index 0ce1ebae..f4716594 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, streams, plugins, actions +from . import root, captures, settings, status, tasks, streams, actions diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py deleted file mode 100644 index c9c5f07f..00000000 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ /dev/null @@ -1,114 +0,0 @@ -""" -Top-level representation of attached and enabled plugins -""" - -from openflexure_microscope.api.views import MicroscopeViewPlugin -from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.utilities import blueprint_for_module - -from openflexure_microscope.common.labthings.plugins import find_plugins -from openflexure_microscope.config import USER_PLUGINS_PATH - -from flask import Blueprint, jsonify, url_for -from openflexure_microscope.api.views import MicroscopeView - -import copy -import logging -import warnings - -_plugins = find_plugins(USER_PLUGINS_PATH) -print(_plugins) - - -def plugins_representation(plugin_list): - """ - 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_list: - logging.debug(f"Representing plugin {plugin._name}") - d = { - "python_name": plugin._name_python_safe, - "plugin": str(plugin), - "views": {}, - "gui": plugin.gui, - "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.plugins") + view_data["rule"][1:] - # uri = view_data["rule"] - # Make links dictionary if it doesn't yet exist - view_d = {"links": {"self": uri}} - - view_d.update(description_from_view(view_data["view"])) - - d["views"][view_id] = view_d - - plugins[plugin._name] = 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. - - """ - global _plugins - return jsonify(plugins_representation(_plugins)) - - -def construct_blueprint(microscope_obj): - - blueprint = blueprint_for_module(__name__) - - for plugin_obj in _plugins: - for plugin_view_id, plugin_view in plugin_obj.views.items(): - # Add route to the plugins blueprint - blueprint.add_url_rule( - plugin_view["rule"], - view_func=plugin_view["view"].as_view( - f"{plugin_obj._name_python_safe}_{plugin_view_id}" - ), - **plugin_view["kwargs"], - ) - - # 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) - ) - - # For each plugin attached to the microscope object - for plugin in microscope_obj.plugins.active: - - 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( - f"{plugin._name_python_safe}_{plugin_view_id}", - microscope=microscope_obj, - plugin=plugin, - ), - **plugin_view["kwargs"], - ) - - return blueprint diff --git a/openflexure_microscope/api/v2/blueprints/root.py b/openflexure_microscope/api/v2/blueprints/root.py index 6790283e..0e8d148e 100644 --- a/openflexure_microscope/api/v2/blueprints/root.py +++ b/openflexure_microscope/api/v2/blueprints/root.py @@ -7,7 +7,6 @@ from openflexure_microscope.api.utilities import blueprint_name_for_module from openflexure_microscope.api.v2.blueprints import ( settings, status, - plugins, captures, actions, streams, diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py new file mode 100644 index 00000000..897d67c6 --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -0,0 +1,65 @@ +""" +Top-level representation of enabled actions +""" + +from flask import Blueprint, url_for, jsonify + +from openflexure_microscope.api.utilities import blueprint_for_module +from openflexure_microscope.utilities import get_docstring, description_from_view +from openflexure_microscope.api.views import MicroscopeView + +from . import camera, stage, system + +_actions = { + "capture": { + "rule": "/camera/capture/", + "view_class": camera.CaptureAPI, + "conditions": True, + }, + "previewStart": { + "rule": "/camera/preview/start", + "view_class": camera.GPUPreviewStartAPI, + "conditions": True, + }, + "previewStop": { + "rule": "/camera/preview/stop", + "view_class": camera.GPUPreviewStopAPI, + "conditions": True, + }, + "move": { + "rule": "/stage/move/", + "view_class": stage.MoveStageAPI, + "conditions": True, + }, + "zeroStage": { + "rule": "/stage/zero/", + "view_class": stage.ZeroStageAPI, + "conditions": True, + }, + "shutdown": { + "rule": "/system/shutdown/", + "view_class": system.ShutdownAPI, + "conditions": system.is_raspberrypi(), + }, + "reboot": { + "rule": "/system/reboot/", + "view_class": system.RebootAPI, + "conditions": system.is_raspberrypi(), + }, +} + + +def enabled_actions(): + global _actions + return {k: v for k, v in _actions.items() if v["conditions"]} + + +def add_actions_to_labthing(labthing, prefix=""): + """ + Add all capture resources to a labthing + """ + for name, action in enabled_actions().items(): + view_class = action["view_class"] + rule = action["rule"] + labthing.add_resource(view_class, f"{prefix}/actions{rule}") + labthing.register_action(view_class) diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py new file mode 100644 index 00000000..b3637c5d --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -0,0 +1,82 @@ +from openflexure_microscope.api.utilities import get_bool, JsonResponse +from openflexure_microscope.common.labthings.resource import Resource +from openflexure_microscope.common.labthings.find import find_device +from openflexure_microscope.utilities import filter_dict + +from openflexure_microscope.api.v2.views.captures import capture_schema + +import logging +from flask import jsonify, request, abort, url_for, redirect, send_file + + +class CaptureAPI(Resource): + """ + Create a new image capture. + """ + + def post(self): + microscope = find_device("openflexure_microscope") + payload = JsonResponse(request) + + filename = payload.param("filename") + temporary = payload.param("temporary", default=False, convert=bool) + use_video_port = payload.param("use_video_port", default=False, convert=bool) + bayer = payload.param("bayer", default=True, convert=bool) + metadata = payload.param("metadata", default={}, convert=dict) + tags = payload.param("tags", default=[], convert=list) + + resize = payload.param("size", default=None) + if resize: + if ("width" in resize) and ("height" in resize): + resize = ( + int(resize["width"]), + int(resize["height"]), + ) # Convert dict to tuple + else: + abort(404) + + # Explicitally acquire lock (prevents empty files being created if lock is unavailable) + with microscope.camera.lock: + output = microscope.camera.new_image(temporary=temporary, filename=filename) + + microscope.camera.capture( + output.file, use_video_port=use_video_port, resize=resize, bayer=bayer + ) + + # Inject system metadata + output.put_metadata(microscope.metadata, system=True) + + # Insert custom metadata + output.put_metadata(metadata) + + # Insert custom tags + output.put_tags(tags) + + return capture_schema.jsonify(output) + + +class GPUPreviewStartAPI(Resource): + def post(self): + microscope = find_device("openflexure_microscope") + payload = JsonResponse(request) + + window = payload.param("window", default=[]) + logging.debug(window) + + if len(window) != 4: + fullscreen = True + window = None + else: + fullscreen = False + window = [int(w) for w in window] + + microscope.camera.start_preview(fullscreen=fullscreen, window=window) + + return jsonify(microscope.state) + + +class GPUPreviewStopAPI(Resource): + def post(self): + microscope = find_device("openflexure_microscope") + microscope.camera.stop_preview() + return jsonify(microscope.state) diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py new file mode 100644 index 00000000..0d18d93d --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -0,0 +1,55 @@ +from openflexure_microscope.api.utilities import JsonResponse +from openflexure_microscope.common.labthings.resource import Resource +from openflexure_microscope.common.labthings.find import find_device +from openflexure_microscope.utilities import axes_to_array, filter_dict + +from flask import Blueprint, jsonify, request + +import logging + + +class MoveStageAPI(Resource): + def post(self): + microscope = find_device("openflexure_microscope") + # Create response object + payload = JsonResponse(request) + logging.debug(payload.json) + + # Handle absolute positioning (calculate a relative move from current position and target) + if (payload.param("absolute") is True) and ( + microscope.stage + ): # Only if stage exists + target_position = axes_to_array(payload.json, ["x", "y", "z"]) + logging.debug("TARGET: {}".format(target_position)) + position = [ + target_position[i] - microscope.stage.position[i] for i in range(3) + ] + logging.debug("DELTA: {}".format(position)) + + else: + # Get coordinates from payload + position = axes_to_array(payload.json, ["x", "y", "z"], [0, 0, 0]) + + logging.debug(position) + + # Move if stage exists + if microscope.stage: + # Explicitally acquire lock + with microscope.stage.lock: + microscope.stage.move_rel(position) + else: + logging.warning("Unable to move. No stage found.") + + return jsonify(microscope.status["stage"]["position"]) + + +class ZeroStageAPI(Resource): + """ + Zero stage coordinates + """ + + def post(self): + microscope = find_device("openflexure_microscope") + microscope.stage.zero_position() + + return jsonify(microscope.status["stage"]) diff --git a/openflexure_microscope/api/v2/views/actions/system.py b/openflexure_microscope/api/v2/views/actions/system.py new file mode 100644 index 00000000..b4243b07 --- /dev/null +++ b/openflexure_microscope/api/v2/views/actions/system.py @@ -0,0 +1,46 @@ +from openflexure_microscope.common.labthings.resource import Resource +import subprocess +import os +from sys import platform + + +def is_raspberrypi(raise_on_errors=False): + """ + Checks if Raspberry Pi. + """ + # I mean, if it works, it works... + return os.path.exists("/usr/bin/raspi-config") + + +class ShutdownAPI(Resource): + """ + Attempt to shutdown the device + """ + + def post(self): + """ + Attempt to shutdown the device + + .. :quickref: Actions; Shutdown + + """ + subprocess.Popen(["sudo", "shutdown", "-h", "now"]) + + return "{}", 201 + + +class RebootAPI(Resource): + """ + Attempt to reboot the device + """ + + def post(self): + """ + Attempt to shutdown the device + + .. :quickref: Actions; Shutdown + + """ + subprocess.Popen(["sudo", "shutdown", "-r", "now"]) + + return "{}", 201 diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index 76e0f7ba..f90623c9 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -33,9 +33,11 @@ class CaptureSchema(Schema): "mimetype": "application/json", }, "download": { - "href": fields.AbsoluteUrlFor("CaptureDownload", id="", filename=""), + "href": fields.AbsoluteUrlFor( + "CaptureDownload", id="", filename="" + ), "mimetype": "image/jpeg", - } + }, } ) @@ -185,11 +187,14 @@ def add_captures_to_labthing(labthing, prefix=""): Add all capture resources to a labthing """ labthing.add_resource(CaptureList, f"{prefix}/captures", endpoint="CaptureList") + labthing.register_property(CaptureList) labthing.add_resource( CaptureResource, f"{prefix}/captures/", endpoint="CaptureResource" ) labthing.add_resource( - CaptureDownload, f"{prefix}/captures//download/", endpoint="CaptureDownload" + CaptureDownload, + f"{prefix}/captures//download/", + endpoint="CaptureDownload", ) labthing.add_resource( CaptureTags, f"{prefix}/captures//tags", endpoint="CaptureTags" diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py new file mode 100644 index 00000000..c2dae44a --- /dev/null +++ b/openflexure_microscope/api/v2/views/state.py @@ -0,0 +1,94 @@ +from openflexure_microscope.api.utilities import JsonResponse +from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path + +from openflexure_microscope.common.labthings.find import find_device +from openflexure_microscope.common.labthings.resource import Resource + +from flask import jsonify, request, abort +import logging + + +class SettingsProperty(Resource): + def get(self): + microscope = find_device("openflexure_microscope") + return jsonify(microscope.read_settings()) + + def put(self): + microscope = find_device("openflexure_microscope") + payload = JsonResponse(request) + + logging.debug("Updating settings from PUT request:") + logging.debug(payload.json) + + microscope.apply_settings(payload.json) + microscope.save_settings() + + return self.get() + + +class NestedSettingsProperty(Resource): + def get(self, route): + microscope = find_device("openflexure_microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.read_settings(), keys) + except KeyError: + return abort(404) + + return jsonify(value) + + def put(self, route): + microscope = find_device("openflexure_microscope") + keys = route.split("/") + payload = JsonResponse(request) + + dictionary = create_from_path(keys) + set_by_path(dictionary, keys, payload.json) + + microscope.apply_settings(dictionary) + microscope.save_settings() + + return self.get(route) + + +class StatusProperty(Resource): + def get(self): + microscope = find_device("openflexure_microscope") + return jsonify(microscope.status) + + +class NestedStatusProperty(Resource): + def get(self, route): + microscope = find_device("openflexure_microscope") + keys = route.split("/") + + try: + value = get_by_path(microscope.status, keys) + except KeyError: + return abort(404) + + return jsonify(value) + + +def add_states_to_labthing(labthing, prefix=""): + """ + Add all settings and status resources to a labthing + """ + labthing.add_resource( + SettingsProperty, f"{prefix}/settings", endpoint="SettingsProperty" + ) + labthing.register_property(SettingsProperty) + labthing.add_resource( + NestedSettingsProperty, + f"{prefix}/settings/", + endpoint="NestedSettingsProperty", + ) + labthing.add_resource(StatusProperty, f"{prefix}/status", endpoint="StatusProperty") + labthing.register_property(StatusProperty) + labthing.add_resource( + NestedStatusProperty, + f"{prefix}/status/", + endpoint="NestedStatusProperty", + ) + diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py new file mode 100644 index 00000000..9527c941 --- /dev/null +++ b/openflexure_microscope/api/v2/views/streams.py @@ -0,0 +1,62 @@ +from openflexure_microscope.api.utilities import gen, JsonResponse +from openflexure_microscope.utilities import get_by_path, set_by_path, create_from_path + +from openflexure_microscope.common.labthings.find import find_device +from openflexure_microscope.common.labthings.resource import Resource + +from flask import jsonify, request, abort, Response +import logging + + +class MjpegStream(Resource): + """ + Real-time MJPEG stream from the microscope camera + """ + + def get(self): + """ + Real-time MJPEG stream from the microscope camera + """ + microscope = find_device("openflexure_microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response( + gen(microscope.camera), mimetype="multipart/x-mixed-replace; boundary=frame" + ) + + +class SnapshotStream(Resource): + """ + Single JPEG snapshot from the camera stream + """ + + def get(self): + """ + Single snapshot from the camera stream + + .. :quickref: Streams; Camera snapshot + + :>header Accept: image/jpeg + :>header Content-Type: image/jpeg + :status 200: stream active + """ + microscope = find_device("openflexure_microscope") + # Restart stream worker thread + microscope.camera.start_worker() + + return Response(microscope.camera.get_frame(), mimetype="image/jpeg") + + +def add_streams_to_labthing(labthing, prefix=""): + """ + Add all stream resources to a labthing + """ + labthing.add_resource( + MjpegStream, f"{prefix}/streams/mjpeg", endpoint="MjpegStream" + ) + labthing.register_property(MjpegStream) + labthing.add_resource( + SnapshotStream, f"{prefix}/streams/snapshot", endpoint="SnapshotStream" + ) + labthing.register_property(SnapshotStream) diff --git a/openflexure_microscope/api/v2/views/tasks.py b/openflexure_microscope/api/v2/views/tasks.py new file mode 100644 index 00000000..67a7e3da --- /dev/null +++ b/openflexure_microscope/api/v2/views/tasks.py @@ -0,0 +1,71 @@ +import logging +from flask import abort, request, redirect, url_for, send_file, jsonify + +from openflexure_microscope.api.utilities import get_bool, JsonResponse + +from openflexure_microscope.common.labthings.schema import Schema +from openflexure_microscope.common.labthings import fields +from openflexure_microscope.common.labthings.resource import Resource + +from openflexure_microscope.common import tasks + + +class TaskSchema(Schema): + _ID = fields.String(data_key="id") + target_string = fields.String(data_key="function") + _status = fields.String(data_key="status") + progress = fields.String() + data = fields.Raw() + _return_value = fields.Raw(data_key="return") + _start_time = fields.String(data_key="start_time") + _end_time = fields.String(data_key="end_time") + + # TODO: Add HTTP methods + links = fields.Hyperlinks( + { + "self": { + "href": fields.AbsoluteUrlFor("TaskResource", id=""), + "mimetype": "application/json", + } + } + ) + + +task_schema = TaskSchema() +task_list_schema = TaskSchema(many=True) + + +class TaskList(Resource): + def get(self): + return task_list_schema.jsonify(tasks.tasks()) + + +class TaskResource(Resource): + def get(self, id): + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + # Return task state + return jsonify(task) + + def delete(self, id): + try: + task = tasks.dict()[id] + except KeyError: + return abort(404) # 404 Not Found + + task.terminate() + + return jsonify(task.state) + + +def add_tasks_to_labthing(labthing, prefix=""): + """ + Add all settings and status resources to a labthing + """ + labthing.add_resource(TaskList, f"{prefix}/tasks", endpoint="TasksProperty") + labthing.register_property(TaskList) + labthing.add_resource(TaskResource, f"{prefix}/tasks/", endpoint="TaskResource") + diff --git a/openflexure_microscope/common/labthings/fields.py b/openflexure_microscope/common/labthings/fields.py index 7920f005..f255065f 100644 --- a/openflexure_microscope/common/labthings/fields.py +++ b/openflexure_microscope/common/labthings/fields.py @@ -150,4 +150,4 @@ class Hyperlinks(Field): Field.__init__(self, **kwargs) def _serialize(self, value, attr, obj): - return _rapply(self.schema, _url_val, key=attr, obj=obj) \ No newline at end of file + return _rapply(self.schema, _url_val, key=attr, obj=obj) diff --git a/openflexure_microscope/common/labthings/find.py b/openflexure_microscope/common/labthings/find.py index 7b09f43e..6952e161 100644 --- a/openflexure_microscope/common/labthings/find.py +++ b/openflexure_microscope/common/labthings/find.py @@ -4,7 +4,7 @@ from flask import current_app from . import EXTENSION_NAME -def _current_labthing(): +def current_labthing(): app = current_app._get_current_object() if not app: return None @@ -17,31 +17,32 @@ def _current_labthing(): def registered_plugins(labthing_instance=None): if not labthing_instance: - labthing_instance = _current_labthing() + labthing_instance = current_labthing() return labthing_instance.plugins def registered_devices(labthing_instance=None): if not labthing_instance: - labthing_instance = _current_labthing() + labthing_instance = current_labthing() return labthing_instance.devices def find_device(device_name, labthing_instance=None): if not labthing_instance: - labthing_instance = _current_labthing() + labthing_instance = current_labthing() if device_name in labthing_instance.devices: return labthing_instance.devices[device_name] else: return None + def find_plugin(plugin_name, labthing_instance=None): if not labthing_instance: - labthing_instance = _current_labthing() + labthing_instance = current_labthing() logging.debug("Current labthing:") - logging.debug(_current_labthing()) + logging.debug(current_labthing()) if plugin_name in labthing_instance.plugins: return labthing_instance.plugins[plugin_name] diff --git a/openflexure_microscope/common/labthings/labthing.py b/openflexure_microscope/common/labthings/labthing.py index 5c301fee..b65a848e 100644 --- a/openflexure_microscope/common/labthings/labthing.py +++ b/openflexure_microscope/common/labthings/labthing.py @@ -1,13 +1,17 @@ -from flask import current_app, _app_ctx_stack, request +from flask import current_app, _app_ctx_stack, request, url_for, jsonify from .plugins import BasePlugin from .views.plugins import PluginListResource +from .resource import Resource + +from ..utilities import get_docstring + from . import EXTENSION_NAME class LabThing(object): - def __init__(self, app=None, prefix="", description=""): + def __init__(self, app=None, prefix="", title="", description=""): self.app = app self.devices = {} @@ -15,10 +19,14 @@ class LabThing(object): self.plugins = {} self.resources = [] + self.properties = {} + self.actions = {} + self.endpoints = set() self.url_prefix = prefix self.description = description + self.title = title if app is not None: self.init_app(app) @@ -33,10 +41,17 @@ class LabThing(object): self._create_base_routes() + if len(self.resources) > 0: + for resource, urls, endpoint, kwargs in self.resources: + self._register_view(app, resource, *urls, endpoint=endpoint, **kwargs) + def teardown(self, exception): print(f"Tearing down devices: {self.devices}") def _create_base_routes(self): + # Add thing description to root + self.app.add_url_rule(self._complete_url("/", ""), "td", self.td) + # Add plugin overview self.add_resource(PluginListResource, "/plugins") ### Device stuff @@ -51,8 +66,6 @@ class LabThing(object): else: raise TypeError("Plugin object must be an instance of BasePlugin") - # TODO: Add plugin routes - for plugin_view_id, plugin_view in plugin_object.views.items(): # Add route to the plugins blueprint self.add_resource( @@ -61,6 +74,12 @@ class LabThing(object): **plugin_view["kwargs"], ) + for prop in plugin_object.properties: + self.register_property(prop) + + for action in plugin_object.actions: + self.register_action(action) + ### Resource stuff def _complete_url(self, url_part, registration_prefix): @@ -73,7 +92,23 @@ class LabThing(object): parts = [registration_prefix, self.url_prefix, url_part] return "".join([part for part in parts if part]) - def add_resource(self, resource, *urls, **kwargs): + def register_property(self, resource): + if hasattr(resource, "endpoint"): + self.properties[resource.endpoint] = resource + else: + raise RuntimeError( + f"Resource {resource} has not yet been added. Cannot set as a property." + ) + + def register_action(self, resource): + if hasattr(resource, "endpoint"): + self.actions[resource.endpoint] = resource + else: + raise RuntimeError( + f"Resource {resource} has not yet been added. Cannot set as an action." + ) + + def add_resource(self, resource, *urls, endpoint=None, **kwargs): """Adds a resource to the api. :param resource: the class name of your resource :type resource: :class:`Type[Resource]` @@ -81,7 +116,7 @@ class LabThing(object): flask routing rules apply. Any url variables will be passed to the resource method as args. :type urls: str - :param endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower` + :param endpoint: endpoint name (defaults to :meth:`Resource.__name__` Can be used to reference this route in :class:`fields.Url` fields :type endpoint: str :param resource_class_args: args to be forwarded to the constructor of @@ -97,10 +132,11 @@ class LabThing(object): api.add_resource(Foo, '/foo', endpoint="foo") api.add_resource(FooSpecial, '/special/foo', endpoint="foo") """ + endpoint = endpoint or resource.__name__ if self.app is not None: - self._register_view(self.app, resource, *urls, **kwargs) + self._register_view(self.app, resource, *urls, endpoint=endpoint, **kwargs) else: - self.resources.append((resource, urls, kwargs)) + self.resources.append((resource, urls, endpoint, kwargs)) def resource(self, *urls, **kwargs): """Wraps a :class:`~flask_restful.Resource` class, adding it to the @@ -120,8 +156,8 @@ class LabThing(object): return decorator - def _register_view(self, app, resource, *urls, **kwargs): - endpoint = kwargs.pop("endpoint", None) or resource.__name__.lower() + def _register_view(self, app, resource, *urls, endpoint=None, **kwargs): + endpoint = endpoint or resource.__name__ self.endpoints.add(endpoint) resource_class_args = kwargs.pop("resource_class_args", ()) resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) @@ -147,3 +183,36 @@ class LabThing(object): rule = self._complete_url(url, "") # Add the url to the application or blueprint app.add_url_rule(rule, view_func=resource_func, **kwargs) + + ### Utilities + + def url_for(self, resource, **values): + """Generates a URL to the given resource. + Works like :func:`flask.url_for`.""" + endpoint = resource.endpoint + return url_for(endpoint, **values) + + ### Description + def td(self): + props = {} + for key, prop in self.properties.items(): + props[key] = {} + props[key]["title"] = prop.__name__ + props[key]["description"] = get_docstring(prop) + props[key]["links"] = [{"href": self.url_for(prop, _external=True)}] + + actions = {} + for key, prop in self.actions.items(): + actions[key] = {} + actions[key]["title"] = prop.__name__ + actions[key]["description"] = get_docstring(prop) + actions[key]["links"] = [{"href": self.url_for(prop, _external=True)}] + + td = { + "title": self.title, + "description": self.description, + "properties": props, + "actions": actions, + } + + return jsonify(td) diff --git a/openflexure_microscope/common/labthings/plugins.py b/openflexure_microscope/common/labthings/plugins.py index 2a3078ee..fca5423d 100644 --- a/openflexure_microscope/common/labthings/plugins.py +++ b/openflexure_microscope/common/labthings/plugins.py @@ -27,6 +27,9 @@ class BasePlugin: self._rules = {} # Key: Original rule. Val: View class self._gui = None + self.actions = [] + self.properties = [] + self.name = name self.methods = {} @@ -35,7 +38,7 @@ class BasePlugin: def views(self): return self._views - def add_view(self, rule, view_class, **kwargs): + def add_view(self, view_class, rule, **kwargs): # Remove all leading slashes from view route cleaned_rule = rule while cleaned_rule[0] == "/": @@ -57,6 +60,12 @@ class BasePlugin: # Store the rule expansion information self._rules[rule] = self._views[view_id] + def register_action(self, view_class): + self.actions.append(view_class) + + def register_property(self, view_class): + self.properties.append(view_class) + @property def gui(self): print(self._gui) @@ -104,13 +113,16 @@ class BasePlugin: def _name_uri_safe(self): return snake_to_spine(self._name_python_safe) - def add_method(self, method_name, method): + def add_method(self, method, method_name): self.methods[method_name] = method if not hasattr(self, method_name): setattr(self, method_name, method) else: - logging.warning("Unable to bind method to plugin. Method name already exists.") + logging.warning( + "Unable to bind method to plugin. Method name already exists." + ) + def find_plugins(plugin_path, module_name="plugins"): print(f"Loading plugins from {plugin_path}") diff --git a/openflexure_microscope/common/tasks/__init__.py b/openflexure_microscope/common/tasks/__init__.py index 6b3e6dcb..282998ca 100644 --- a/openflexure_microscope/common/tasks/__init__.py +++ b/openflexure_microscope/common/tasks/__init__.py @@ -1,6 +1,7 @@ __all__ = [ "taskify", "tasks", + "dict", "states", "current_task", "update_task_progress", @@ -12,6 +13,7 @@ __all__ = [ from .pool import ( tasks, + dict, states, current_task, update_task_progress, diff --git a/openflexure_microscope/common/tasks/pool.py b/openflexure_microscope/common/tasks/pool.py index b8b526e9..69fa3ca0 100644 --- a/openflexure_microscope/common/tasks/pool.py +++ b/openflexure_microscope/common/tasks/pool.py @@ -6,12 +6,21 @@ from .thread import TaskThread from flask import copy_current_request_context + class TaskMaster: def __init__(self, *args, **kwargs): self._tasks = [] @property def tasks(self): + """ + Returns: + list: List of TaskThread objects. + """ + return self._tasks + + @property + def dict(self): """ Returns: dict: Dictionary of TaskThread objects. Key is TaskThread ID. @@ -28,7 +37,9 @@ class TaskMaster: def new(self, f, *args, **kwargs): # copy_current_request_context allows threads to access flask current_app - task = TaskThread(target=copy_current_request_context(f), args=args, kwargs=kwargs) + task = TaskThread( + target=copy_current_request_context(f), args=args, kwargs=kwargs + ) self._tasks.append(task) return task @@ -47,13 +58,23 @@ class TaskMaster: def tasks(): + """ + List of tasks in default taskmaster + Returns: + list: List of tasks in default taskmaster + """ + global _default_task_master + return _default_task_master.tasks + + +def dict(): """ Dictionary of tasks in default taskmaster Returns: dict: Dictionary of tasks in default taskmaster """ global _default_task_master - return _default_task_master.tasks + return _default_task_master.dict def states(): diff --git a/openflexure_microscope/common/utilities.py b/openflexure_microscope/common/utilities.py new file mode 100644 index 00000000..c82d54f9 --- /dev/null +++ b/openflexure_microscope/common/utilities.py @@ -0,0 +1,6 @@ +def get_docstring(obj): + ds = obj.__doc__ + if ds: + return ds.strip() + else: + return "" diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index a62c6fea..694a8c32 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -147,7 +147,6 @@ class Microscope: else: return False - # Create unified status @property def status(self): @@ -239,7 +238,7 @@ class Microscope: """ system_metadata = { "microscope_settings": self.read_settings(), - "microscope_state": self.state, + "microscope_state": self.status, "microscope_id": self.id, "microscope_name": self.name, } diff --git a/openflexure_microscope/plugins/v2/autofocus.py b/openflexure_microscope/plugins/v2/autofocus.py index 3b4408ad..6271c99d 100644 --- a/openflexure_microscope/plugins/v2/autofocus.py +++ b/openflexure_microscope/plugins/v2/autofocus.py @@ -356,9 +356,10 @@ class FastAutofocusAPI(MethodView): autofocus_plugin_v2 = BasePlugin("autofocus") -autofocus_plugin_v2.add_method("fast_autofocus", fast_autofocus) -autofocus_plugin_v2.add_method("autofocus", autofocus) +autofocus_plugin_v2.add_method(fast_autofocus, "fast_autofocus") +autofocus_plugin_v2.add_method(autofocus, "autofocus") + +autofocus_plugin_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness") +autofocus_plugin_v2.add_view(AutofocusAPI, "/autofocus") +autofocus_plugin_v2.add_view(FastAutofocusAPI, "/fast_autofocus") -autofocus_plugin_v2.add_view("/measure_sharpness", MeasureSharpnessAPI) -autofocus_plugin_v2.add_view("/autofocus", AutofocusAPI) -autofocus_plugin_v2.add_view("/fast_autofocus", FastAutofocusAPI) diff --git a/openflexure_microscope/plugins/v2/scan.py b/openflexure_microscope/plugins/v2/scan.py index 7a6e2d2f..f774c2f8 100644 --- a/openflexure_microscope/plugins/v2/scan.py +++ b/openflexure_microscope/plugins/v2/scan.py @@ -25,6 +25,7 @@ import time ### Grid construction + def construct_grid(initial, step_sizes, n_steps, style="raster"): """ Given an initial position, step sizes, and number of steps, @@ -64,6 +65,7 @@ def flatten_grid(grid): _images_to_be_captured: int = 1 _images_captured_so_far: int = 0 + def progress(): progress = (_images_captured_so_far / _images_to_be_captured) * 100 logging.info(progress) @@ -72,6 +74,7 @@ def progress(): ### Capturing + def capture( microscope, basename, @@ -114,6 +117,7 @@ def capture( ### Scanning + def tile( microscope, basename: str = None, @@ -181,9 +185,7 @@ def tile( ) # shorthand for Z stack range # Construct an x-y grid (worry about z later) - x_y_grid = construct_grid( - initial_position, step_size[:2], grid[:2], style=style - ) + x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style) # Keep the initial Z position the same as our current position next_z = initial_position[2] @@ -270,6 +272,7 @@ def tile( logging.debug("Returning to {}".format(initial_position)) microscope.stage.move_abs(initial_position) + def stack( microscope, basename: str = None, @@ -336,6 +339,7 @@ def stack( ### Web views + class TileScanAPI(MethodView): def post(self): payload = JsonResponse(request) @@ -396,4 +400,5 @@ class TileScanAPI(MethodView): scan_plugin_v2 = BasePlugin("scan") -scan_plugin_v2.add_view("/tile", TileScanAPI) +scan_plugin_v2.add_view(TileScanAPI, "/tile") +scan_plugin_v2.register_action(TileScanAPI) diff --git a/poetry.lock b/poetry.lock index c54fe7f2..2858799b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -190,6 +190,14 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "1.1.1" +[[package]] +category = "main" +description = "A lightweight library for converting complex datatypes to and from native Python datatypes." +name = "marshmallow" +optional = false +python-versions = ">=3.5" +version = "3.3.0" + [[package]] category = "dev" description = "McCabe checker, plugin for flake8" @@ -230,6 +238,7 @@ version = "1.13.1b0" reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" type = "git" url = "https://github.com/rwb27/picamera.git" + [[package]] category = "main" description = "Python Imaging Library (Fork)" @@ -473,7 +482,7 @@ version = "1.11.2" rpi = ["picamera", "RPi.GPIO"] [metadata] -content-hash = "ea3fcd1909a76b04efa8ebefdcb77f0b449642820a94429fbbbf20bb4f9a64ab" +content-hash = "254e455bd1b5a5482f296f8afccf5bd10d109206e8350b9e8ec304576e0d0ae5" python-versions = "^3.6" [metadata.hashes] @@ -497,6 +506,7 @@ itsdangerous = ["321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f1 jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"] lazy-object-proxy = ["0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d", "194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449", "1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08", "4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a", "48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50", "5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd", "59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239", "8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb", "9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea", "9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e", "97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156", "9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142", "a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442", "a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62", "ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db", "cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531", "d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383", "d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a", "eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357", "efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"] markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"] +marshmallow = ["0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e", "3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"] mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] numpy = ["0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f", "0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6", "3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e", "43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447", "475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76", "64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba", "683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc", "6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae", "75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c", "7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9", "8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f", "9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725", "a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1", "acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761", "ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170", "c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e", "e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018", "e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3", "ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143", "f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316", "fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"] packaging = ["28b924174df7a2fa32c1953825ff29c61e2f5e082343165438812f00d3a7fc47", "d9551545c6d761f3def1677baf08ab2a3ca17c56879e70fecba2fc4dde4ed108"] @@ -522,7 +532,7 @@ sphinxcontrib-jsmath = ["2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c sphinxcontrib-qthelp = ["513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"] sphinxcontrib-serializinghtml = ["c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"] toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"] -typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] +typed-ast = ["18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"] urllib3 = ["a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293", "f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"] werkzeug = ["7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", "e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"] wrapt = ["565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"] diff --git a/pyproject.toml b/pyproject.toml index f35db200..dd1da294 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ scipy = "1.3.0" # Exact version until we can guarantee a wheel picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch. "RPi.GPIO" = { version = "^0.6.5", optional = true } +marshmallow = "^3.3" [tool.poetry.extras] rpi = ["picamera", "RPi.GPIO"]