Moved eV GUI stuff out of LabThings
This commit is contained in:
parent
3fef94dc95
commit
eeb15453d0
5 changed files with 169 additions and 33 deletions
113
openflexure_microscope/api/example_extensions/ev_gui.py
Normal file
113
openflexure_microscope/api/example_extensions/ev_gui.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
from openflexure_microscope.common.flask_labthings.resource import Resource
|
||||||
|
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities.gui import expand_routes
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
# Some value that will change over time
|
||||||
|
# Here, we add 1 to it every time a GET request is made
|
||||||
|
val_int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def dynamic_form():
|
||||||
|
return {
|
||||||
|
"id": "test-plugin",
|
||||||
|
"icon": "pets",
|
||||||
|
"forms": [
|
||||||
|
{
|
||||||
|
"name": "Simple request",
|
||||||
|
"isCollapsible": False,
|
||||||
|
"isTask": False,
|
||||||
|
"selfUpdate": True,
|
||||||
|
"route": "/do",
|
||||||
|
"submitLabel": "Do things",
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"fieldType": "numberInput",
|
||||||
|
"name": "val_int",
|
||||||
|
"label": "Number value",
|
||||||
|
"minValue": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldType": "htmlBlock",
|
||||||
|
"name": "html_block",
|
||||||
|
"content": f"<i>Value is: </i><br>{val_int}.", # We dynamically use the val_int variable
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Alternate form without any dynamic parts
|
||||||
|
static_form = {
|
||||||
|
"id": "test-plugin",
|
||||||
|
"icon": "pets",
|
||||||
|
"forms": [
|
||||||
|
{
|
||||||
|
"name": "Simple request",
|
||||||
|
"isCollapsible": False,
|
||||||
|
"isTask": False,
|
||||||
|
"selfUpdate": True,
|
||||||
|
"route": "/do",
|
||||||
|
"submitLabel": "Do things",
|
||||||
|
"schema": [
|
||||||
|
{
|
||||||
|
"fieldType": "numberInput",
|
||||||
|
"name": "val_int",
|
||||||
|
"label": "Number value",
|
||||||
|
"minValue": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldType": "htmlBlock",
|
||||||
|
"name": "html_block",
|
||||||
|
"content": f"<i>Value is fixed</i>.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestAPIView(Resource):
|
||||||
|
def get(self):
|
||||||
|
global val_int
|
||||||
|
val_int += 1
|
||||||
|
return jsonify({"val": True})
|
||||||
|
|
||||||
|
|
||||||
|
class TestDoAPIView(Resource):
|
||||||
|
def post(self):
|
||||||
|
global val_int
|
||||||
|
val_int += 1
|
||||||
|
return jsonify({"val": True})
|
||||||
|
|
||||||
|
|
||||||
|
# Using the dynamic form
|
||||||
|
dynamic_test_extension_v2 = BaseExtension("dynamic_test_extension")
|
||||||
|
dynamic_test_extension_v2.add_view(
|
||||||
|
TestAPIView, "/get", endpoint="dynamic_test_extension_get"
|
||||||
|
)
|
||||||
|
dynamic_test_extension_v2.add_view(
|
||||||
|
TestDoAPIView, "/do", endpoint="dynamic_test_extension_do"
|
||||||
|
)
|
||||||
|
|
||||||
|
dynamic_test_extension_v2.add_meta(
|
||||||
|
"gui", expand_routes(dynamic_form, dynamic_test_extension_v2)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Using the static form
|
||||||
|
static_test_extension_v2 = BaseExtension("static_test_extension")
|
||||||
|
static_test_extension_v2.add_view(
|
||||||
|
TestAPIView, "/get", endpoint="static_test_extension_get"
|
||||||
|
)
|
||||||
|
static_test_extension_v2.add_view(
|
||||||
|
TestDoAPIView, "/do", endpoint="static_test_extension_do"
|
||||||
|
)
|
||||||
|
static_test_extension_v2.add_meta(
|
||||||
|
"gui", expand_routes(static_form, static_test_extension_v2)
|
||||||
|
)
|
||||||
|
|
@ -4,6 +4,8 @@ import errno
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from flask import url_for, Blueprint, current_app
|
from flask import url_for, Blueprint, current_app
|
||||||
|
|
||||||
|
from . import gui
|
||||||
|
|
||||||
|
|
||||||
def view_class_from_endpoint(endpoint: str):
|
def view_class_from_endpoint(endpoint: str):
|
||||||
return current_app.view_functions[endpoint].view_class
|
return current_app.view_functions[endpoint].view_class
|
||||||
41
openflexure_microscope/api/utilities/gui.py
Normal file
41
openflexure_microscope/api/utilities/gui.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def expand_routes_from_dict(gui_description, extension_object):
|
||||||
|
api_gui = copy.deepcopy(gui_description)
|
||||||
|
if "forms" in gui_description and isinstance(api_gui["forms"], list):
|
||||||
|
for form in api_gui["forms"]:
|
||||||
|
if "route" in form and form["route"] in extension_object._rules.keys():
|
||||||
|
form["route"] = extension_object._rules[form["route"]]["rule"]
|
||||||
|
else:
|
||||||
|
logging.warn(
|
||||||
|
"No valid expandable route found for {}".format(form["route"])
|
||||||
|
)
|
||||||
|
return api_gui
|
||||||
|
|
||||||
|
|
||||||
|
def expand_routes_from_func(func, extension_object):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
return expand_routes_from_dict(func(*args, **kwargs), extension_object)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def expand_routes(gui_description, extension_object):
|
||||||
|
# If given a function that generates a GUI dictionary
|
||||||
|
if callable(gui_description):
|
||||||
|
# Wrap in the route expander
|
||||||
|
return expand_routes_from_func(gui_description, extension_object)
|
||||||
|
# If given a dictionary directly
|
||||||
|
elif isinstance(gui_description, dict):
|
||||||
|
# Build a GUI generator function
|
||||||
|
def gui_description_func():
|
||||||
|
return gui_description
|
||||||
|
|
||||||
|
# Wrap in the route expander
|
||||||
|
return expand_routes_from_func(gui_description_func, extension_object)
|
||||||
|
else:
|
||||||
|
raise RuntimeError("GUI description must be a function or a dictionary")
|
||||||
|
|
@ -26,6 +26,7 @@ class BaseExtension:
|
||||||
{}
|
{}
|
||||||
) # Key: Full, Python-safe ID. Val: Original rule, and view class
|
) # Key: Full, Python-safe ID. Val: Original rule, and view class
|
||||||
self._rules = {} # Key: Original rule. Val: View class
|
self._rules = {} # Key: Original rule. Val: View class
|
||||||
|
self._meta = {} # Extra metadata to add to the extension description
|
||||||
self._gui = None
|
self._gui = None
|
||||||
|
|
||||||
self._cls = str(self) # String description of extension instance
|
self._cls = str(self) # String description of extension instance
|
||||||
|
|
@ -71,36 +72,17 @@ class BaseExtension:
|
||||||
self.properties.append(view_class)
|
self.properties.append(view_class)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gui(self):
|
def meta(self):
|
||||||
# Handle missing/no GUI
|
d = {}
|
||||||
if not self._gui:
|
for k, v in self._meta.items():
|
||||||
return None
|
if callable(v):
|
||||||
# Handle GUI as a dictionary-returning callable function
|
d[k] = v()
|
||||||
elif isinstance(self._gui, collections.Callable):
|
else:
|
||||||
api_gui = self._gui()
|
d[k] = v
|
||||||
# Handle GUI as a static dictionary
|
return d
|
||||||
elif isinstance(self._gui, dict):
|
|
||||||
api_gui = copy.deepcopy(self._gui)
|
|
||||||
# Handle borked GUI
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
"GUI must be a dictionary, or a dictionary-returning function with no arguments."
|
|
||||||
)
|
|
||||||
|
|
||||||
api_gui["id"] = self._name
|
def add_meta(self, key, val):
|
||||||
|
self._meta[key] = val
|
||||||
if "forms" in api_gui and isinstance(api_gui["forms"], list):
|
|
||||||
for form in api_gui["forms"]:
|
|
||||||
if "route" in form and form["route"] in self._rules.keys():
|
|
||||||
form["route"] = self._rules[form["route"]]["rule"]
|
|
||||||
else:
|
|
||||||
logging.warn(
|
|
||||||
"No valid expandable route found for {}".format(form["route"])
|
|
||||||
)
|
|
||||||
return api_gui
|
|
||||||
|
|
||||||
def set_gui(self, form_dictionary: dict):
|
|
||||||
self._gui = form_dictionary
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _name(self):
|
def _name(self):
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class ExtensionSchema(Schema):
|
||||||
name = fields.String(data_key="title")
|
name = fields.String(data_key="title")
|
||||||
_name_python_safe = fields.String(data_key="pythonName")
|
_name_python_safe = fields.String(data_key="pythonName")
|
||||||
_cls = fields.String(data_key="pythonObject")
|
_cls = fields.String(data_key="pythonObject")
|
||||||
gui = fields.Dict()
|
meta = fields.Dict()
|
||||||
description = fields.String()
|
description = fields.String()
|
||||||
|
|
||||||
links = fields.Dict()
|
links = fields.Dict()
|
||||||
|
|
@ -81,12 +81,10 @@ class ExtensionSchema(Schema):
|
||||||
d = {}
|
d = {}
|
||||||
for view_id, view_data in data.views.items():
|
for view_id, view_data in data.views.items():
|
||||||
view_cls = view_data["view"]
|
view_cls = view_data["view"]
|
||||||
view_kwargs = view_data["kwargs"]
|
|
||||||
view_rule = view_data["rule"]
|
view_rule = view_data["rule"]
|
||||||
# Make links dictionary if it doesn't yet exist
|
# Make links dictionary if it doesn't yet exist
|
||||||
d[view_id] = {
|
d[view_id] = {
|
||||||
"href": url_for(EXTENSION_LIST_ENDPOINT, **view_kwargs, _external=True)
|
"href": url_for(EXTENSION_LIST_ENDPOINT, _external=True) + view_rule,
|
||||||
+ view_rule,
|
|
||||||
**description_from_view(view_cls),
|
**description_from_view(view_cls),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue