Added base plugin resource
This commit is contained in:
parent
469e8218e0
commit
2b5b0b8744
8 changed files with 299 additions and 60 deletions
|
|
@ -20,7 +20,10 @@ from openflexure_microscope.config import settings_file_path, JSONEncoder
|
||||||
from openflexure_microscope.api.v1 import blueprints
|
from openflexure_microscope.api.v1 import blueprints
|
||||||
from openflexure_microscope.api import v2
|
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
|
from openflexure_microscope.api.microscope import default_microscope as api_microscope
|
||||||
|
|
||||||
|
|
@ -78,30 +81,16 @@ handler = JSONExceptionHandler(app)
|
||||||
|
|
||||||
# Attach lab devices
|
# Attach lab devices
|
||||||
labthing = LabThing(app)
|
labthing = LabThing(app)
|
||||||
|
labthing.url_prefix = "/api/v2b"
|
||||||
|
labthing.description = "Test LabThing-based API for OpenFlexure Microscope"
|
||||||
|
|
||||||
labthing.register_device(api_microscope, "openflexure_microscope")
|
labthing.register_device(api_microscope, "openflexure_microscope")
|
||||||
|
|
||||||
|
for _plugin in find_plugins(USER_PLUGINS_PATH):
|
||||||
|
labthing.register_plugin(_plugin)
|
||||||
|
|
||||||
# WEBAPP ROUTES
|
# 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
|
### V2
|
||||||
# Root routes
|
# Root routes
|
||||||
v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope)
|
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"))
|
app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2"))
|
||||||
|
|
||||||
# Plugins routes
|
# Plugins routes
|
||||||
v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope)
|
# v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
|
# app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
|
||||||
|
|
||||||
# Tasks routes
|
# Tasks routes
|
||||||
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
|
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1 @@
|
||||||
from flask import current_app, _app_ctx_stack
|
|
||||||
|
|
||||||
EXTENSION_NAME = "flask-lab"
|
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
|
|
||||||
|
|
|
||||||
32
openflexure_microscope/common/labthings/find.py
Normal file
32
openflexure_microscope/common/labthings/find.py
Normal file
|
|
@ -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
|
||||||
139
openflexure_microscope/common/labthings/labthing.py
Normal file
139
openflexure_microscope/common/labthings/labthing.py
Normal file
|
|
@ -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)
|
||||||
9
openflexure_microscope/common/labthings/resource.py
Normal file
9
openflexure_microscope/common/labthings/resource.py
Normal file
|
|
@ -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)
|
||||||
106
openflexure_microscope/common/labthings/views/plugins.py
Normal file
106
openflexure_microscope/common/labthings/views/plugins.py
Normal file
|
|
@ -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
|
||||||
|
"""
|
||||||
|
|
@ -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.common.labthings.plugins import BasePlugin
|
||||||
from openflexure_microscope.microscope import Microscope
|
from openflexure_microscope.microscope import Microscope
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue