Added base plugin resource

This commit is contained in:
Joel Collins 2019-12-16 16:20:13 +00:00
parent 469e8218e0
commit 2b5b0b8744
8 changed files with 299 additions and 60 deletions

View file

@ -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

View 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

View 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)

View 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)

View 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
"""