diff --git a/openflexure_microscope/api/example_extensions/ev_gui.py b/openflexure_microscope/api/example_extensions/ev_gui.py new file mode 100644 index 00000000..aa950e4b --- /dev/null +++ b/openflexure_microscope/api/example_extensions/ev_gui.py @@ -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"Value is:
{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"Value is fixed.", + }, + ], + } + ], +} + + +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) +) diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities/__init__.py similarity index 99% rename from openflexure_microscope/api/utilities.py rename to openflexure_microscope/api/utilities/__init__.py index 1e29cc3a..4b5a7cee 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities/__init__.py @@ -4,6 +4,8 @@ import errno from werkzeug.exceptions import BadRequest from flask import url_for, Blueprint, current_app +from . import gui + def view_class_from_endpoint(endpoint: str): return current_app.view_functions[endpoint].view_class diff --git a/openflexure_microscope/api/utilities/gui.py b/openflexure_microscope/api/utilities/gui.py new file mode 100644 index 00000000..170f8657 --- /dev/null +++ b/openflexure_microscope/api/utilities/gui.py @@ -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") diff --git a/openflexure_microscope/common/flask_labthings/extensions.py b/openflexure_microscope/common/flask_labthings/extensions.py index 53b61aec..aa75e910 100644 --- a/openflexure_microscope/common/flask_labthings/extensions.py +++ b/openflexure_microscope/common/flask_labthings/extensions.py @@ -26,6 +26,7 @@ class BaseExtension: {} ) # Key: Full, Python-safe ID. Val: Original rule, and view class self._rules = {} # Key: Original rule. Val: View class + self._meta = {} # Extra metadata to add to the extension description self._gui = None self._cls = str(self) # String description of extension instance @@ -71,36 +72,17 @@ class BaseExtension: self.properties.append(view_class) @property - def gui(self): - # Handle missing/no GUI - if not self._gui: - return None - # Handle GUI as a dictionary-returning callable function - elif isinstance(self._gui, collections.Callable): - api_gui = self._gui() - # Handle GUI as a static dictionary - 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." - ) + def meta(self): + d = {} + for k, v in self._meta.items(): + if callable(v): + d[k] = v() + else: + d[k] = v + return d - api_gui["id"] = self._name - - 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 + def add_meta(self, key, val): + self._meta[key] = val @property def _name(self): diff --git a/openflexure_microscope/common/flask_labthings/schema.py b/openflexure_microscope/common/flask_labthings/schema.py index bc2691b5..7a8166e7 100644 --- a/openflexure_microscope/common/flask_labthings/schema.py +++ b/openflexure_microscope/common/flask_labthings/schema.py @@ -71,7 +71,7 @@ class ExtensionSchema(Schema): name = fields.String(data_key="title") _name_python_safe = fields.String(data_key="pythonName") _cls = fields.String(data_key="pythonObject") - gui = fields.Dict() + meta = fields.Dict() description = fields.String() links = fields.Dict() @@ -81,12 +81,10 @@ class ExtensionSchema(Schema): d = {} for view_id, view_data in data.views.items(): view_cls = view_data["view"] - view_kwargs = view_data["kwargs"] view_rule = view_data["rule"] # Make links dictionary if it doesn't yet exist d[view_id] = { - "href": url_for(EXTENSION_LIST_ENDPOINT, **view_kwargs, _external=True) - + view_rule, + "href": url_for(EXTENSION_LIST_ENDPOINT, _external=True) + view_rule, **description_from_view(view_cls), }