From 2b5b0b874404c2fa3810bc3ab96aa1e6385f8f95 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Mon, 16 Dec 2019 16:20:13 +0000 Subject: [PATCH] Added base plugin resource --- openflexure_microscope/api/app.py | 35 ++--- .../common/labthings/__init__.py | 36 ----- .../common/labthings/find.py | 32 ++++ .../common/labthings/labthing.py | 139 ++++++++++++++++++ .../common/labthings/resource.py | 9 ++ .../labthings/{views.py => views/__init__.py} | 0 .../common/labthings/views/plugins.py | 106 +++++++++++++ .../plugins/v2/autofocus.py | 2 +- 8 files changed, 299 insertions(+), 60 deletions(-) create mode 100644 openflexure_microscope/common/labthings/find.py create mode 100644 openflexure_microscope/common/labthings/labthing.py create mode 100644 openflexure_microscope/common/labthings/resource.py rename openflexure_microscope/common/labthings/{views.py => views/__init__.py} (100%) create mode 100644 openflexure_microscope/common/labthings/views/plugins.py diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 389afed7..5917f0dc 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -20,7 +20,10 @@ from openflexure_microscope.config import settings_file_path, JSONEncoder from openflexure_microscope.api.v1 import blueprints from openflexure_microscope.api import v2 -from openflexure_microscope.common.labthings import LabThing +from openflexure_microscope.common.labthings.labthing import LabThing +from openflexure_microscope.common.labthings.find import registered_plugins +from openflexure_microscope.common.labthings.plugins import find_plugins +from openflexure_microscope.config import USER_PLUGINS_PATH from openflexure_microscope.api.microscope import default_microscope as api_microscope @@ -78,30 +81,16 @@ handler = JSONExceptionHandler(app) # Attach lab devices labthing = LabThing(app) +labthing.url_prefix = "/api/v2b" +labthing.description = "Test LabThing-based API for OpenFlexure Microscope" + labthing.register_device(api_microscope, "openflexure_microscope") +for _plugin in find_plugins(USER_PLUGINS_PATH): + labthing.register_plugin(_plugin) + # WEBAPP ROUTES -# Base routes -base_blueprint = blueprints.base.construct_blueprint(api_microscope) -app.register_blueprint(base_blueprint, url_prefix=uri("", "v1")) - -# Stage routes -stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) -app.register_blueprint(stage_blueprint, url_prefix=uri("/stage", "v1")) - -# Camera routes -camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) -app.register_blueprint(camera_blueprint, url_prefix=uri("/camera", "v1")) - -# Plugin routes -plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) -app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1")) - -# Task routes -task_blueprint = blueprints.task.construct_blueprint(api_microscope) -app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1")) - ### V2 # Root routes v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope) @@ -123,8 +112,8 @@ 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")) +# 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) diff --git a/openflexure_microscope/common/labthings/__init__.py b/openflexure_microscope/common/labthings/__init__.py index 0ed5c46d..3b4b1d5b 100644 --- a/openflexure_microscope/common/labthings/__init__.py +++ b/openflexure_microscope/common/labthings/__init__.py @@ -1,37 +1 @@ -from flask import current_app, _app_ctx_stack - EXTENSION_NAME = "flask-lab" - - -class LabThing(object): - def __init__(self, app=None): - self.app = app - self.devices = {} - if app is not None: - self.init_app(app) - - def init_app(self, app): - app.teardown_appcontext(self.teardown) - - app.extensions = getattr(app, "extensions", {}) - app.extensions[EXTENSION_NAME] = self - - def teardown(self, exception): - print(f"Tearing down devices: {self.devices}") - - def register_device(self, device_object, device_name: str): - self.devices[device_name] = device_object - - -def find_device(device_name): - app = current_app - - if not app: - return None - - pylot_instance = app.extensions[EXTENSION_NAME] - - if device_name in pylot_instance.devices: - return pylot_instance.devices[device_name] - else: - return None diff --git a/openflexure_microscope/common/labthings/find.py b/openflexure_microscope/common/labthings/find.py new file mode 100644 index 00000000..b5b6a7e0 --- /dev/null +++ b/openflexure_microscope/common/labthings/find.py @@ -0,0 +1,32 @@ +from flask import current_app + +from . import EXTENSION_NAME + + +def _current_labthing(): + app = current_app + if not app: + return None + return app.extensions[EXTENSION_NAME] + + +def registered_plugins(labthing_instance=None): + if not labthing_instance: + labthing_instance = _current_labthing() + return labthing_instance.plugins + + +def registered_devices(labthing_instance=None): + if not labthing_instance: + labthing_instance = _current_labthing() + return labthing_instance.devices + + +def find_device(device_name, labthing_instance=None): + if not labthing_instance: + labthing_instance = _current_labthing() + + if device_name in labthing_instance.devices: + return labthing_instance.devices[device_name] + else: + return None diff --git a/openflexure_microscope/common/labthings/labthing.py b/openflexure_microscope/common/labthings/labthing.py new file mode 100644 index 00000000..7ea2454d --- /dev/null +++ b/openflexure_microscope/common/labthings/labthing.py @@ -0,0 +1,139 @@ +from flask import current_app, _app_ctx_stack, request + +from .plugins import BasePlugin +from .views.plugins import PluginListResource + +from . import EXTENSION_NAME + + +class LabThing(object): + def __init__(self, app=None, url_prefix="", description=""): + self.app = app + + self.devices = {} + + self.plugins = {} + + self.resources = [] + self.endpoints = set() + + self.url_prefix = url_prefix + self.description = description + + if app is not None: + self.init_app(app) + + ### Flask stuff + + def init_app(self, app): + app.teardown_appcontext(self.teardown) + + app.extensions = getattr(app, "extensions", {}) + app.extensions[EXTENSION_NAME] = self + + self._create_base_routes() + + def teardown(self, exception): + print(f"Tearing down devices: {self.devices}") + + def _create_base_routes(self): + self.add_resource(PluginListResource, "/plugins") + + ### Device stuff + + def register_device(self, device_object, device_name: str): + 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 + else: + raise TypeError("Plugin object must be an instance of BasePlugin") + + ### Resource stuff + + def _complete_url(self, url_part, registration_prefix): + """This method is used to defer the construction of the final url in + the case that the Api is created with a Blueprint. + :param url_part: The part of the url the endpoint is registered with + :param registration_prefix: The part of the url contributed by the + blueprint. Generally speaking, BlueprintSetupState.url_prefix + """ + parts = [registration_prefix, self.url_prefix, url_part] + return "".join([part for part in parts if part]) + + def add_resource(self, resource, *urls, **kwargs): + """Adds a resource to the api. + :param resource: the class name of your resource + :type resource: :class:`Type[Resource]` + :param urls: one or more url routes to match for the resource, standard + 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` + 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 + the resource. + :type resource_class_args: tuple + :param resource_class_kwargs: kwargs to be forwarded to the constructor + of the resource. + :type resource_class_kwargs: dict + Additional keyword arguments not specified above will be passed as-is + to :meth:`flask.Flask.add_url_rule`. + Examples:: + api.add_resource(HelloWorld, '/', '/hello') + api.add_resource(Foo, '/foo', endpoint="foo") + api.add_resource(FooSpecial, '/special/foo', endpoint="foo") + """ + if self.app is not None: + self._register_view(self.app, resource, *urls, **kwargs) + else: + self.resources.append((resource, urls, kwargs)) + + def resource(self, *urls, **kwargs): + """Wraps a :class:`~flask_restful.Resource` class, adding it to the + api. Parameters are the same as :meth:`~flask_restful.Api.add_resource`. + Example:: + app = Flask(__name__) + api = restful.Api(app) + @api.resource('/foo') + class Foo(Resource): + def get(self): + return 'Hello, World!' + """ + + def decorator(cls): + self.add_resource(cls, *urls, **kwargs) + return cls + + return decorator + + def _register_view(self, app, resource, *urls, **kwargs): + endpoint = kwargs.pop("endpoint", None) or resource.__name__.lower() + self.endpoints.add(endpoint) + resource_class_args = kwargs.pop("resource_class_args", ()) + resource_class_kwargs = kwargs.pop("resource_class_kwargs", {}) + + # NOTE: 'view_functions' is cleaned up from Blueprint class in Flask 1.0 + if endpoint in getattr(app, "view_functions", {}): + previous_view_class = app.view_functions[endpoint].__dict__["view_class"] + + # if you override the endpoint with a different class, avoid the collision by raising an exception + if previous_view_class != resource: + raise ValueError( + "This endpoint (%s) is already set to the class %s." + % (endpoint, previous_view_class.__name__) + ) + + resource.endpoint = endpoint + resource_func = resource.as_view( + endpoint, *resource_class_args, **resource_class_kwargs + ) + + for url in urls: + # If we've got no Blueprint, just build a url with no prefix + rule = self._complete_url(url, "") + # Add the url to the application or blueprint + app.add_url_rule(rule, view_func=resource_func, **kwargs) diff --git a/openflexure_microscope/common/labthings/resource.py b/openflexure_microscope/common/labthings/resource.py new file mode 100644 index 00000000..fc902d3e --- /dev/null +++ b/openflexure_microscope/common/labthings/resource.py @@ -0,0 +1,9 @@ +from flask.views import MethodView + + +class Resource(MethodView): + """Currently identical to MethodView + """ + + def __init__(self, *args, **kwargs): + MethodView.__init__(self, *args, **kwargs) diff --git a/openflexure_microscope/common/labthings/views.py b/openflexure_microscope/common/labthings/views/__init__.py similarity index 100% rename from openflexure_microscope/common/labthings/views.py rename to openflexure_microscope/common/labthings/views/__init__.py diff --git a/openflexure_microscope/common/labthings/views/plugins.py b/openflexure_microscope/common/labthings/views/plugins.py new file mode 100644 index 00000000..dabc1180 --- /dev/null +++ b/openflexure_microscope/common/labthings/views/plugins.py @@ -0,0 +1,106 @@ +""" +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/plugins/v2/autofocus.py b/openflexure_microscope/plugins/v2/autofocus.py index 9f8e745c..875bb58d 100644 --- a/openflexure_microscope/plugins/v2/autofocus.py +++ b/openflexure_microscope/plugins/v2/autofocus.py @@ -1,4 +1,4 @@ -from openflexure_microscope.common.labthings import find_device +from openflexure_microscope.common.labthings.find import find_device from openflexure_microscope.common.labthings.plugins import BasePlugin from openflexure_microscope.microscope import Microscope