Restructured labthings
This commit is contained in:
parent
206dd521ec
commit
a8a3cafa97
39 changed files with 275 additions and 269 deletions
|
|
@ -1 +1,2 @@
|
|||
from . import tasks, labthings
|
||||
from . import flask_labthings
|
||||
from openflexure_microscope.common.labthings_core import tasks
|
||||
|
|
|
|||
37
openflexure_microscope/common/flask_labthings/exceptions.py
Normal file
37
openflexure_microscope/common/flask_labthings/exceptions.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
from flask import jsonify, escape
|
||||
from werkzeug.exceptions import default_exceptions
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
|
||||
class JSONExceptionHandler(object):
|
||||
|
||||
"""
|
||||
A class to be registered as a Flask error handler,
|
||||
converts error codes into a JSON response
|
||||
"""
|
||||
|
||||
def __init__(self, app=None):
|
||||
if app:
|
||||
self.init_app(app)
|
||||
|
||||
def std_handler(self, error):
|
||||
if isinstance(error, HTTPException):
|
||||
message = error.description
|
||||
elif hasattr(error, "message"):
|
||||
message = error.message
|
||||
else:
|
||||
message = str(error)
|
||||
|
||||
status_code = error.code if isinstance(error, HTTPException) else 500
|
||||
|
||||
response = {"code": status_code, "message": escape(message)}
|
||||
return jsonify(response), status_code
|
||||
|
||||
def init_app(self, app):
|
||||
self.app = app
|
||||
self.register(HTTPException)
|
||||
for code, v in default_exceptions.items():
|
||||
self.register(code)
|
||||
|
||||
def register(self, exception_or_code, handler=None):
|
||||
self.app.errorhandler(exception_or_code)(handler or self.std_handler)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
13
openflexure_microscope/common/flask_labthings/utilities.py
Normal file
13
openflexure_microscope/common/flask_labthings/utilities.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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()))
|
||||
|
|
@ -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="<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="<id>"),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(TaskResource)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
task_schema = TaskSchema()
|
||||
task_list_schema = TaskSchema(many=True)
|
||||
|
|
@ -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
|
||||
"""
|
||||
0
openflexure_microscope/common/labthings_core/__init__.py
Normal file
0
openflexure_microscope/common/labthings_core/__init__.py
Normal file
Loading…
Add table
Add a link
Reference in a new issue