diff --git a/docs/source/plugins/example/plugin.py b/docs/source/plugins/example/plugin.py index e99f9e85..0ea51b23 100644 --- a/docs/source/plugins/example/plugin.py +++ b/docs/source/plugins/example/plugin.py @@ -2,7 +2,7 @@ from openflexure_microscope.plugins import MicroscopePlugin from openflexure_microscope.api.views import MicroscopeViewPlugin from openflexure_microscope.api.utilities import JsonResponse -from openflexure_microscope.common.tasks import taskify +from openflexure_microscope.common.labthings_core.tasks import taskify import os import time diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 602a6a27..50123f81 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -3,29 +3,24 @@ import time import atexit import logging -import sys import os -from flask import Flask, jsonify, send_file, url_for +from flask import Flask, jsonify, send_file -from serial import SerialException from datetime import datetime from flask_cors import CORS -from openflexure_microscope.api.exceptions import JSONExceptionHandler -from openflexure_microscope.api.utilities import list_routes +from openflexure_microscope.api.utilities import list_routes, init_default_plugins from openflexure_microscope.config import ( settings_file_path, JSONEncoder, USER_PLUGINS_PATH, ) -from openflexure_microscope.api import v2 -from openflexure_microscope.common.labthings.labthing import LabThing -from openflexure_microscope.common.labthings.find import registered_plugins -from openflexure_microscope.common.labthings.plugins import find_plugins +from openflexure_microscope.common.flask_labthings.labthing import LabThing +from openflexure_microscope.common.flask_labthings.plugins import find_plugins from openflexure_microscope.api.microscope import default_microscope as api_microscope @@ -36,14 +31,13 @@ is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log") +logger = logging.getLogger() if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn print("Letting sys handle logs") - logger = logging.getLogger() logger.setLevel(logging.DEBUG) else: # Direct standard Python logging to file and console - root = logging.getLogger() error_formatter = logging.Formatter( "[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s" ) @@ -56,9 +50,9 @@ else: for handler in error_handlers: handler.setFormatter(error_formatter) - root.addHandler(handler) + logger.addHandler(handler) - root.setLevel(logging.getLogger("gunicorn.error").level) + logger.setLevel(logging.getLogger("gunicorn.error").level) # Create flask app @@ -71,9 +65,6 @@ app.json_encoder = JSONEncoder # Enable CORS everywhere CORS(app, resources=r"*") -# Make errors more API friendly -handler = JSONExceptionHandler(app) - # Build a labthing labthing = LabThing(app, prefix="/api/v2") labthing.description = "Test LabThing-based API for OpenFlexure Microscope" @@ -83,6 +74,8 @@ labthing.title = f"OpenFlexure Microscope {api_microscope.name}" labthing.register_device(api_microscope, "openflexure_microscope") # Attach plugins +if not os.path.isfile(USER_PLUGINS_PATH): + init_default_plugins(USER_PLUGINS_PATH) for plugin in find_plugins(USER_PLUGINS_PATH): labthing.register_plugin(plugin) @@ -120,7 +113,7 @@ labthing.register_property(views.SnapshotStream) for name, action in views.enabled_root_actions().items(): view_class = action["view_class"] rule = action["rule"] - labthing.add_resource(view_class, "/actions{rule}") + labthing.add_resource(view_class, f"/actions{rule}") labthing.register_action(view_class) @@ -159,7 +152,6 @@ def err_log(): # Automatically clean up microscope at exit def cleanup(): - global api_microscope logging.debug("App teardown started...") logging.debug("Settling...") time.sleep(0.5) diff --git a/openflexure_microscope/api/microscope.py b/openflexure_microscope/api/microscope.py index c950b83d..04053892 100644 --- a/openflexure_microscope/api/microscope.py +++ b/openflexure_microscope/api/microscope.py @@ -28,12 +28,8 @@ except Exception as e: # Initialise stage logging.debug("Creating stage object...") -try: - api_stage = SangaStage() -except Exception as e: - logging.error(e) - logging.warning("No valid stage hardware found. Falling back to mock stage!") - api_stage = MockStage() + +api_stage = MockStage() # Attach devices to microscope logging.debug("Attaching devices to microscope...") diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index 32a854e5..98232ecb 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -1,4 +1,6 @@ import logging +import os +import errno from werkzeug.exceptions import BadRequest from flask import url_for, Blueprint @@ -95,3 +97,31 @@ def list_routes(app): output[url] = line return output + + +def create_file(config_path): + if not os.path.exists(os.path.dirname(config_path)): + try: + os.makedirs(os.path.dirname(config_path)) + except OSError as exc: # Guard against race condition + if exc.errno != errno.EEXIST: + raise + + +def init_default_plugins(plugin_path): + global _DEFAULT_PLUGIN_INIT + os.makedirs(os.path.dirname(plugin_path), exist_ok=True) + + if not os.path.exists(plugin_path): # If user plugins file doesn't exist + logging.warning("No plugin file found at {}. Creating...".format(plugin_path)) + create_file(plugin_path) + + logging.info("Populating {}...".format(plugin_path)) + with open(plugin_path, "w") as outfile: + outfile.write(_DEFAULT_PLUGIN_INIT) + + +_DEFAULT_PLUGIN_INIT = """from openflexure_microscope.plugins.v2.autofocus import autofocus_plugin_v2 +from openflexure_microscope.plugins.v2.scan import scan_plugin_v2 + +__plugins__ = [autofocus_plugin_v2, scan_plugin_v2]""" \ No newline at end of file diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py index 15c287d7..20d3a4aa 100644 --- a/openflexure_microscope/api/v2/views/actions/__init__.py +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -2,11 +2,6 @@ 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 . import camera, stage, system _actions = { diff --git a/openflexure_microscope/api/v2/views/actions/camera.py b/openflexure_microscope/api/v2/views/actions/camera.py index b3637c5d..f3b59802 100644 --- a/openflexure_microscope/api/v2/views/actions/camera.py +++ b/openflexure_microscope/api/v2/views/actions/camera.py @@ -1,6 +1,6 @@ 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.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.find import find_device from openflexure_microscope.utilities import filter_dict from openflexure_microscope.api.v2.views.captures import capture_schema diff --git a/openflexure_microscope/api/v2/views/actions/stage.py b/openflexure_microscope/api/v2/views/actions/stage.py index 0d18d93d..069ed059 100644 --- a/openflexure_microscope/api/v2/views/actions/stage.py +++ b/openflexure_microscope/api/v2/views/actions/stage.py @@ -1,6 +1,6 @@ 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.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.find import find_device from openflexure_microscope.utilities import axes_to_array, filter_dict from flask import Blueprint, jsonify, request diff --git a/openflexure_microscope/api/v2/views/actions/system.py b/openflexure_microscope/api/v2/views/actions/system.py index b4243b07..b148de7d 100644 --- a/openflexure_microscope/api/v2/views/actions/system.py +++ b/openflexure_microscope/api/v2/views/actions/system.py @@ -1,4 +1,4 @@ -from openflexure_microscope.common.labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.resource import Resource import subprocess import os from sys import platform diff --git a/openflexure_microscope/api/v2/views/captures.py b/openflexure_microscope/api/v2/views/captures.py index f90623c9..05c98624 100644 --- a/openflexure_microscope/api/v2/views/captures.py +++ b/openflexure_microscope/api/v2/views/captures.py @@ -3,47 +3,12 @@ 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.flask_labthings.schema import Schema +from openflexure_microscope.common.flask_labthings import fields +from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.utilities import description_from_view -from openflexure_microscope.common.labthings.find import find_device - - -class CaptureSchema(Schema): - id = fields.String() - file = fields.String(data_key="path") - exists = fields.Bool(data_key="available") - filename = fields.String() - metadata = fields.Dict() - - # TODO: Add HTTP methods - links = fields.Hyperlinks( - { - "self": { - "href": fields.AbsoluteUrlFor("CaptureResource", id=""), - "mimetype": "application/json", - }, - "tags": { - "href": fields.AbsoluteUrlFor("CaptureTags", id=""), - "mimetype": "application/json", - }, - "metadata": { - "href": fields.AbsoluteUrlFor("CaptureMetadata", id=""), - "mimetype": "application/json", - }, - "download": { - "href": fields.AbsoluteUrlFor( - "CaptureDownload", id="", filename="" - ), - "mimetype": "image/jpeg", - }, - } - ) - - -capture_schema = CaptureSchema() -capture_list_schema = CaptureSchema(many=True) +from openflexure_microscope.common.flask_labthings.find import find_device class CaptureList(Resource): @@ -182,6 +147,46 @@ class CaptureMetadata(Resource): return jsonify(capture_obj.metadata) +class CaptureSchema(Schema): + id = fields.String() + file = fields.String(data_key="path") + exists = fields.Bool(data_key="available") + filename = fields.String() + metadata = fields.Dict() + + # TODO: Add HTTP methods + links = fields.Hyperlinks( + { + "self": { + "href": fields.AbsoluteUrlFor(CaptureResource, id=""), + "mimetype": "application/json", + **description_from_view(CaptureResource) + }, + "tags": { + "href": fields.AbsoluteUrlFor(CaptureTags, id=""), + "mimetype": "application/json", + **description_from_view(CaptureTags) + }, + "metadata": { + "href": fields.AbsoluteUrlFor(CaptureMetadata, id=""), + "mimetype": "application/json", + **description_from_view(CaptureMetadata) + }, + "download": { + "href": fields.AbsoluteUrlFor( + CaptureDownload, id="", filename="" + ), + "mimetype": "image/jpeg", + **description_from_view(CaptureDownload) + }, + } + ) + + +capture_schema = CaptureSchema() +capture_list_schema = CaptureSchema(many=True) + + def add_captures_to_labthing(labthing, prefix=""): """ Add all capture resources to a labthing diff --git a/openflexure_microscope/api/v2/views/state.py b/openflexure_microscope/api/v2/views/state.py index 176ae0ea..84c37850 100644 --- a/openflexure_microscope/api/v2/views/state.py +++ b/openflexure_microscope/api/v2/views/state.py @@ -1,8 +1,8 @@ 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 openflexure_microscope.common.flask_labthings.find import find_device +from openflexure_microscope.common.flask_labthings.resource import Resource from flask import jsonify, request, abort import logging diff --git a/openflexure_microscope/api/v2/views/streams.py b/openflexure_microscope/api/v2/views/streams.py index 9527c941..fe22c959 100644 --- a/openflexure_microscope/api/v2/views/streams.py +++ b/openflexure_microscope/api/v2/views/streams.py @@ -1,8 +1,8 @@ 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 openflexure_microscope.common.flask_labthings.find import find_device +from openflexure_microscope.common.flask_labthings.resource import Resource from flask import jsonify, request, abort, Response import logging diff --git a/openflexure_microscope/camera/base.py b/openflexure_microscope/camera/base.py index ebce1bf8..1bdf33bf 100644 --- a/openflexure_microscope/camera/base.py +++ b/openflexure_microscope/camera/base.py @@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod from .capture import CaptureObject from openflexure_microscope.utilities import entry_by_id -from openflexure_microscope.common.lock import StrictLock +from openflexure_microscope.common.labthings_core.lock import StrictLock BASE_CAPTURE_PATH = os.path.join(os.path.expanduser("~"), "micrographs") diff --git a/openflexure_microscope/common/__init__.py b/openflexure_microscope/common/__init__.py index 80899ff0..c9450d14 100644 --- a/openflexure_microscope/common/__init__.py +++ b/openflexure_microscope/common/__init__.py @@ -1 +1,2 @@ -from . import tasks, labthings +from . import flask_labthings +from openflexure_microscope.common.labthings_core import tasks diff --git a/openflexure_microscope/common/labthings/__init__.py b/openflexure_microscope/common/flask_labthings/__init__.py similarity index 100% rename from openflexure_microscope/common/labthings/__init__.py rename to openflexure_microscope/common/flask_labthings/__init__.py diff --git a/openflexure_microscope/api/exceptions.py b/openflexure_microscope/common/flask_labthings/exceptions.py similarity index 93% rename from openflexure_microscope/api/exceptions.py rename to openflexure_microscope/common/flask_labthings/exceptions.py index 624d5dba..bbb83d1e 100644 --- a/openflexure_microscope/api/exceptions.py +++ b/openflexure_microscope/common/flask_labthings/exceptions.py @@ -24,7 +24,7 @@ class JSONExceptionHandler(object): status_code = error.code if isinstance(error, HTTPException) else 500 - response = {"status_code": status_code, "message": escape(message)} + response = {"code": status_code, "message": escape(message)} return jsonify(response), status_code def init_app(self, app): diff --git a/openflexure_microscope/common/labthings/fields.py b/openflexure_microscope/common/flask_labthings/fields.py similarity index 83% rename from openflexure_microscope/common/labthings/fields.py rename to openflexure_microscope/common/flask_labthings/fields.py index f255065f..13ea7da9 100644 --- a/openflexure_microscope/common/labthings/fields.py +++ b/openflexure_microscope/common/flask_labthings/fields.py @@ -2,6 +2,7 @@ from marshmallow.fields import * from marshmallow import missing import re from flask import url_for +from flask.views import View _tpl_pattern = re.compile(r"\s*<\s*(\S*)\s*>\s*") @@ -62,7 +63,16 @@ class URLFor(Field): _CHECK_ATTRIBUTE = False def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint + # Handle the case where endpoint is an attached flask View of any kind + if isinstance(endpoint, type) and issubclass(endpoint, View): + self.view_class = endpoint + self.endpoint = None + # Handle cases where endpoint is passed directly as a string + elif type(endpoint) == str: + self.view_class = None + self.endpoint = endpoint + else: + raise RuntimeError(f"Endpoint {endpoint} is not a valid Flask view or endpoint string.") self.params = kwargs Field.__init__(self, **kwargs) @@ -70,6 +80,14 @@ class URLFor(Field): """Output the URL for the endpoint, given the kwargs passed to ``__init__``. """ + # Get endpoint from view_class, if needed + if self.view_class and not self.endpoint: + if hasattr(self.view_class, "endpoint"): + self.endpoint = self.view_class.endpoint + else: + raise RuntimeError(f"Resource {self.endpoint} has not been added to a LabThing application. Unable to generate URL.") + + # Generate URL for param_values = {} for name, attr_tpl in self.params.items(): attr_name = _tpl(str(attr_tpl)) diff --git a/openflexure_microscope/common/labthings/find.py b/openflexure_microscope/common/flask_labthings/find.py similarity index 100% rename from openflexure_microscope/common/labthings/find.py rename to openflexure_microscope/common/flask_labthings/find.py diff --git a/openflexure_microscope/common/labthings/labthing.py b/openflexure_microscope/common/flask_labthings/labthing.py similarity index 84% rename from openflexure_microscope/common/labthings/labthing.py rename to openflexure_microscope/common/flask_labthings/labthing.py index 0f23f258..e917ca50 100644 --- a/openflexure_microscope/common/labthings/labthing.py +++ b/openflexure_microscope/common/flask_labthings/labthing.py @@ -1,16 +1,17 @@ -from flask import current_app, _app_ctx_stack, request, url_for, jsonify +from flask import url_for, jsonify from .plugins import BasePlugin from .views.plugins import PluginListResource from .views.tasks import TaskList, TaskResource -from ..utilities import get_docstring +from openflexure_microscope.common.labthings_core.utilities import get_docstring +from .exceptions import JSONExceptionHandler from . import EXTENSION_NAME class LabThing(object): - def __init__(self, app=None, prefix="", title="", description=""): + def __init__(self, app=None, prefix: str = "", title: str = "", description: str = "", handle_errors: bool = True): self.app = app self.devices = {} @@ -27,6 +28,11 @@ class LabThing(object): self.description = description self.title = title + if handle_errors: + self.error_handler = JSONExceptionHandler() + else: + self.error_handler = None + if app is not None: self.init_app(app) @@ -35,21 +41,28 @@ class LabThing(object): def init_app(self, app): app.teardown_appcontext(self.teardown) + # Register Flask extension app.extensions = getattr(app, "extensions", {}) app.extensions[EXTENSION_NAME] = self + # Register error handler if one exists + if self.error_handler: + self.error_handler.init_app(self.app) + + # Create base routes self._create_base_routes() + # Add resources, if registered before tying to a Flask app 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}") + pass def _create_base_routes(self): # Add thing description to root - self.app.add_url_rule(self._complete_url("/", ""), "td", self.td) + self.app.add_url_rule(self._complete_url("/td", ""), "td", self.td) # Add plugin overview self.add_resource(PluginListResource, "/plugins") self.register_property(PluginListResource) @@ -64,6 +77,7 @@ class LabThing(object): self.devices[device_name] = device_object ### Plugin stuff + def register_plugin(self, plugin_object): if isinstance(plugin_object, BasePlugin): self.plugins[plugin_object.name] = plugin_object @@ -196,8 +210,25 @@ class LabThing(object): endpoint = resource.endpoint return url_for(endpoint, **values) + def owns_endpoint(self, endpoint): + """Tests if an endpoint name (not path) belongs to this Api. Takes + in to account the Blueprint name part of the endpoint name. + :param endpoint: The name of the endpoint being checked + :return: bool + """ + + if self.blueprint: + if endpoint.startswith(self.blueprint.name): + endpoint = endpoint.split(self.blueprint.name + '.', 1)[-1] + else: + return False + return endpoint in self.endpoints + ### Description def td(self): + """ + W3C-style Thing Description + """ props = {} for key, prop in self.properties.items(): props[key] = {} @@ -221,3 +252,5 @@ class LabThing(object): } return jsonify(td) + + # TODO: Add a nicer root resource like the old self-documenting system \ No newline at end of file diff --git a/openflexure_microscope/common/labthings/plugins.py b/openflexure_microscope/common/flask_labthings/plugins.py similarity index 92% rename from openflexure_microscope/common/labthings/plugins.py rename to openflexure_microscope/common/flask_labthings/plugins.py index fca5423d..05a431a1 100644 --- a/openflexure_microscope/common/labthings/plugins.py +++ b/openflexure_microscope/common/flask_labthings/plugins.py @@ -5,10 +5,8 @@ import copy from importlib import util import sys -from openflexure_microscope.config import USER_CONFIG_DIR from openflexure_microscope.utilities import ( camel_to_snake, - camel_to_spine, snake_to_spine, ) @@ -125,11 +123,6 @@ class BasePlugin: def find_plugins(plugin_path, module_name="plugins"): - print(f"Loading plugins from {plugin_path}") - # plugin_path = os.path.join(USER_CONFIG_DIR, "microscope_plugins") - # plugins = importlib.machinery.SourceFileLoader( - # module_name, plugin_path - # ).exec_module() logging.debug(f"Loading plugins from {plugin_path}") spec = util.spec_from_file_location(module_name, plugin_path) diff --git a/openflexure_microscope/common/labthings/resource.py b/openflexure_microscope/common/flask_labthings/resource.py similarity index 100% rename from openflexure_microscope/common/labthings/resource.py rename to openflexure_microscope/common/flask_labthings/resource.py diff --git a/openflexure_microscope/common/labthings/schema.py b/openflexure_microscope/common/flask_labthings/schema.py similarity index 100% rename from openflexure_microscope/common/labthings/schema.py rename to openflexure_microscope/common/flask_labthings/schema.py diff --git a/openflexure_microscope/common/flask_labthings/utilities.py b/openflexure_microscope/common/flask_labthings/utilities.py new file mode 100644 index 00000000..681471c9 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/utilities.py @@ -0,0 +1,13 @@ +from openflexure_microscope.common.labthings_core.utilities import get_docstring + + +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 diff --git a/openflexure_microscope/common/labthings/views/__init__.py b/openflexure_microscope/common/flask_labthings/views/__init__.py similarity index 100% rename from openflexure_microscope/common/labthings/views/__init__.py rename to openflexure_microscope/common/flask_labthings/views/__init__.py diff --git a/openflexure_microscope/common/flask_labthings/views/plugins.py b/openflexure_microscope/common/flask_labthings/views/plugins.py new file mode 100644 index 00000000..eb6b27b0 --- /dev/null +++ b/openflexure_microscope/common/flask_labthings/views/plugins.py @@ -0,0 +1,64 @@ +""" +Top-level representation of attached and enabled plugins +""" + +from openflexure_microscope.common.labthings_core.utilities import get_docstring +from ..utilities import description_from_view + +from openflexure_microscope.common.flask_labthings.find import registered_plugins +from openflexure_microscope.common.flask_labthings.resource import Resource + +from flask import jsonify, url_for + +import logging + + +def plugins_representation(plugin_dict): + """ + Generate a dictionary representation of all plugins, including Flask route URLs + + Args: + plugin_dict (dict): Dictionary of plugin objects + + Returns: + dict: Dictionary representation of all plugins + """ + plugins = {} + + for plugin_name, plugin in plugin_dict.items(): + logging.debug(f"Representing plugin {plugin._name}") + d = { + "python_name": plugin._name_python_safe, + "plugin": str(plugin), + "links": {}, + "gui": plugin.gui, + "description": get_docstring(plugin), + } + + for view_id, view_data in plugin.views.items(): + uri = url_for(f"PluginListResource", _external=True) + "/" + view_data["rule"][1:] + # Make links dictionary if it doesn't yet exist + view_d = {"href": uri, **description_from_view(view_data["view"])} + + d["links"][view_id] = view_d + + plugins[plugin_name] = d + + return plugins + + +class PluginListResource(Resource): + 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(registered_plugins())) diff --git a/openflexure_microscope/common/labthings/views/tasks.py b/openflexure_microscope/common/flask_labthings/views/tasks.py similarity index 69% rename from openflexure_microscope/common/labthings/views/tasks.py rename to openflexure_microscope/common/flask_labthings/views/tasks.py index afcba39b..bc2ef32b 100644 --- a/openflexure_microscope/common/labthings/views/tasks.py +++ b/openflexure_microscope/common/flask_labthings/views/tasks.py @@ -1,38 +1,10 @@ -import logging -from flask import abort, request, redirect, url_for, send_file, jsonify +from flask import abort -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) +from openflexure_microscope.common.flask_labthings.schema import Schema +from openflexure_microscope.common.flask_labthings import fields +from openflexure_microscope.common.labthings_core import tasks +from openflexure_microscope.common.flask_labthings.resource import Resource +from openflexure_microscope.common.flask_labthings.utilities import description_from_view class TaskList(Resource): @@ -59,3 +31,29 @@ class TaskResource(Resource): task.terminate() return task_schema.jsonify(task) + + +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", + **description_from_view(TaskResource) + } + } + ) + + +task_schema = TaskSchema() +task_list_schema = TaskSchema(many=True) diff --git a/openflexure_microscope/common/labthings/views/plugins.py b/openflexure_microscope/common/labthings/views/plugins.py deleted file mode 100644 index fa160b53..00000000 --- a/openflexure_microscope/common/labthings/views/plugins.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -Top-level representation of attached and enabled plugins -""" - -from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.utilities import blueprint_for_module - -from openflexure_microscope.common.labthings.find import registered_plugins -from openflexure_microscope.common.labthings.resource import Resource - -from flask import Blueprint, jsonify, url_for - -import copy -import logging -import warnings - - -def plugins_representation(plugin_dict): - """ - 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_name, plugin in plugin_dict.items(): - 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"PluginListResource") + "/" + 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 PluginListResource(Resource): - 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(registered_plugins())) - - -""" -def construct_blueprint(): - - blueprint = blueprint_for_module(__name__) - - for plugin_obj in registered_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")) - - # 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}", plugin=plugin - ), - **plugin_view["kwargs"], - ) - - return blueprint -""" diff --git a/openflexure_microscope/common/labthings_core/__init__.py b/openflexure_microscope/common/labthings_core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/openflexure_microscope/common/lock.py b/openflexure_microscope/common/labthings_core/lock.py similarity index 100% rename from openflexure_microscope/common/lock.py rename to openflexure_microscope/common/labthings_core/lock.py diff --git a/openflexure_microscope/common/tasks/__init__.py b/openflexure_microscope/common/labthings_core/tasks/__init__.py similarity index 100% rename from openflexure_microscope/common/tasks/__init__.py rename to openflexure_microscope/common/labthings_core/tasks/__init__.py diff --git a/openflexure_microscope/common/tasks/pool.py b/openflexure_microscope/common/labthings_core/tasks/pool.py similarity index 100% rename from openflexure_microscope/common/tasks/pool.py rename to openflexure_microscope/common/labthings_core/tasks/pool.py diff --git a/openflexure_microscope/common/tasks/thread.py b/openflexure_microscope/common/labthings_core/tasks/thread.py similarity index 100% rename from openflexure_microscope/common/tasks/thread.py rename to openflexure_microscope/common/labthings_core/tasks/thread.py diff --git a/openflexure_microscope/common/utilities.py b/openflexure_microscope/common/labthings_core/utilities.py similarity index 100% rename from openflexure_microscope/common/utilities.py rename to openflexure_microscope/common/labthings_core/utilities.py diff --git a/openflexure_microscope/devel/__init__.py b/openflexure_microscope/devel/__init__.py index 05e9df1e..d412d008 100644 --- a/openflexure_microscope/devel/__init__.py +++ b/openflexure_microscope/devel/__init__.py @@ -8,7 +8,7 @@ as well as some Flask imports to simplify API route development from openflexure_microscope.api.utilities import JsonResponse # Task management -from openflexure_microscope.common.tasks import ( +from openflexure_microscope.common.labthings_core.tasks import ( current_task, update_task_progress, update_task_data, diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 8c7ca5f8..de9cf00e 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -12,7 +12,7 @@ from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.camera.mock import MockStreamer from openflexure_microscope.utilities import serialise_array_b64 -from openflexure_microscope.common.lock import CompositeLock +from openflexure_microscope.common.labthings_core.lock import CompositeLock from openflexure_microscope.config import user_settings diff --git a/openflexure_microscope/microscope_settings.default.json b/openflexure_microscope/microscope_settings.default.json index 3660048a..bde62f47 100644 --- a/openflexure_microscope/microscope_settings.default.json +++ b/openflexure_microscope/microscope_settings.default.json @@ -3,11 +3,5 @@ 4100, 3146 ], - "jpeg_quality": 75, - "plugins": [ - "openflexure_microscope.plugins.default.autofocus:AutofocusPlugin", - "openflexure_microscope.plugins.default.scan:ScanPlugin", - "openflexure_microscope.plugins.default.camera_calibration:AutocalibrationPlugin", - "openflexure_microscope.plugins.default.zip_builder:ZipBuilderPlugin" - ] + "jpeg_quality": 75 } \ No newline at end of file diff --git a/openflexure_microscope/plugins/v2/autofocus.py b/openflexure_microscope/plugins/v2/autofocus.py index 71c3c938..8f748088 100644 --- a/openflexure_microscope/plugins/v2/autofocus.py +++ b/openflexure_microscope/plugins/v2/autofocus.py @@ -1,5 +1,5 @@ -from openflexure_microscope.common.labthings.find import find_device -from openflexure_microscope.common.labthings.plugins import BasePlugin +from openflexure_microscope.common.flask_labthings.find import find_device +from openflexure_microscope.common.flask_labthings.plugins import BasePlugin from openflexure_microscope.microscope import Microscope from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort diff --git a/openflexure_microscope/plugins/v2/scan.py b/openflexure_microscope/plugins/v2/scan.py index f774c2f8..adea2e54 100644 --- a/openflexure_microscope/plugins/v2/scan.py +++ b/openflexure_microscope/plugins/v2/scan.py @@ -6,8 +6,8 @@ from typing import Tuple from functools import reduce from openflexure_microscope.camera.base import generate_basename -from openflexure_microscope.common.labthings.find import find_device, find_plugin -from openflexure_microscope.common.labthings.plugins import BasePlugin +from openflexure_microscope.common.flask_labthings.find import find_device, find_plugin +from openflexure_microscope.common.flask_labthings.plugins import BasePlugin from openflexure_microscope.devel import ( JsonResponse, diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 45607a77..4b591994 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -1,6 +1,6 @@ import numpy as np from abc import ABCMeta, abstractmethod -from openflexure_microscope.common.lock import StrictLock +from openflexure_microscope.common.labthings_core.lock import StrictLock class BaseStage(metaclass=ABCMeta): diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 7fec2e36..0cf33c5c 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -41,26 +41,6 @@ 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: - return ds.strip() - 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()