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),
}