Plugin schema are now just plugin forms (requires eV 1.2)

This commit is contained in:
Joel Collins 2019-09-14 15:51:51 +01:00
parent 0d7a5f71e0
commit d9e07f913a
16 changed files with 106 additions and 98 deletions

View file

@ -8,6 +8,6 @@ Developing Plugins
./plugins/structure.rst ./plugins/structure.rst
./plugins/routes.rst ./plugins/routes.rst
./plugins/hardware.rst ./plugins/hardware.rst
./plugins/schema.rst ./plugins/form.rst
./plugins/example.rst ./plugins/example.rst
./plugins/class.rst ./plugins/class.rst

View file

@ -7,10 +7,10 @@ plugin.py
.. literalinclude:: example/plugin.py .. literalinclude:: example/plugin.py
:language: python :language: python
schema.json form.json
+++++++++++ +++++++++++
.. literalinclude:: example/schema.json .. literalinclude:: example/form.json
:language: JSON :language: JSON
Notes Notes

View file

@ -7,7 +7,7 @@
"isTask": false, "isTask": false,
"selfUpdate": true, "selfUpdate": true,
"route": "/hello", "route": "/hello",
"schema": [ "form": [
{ {
"fieldType": "textInput", "fieldType": "textInput",
"placeholder": "Enter a string", "placeholder": "Enter a string",
@ -21,7 +21,7 @@
"isTask": true, "isTask": true,
"route": "/timelapse", "route": "/timelapse",
"submitLabel": "Start timelapse", "submitLabel": "Start timelapse",
"schema": [ "form": [
{ {
"fieldType": "numberInput", "fieldType": "numberInput",
"name": "n_images", "name": "n_images",

View file

@ -9,7 +9,7 @@ import json
from flask import request, Response, escape, jsonify from flask import request, Response, escape, jsonify
HERE = os.path.dirname(os.path.realpath(__file__)) HERE = os.path.dirname(os.path.realpath(__file__))
SCHEMA_PATH = os.path.join(HERE, "schema.json") FORM_PATH = os.path.join(HERE, "forms.json")
### MICROSCOPE PLUGIN ### ### MICROSCOPE PLUGIN ###
@ -18,10 +18,10 @@ class MyPluginClass(MicroscopePlugin):
A set of default plugins A set of default plugins
""" """
global SCHEMA_PATH global FORM_PATH
with open(SCHEMA_PATH, 'r') as sc: with open(FORM_PATH, 'r') as sc:
api_schema = json.load(sc) api_form = json.load(sc)
api_views = { api_views = {
'/identify': IdentifyAPI, '/identify': IdentifyAPI,

View file

@ -4,39 +4,39 @@ Adding a GUI
Introduction Introduction
------------ ------------
In order to bind user-interface elements to your plugin, an ``api_schema`` variable must be included in your microscope plugin, similar to your ``api_views``. In order to bind user-interface elements to your plugin, an ``api_form`` variable must be included in your microscope plugin, similar to your ``api_views``.
This variable must be in the form of a dictionary, with all data able to be parsed into JSON. Because of this requirement, it is suggested that you create your API schema as a JSON file, and load that file as a dictionary into your plugin. For example: This variable must be in the form of a dictionary, with all data able to be parsed into JSON. Because of this requirement, it is suggested that you create your API form as a JSON file, and load that file as a dictionary into your plugin. For example:
.. code-block:: python .. code-block:: python
import json import json
HERE = os.path.dirname(os.path.realpath(__file__)) # Find the full path of your plugin Python file HERE = os.path.dirname(os.path.realpath(__file__)) # Find the full path of your plugin Python file
SCHEMA_PATH = os.path.join(HERE, "schema.json") # Find the full path of the adjascent JSON file FORM_PATH = os.path.join(HERE, "form.json") # Find the full path of the adjascent JSON file
class MyPluginClass(MicroscopePlugin): class MyPluginClass(MicroscopePlugin):
with open(SCHEMA_PATH, 'r') as sc: # Open the JSON file with open(FORM_PATH, 'r') as sc: # Open the JSON file
api_schema = json.load(sc) # Load the JSON file into the api_schema dictionary api_form = json.load(sc) # Load the JSON file into the api_form dictionary
Throughout this documentation, all example of ``api_schema`` sections will be in JSON format. Keep in mind however that it is possible to directly define your ``api_schema`` as a dictionary, without loading an external file. Throughout this documentation, all example of ``api_form`` sections will be in JSON format. Keep in mind however that it is possible to directly define your ``api_form`` as a dictionary, without loading an external file.
Forms from ``api_schema`` Forms from ``api_form``
------------------------- -------------------------
The ``api_schema`` object essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values. A form can update it's values by sending a GET request to the API route bound to that form, and can send it's current values via a POST request to *this same API route*. The ``api_form`` object essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values. A form can update it's values by sending a GET request to the API route bound to that form, and can send it's current values via a POST request to *this same API route*.
Each component in the form has a ``name`` property, which must match up to a property your API route expects in JSON POST requests, and returns in JSON GET requests. Each component in the form has a ``name`` property, which must match up to a property your API route expects in JSON POST requests, and returns in JSON GET requests.
Structure of ``api_schema`` Structure of ``api_form``
--------------------------- ---------------------------
Root level Root level
++++++++++ ++++++++++
The root of your ``api_schema`` expects 3 properties: The root of your ``api_form`` expects 3 properties:
``id`` - A unique ID to give your client-side plugin ``id`` - A unique ID to give your client-side plugin
@ -61,7 +61,7 @@ Each form is described by a JSON object, with the following properties:
``submitLabel`` *(optional)* - String to place inside of the form's submit button ``submitLabel`` *(optional)* - String to place inside of the form's submit button
``schema`` - An array of form components as described below ``form`` - An array of form components as described below
Component level Component level
+++++++++++++++ +++++++++++++++
@ -182,11 +182,11 @@ Overview of components
JSON form example JSON form example
----------------- -----------------
.. literalinclude:: schema_example.json .. literalinclude:: forms_example.json
:language: JSON :language: JSON
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
./schema/json_schema.rst ./form/json_form.rst

View file

@ -9,7 +9,7 @@
"selfUpdate": true, "selfUpdate": true,
"route": "/do", "route": "/do",
"submitLabel": "Do things", "submitLabel": "Do things",
"schema": [ "form": [
{ {
"fieldType": "numberInput", "fieldType": "numberInput",
"placeholder": "Some integer", "placeholder": "Some integer",
@ -75,7 +75,7 @@
"selfUpdate": true, "selfUpdate": true,
"route": "/task", "route": "/task",
"submitLabel": "Start task", "submitLabel": "Start task",
"schema": [ "form": [
{ {
"fieldType": "numberInput", "fieldType": "numberInput",
"name": "run_time", "name": "run_time",

View file

@ -1,5 +1,5 @@
Full JSON Schema Full JSON Form
================ ================
.. literalinclude:: schema.json .. literalinclude:: form.json
:language: JSON :language: JSON

View file

@ -1,9 +1,9 @@
{ {
"definitions": {}, "definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#", "$form": "http://json-form.org/draft-07/form#",
"$id": "http://example.com/root.json", "$id": "http://example.com/root.json",
"type": "object", "type": "object",
"title": "The Root Schema", "title": "The Root Form",
"required": [ "required": [
"id", "id",
"icon", "icon",
@ -41,11 +41,11 @@
"items": { "items": {
"$id": "#/properties/forms/items", "$id": "#/properties/forms/items",
"type": "object", "type": "object",
"title": "The Items Schema", "title": "The Items Form",
"required": [ "required": [
"name", "name",
"route", "route",
"schema" "form"
], ],
"properties": { "properties": {
"name": { "name": {
@ -62,7 +62,7 @@
"isCollapsible": { "isCollapsible": {
"$id": "#/properties/forms/items/properties/isCollapsible", "$id": "#/properties/forms/items/properties/isCollapsible",
"type": "boolean", "type": "boolean",
"title": "The isCollapsible Schema", "title": "The isCollapsible Form",
"description": "Should the form be rendered as a collapsible item", "description": "Should the form be rendered as a collapsible item",
"default": false, "default": false,
"examples": [ "examples": [
@ -72,7 +72,7 @@
"isTask": { "isTask": {
"$id": "#/properties/forms/items/properties/isTask", "$id": "#/properties/forms/items/properties/isTask",
"type": "boolean", "type": "boolean",
"title": "The isTask Schema", "title": "The isTask Form",
"description": "Should the form's submit function be treated as a long-running task", "description": "Should the form's submit function be treated as a long-running task",
"default": false, "default": false,
"examples": [ "examples": [
@ -82,7 +82,7 @@
"selfUpdate": { "selfUpdate": {
"$id": "#/properties/forms/items/properties/selfUpdate", "$id": "#/properties/forms/items/properties/selfUpdate",
"type": "boolean", "type": "boolean",
"title": "The selfUpdate Schema", "title": "The selfUpdate Form",
"description": "Should the form's component values be automatically updated with a GET request", "description": "Should the form's component values be automatically updated with a GET request",
"default": false, "default": false,
"examples": [ "examples": [
@ -92,7 +92,7 @@
"route": { "route": {
"$id": "#/properties/forms/items/properties/route", "$id": "#/properties/forms/items/properties/route",
"type": "string", "type": "string",
"title": "The Route Schema", "title": "The Route Form",
"description": "Form submit POST request's corresponding api_views route", "description": "Form submit POST request's corresponding api_views route",
"default": "", "default": "",
"examples": [ "examples": [
@ -103,7 +103,7 @@
"submitLabel": { "submitLabel": {
"$id": "#/properties/forms/items/properties/submitLabel", "$id": "#/properties/forms/items/properties/submitLabel",
"type": "string", "type": "string",
"title": "The submitLabel Schema", "title": "The submitLabel Form",
"description": "String to place in submit button", "description": "String to place in submit button",
"default": "Submit", "default": "Submit",
"examples": [ "examples": [
@ -111,12 +111,12 @@
], ],
"pattern": "^(.*)$" "pattern": "^(.*)$"
}, },
"schema": { "form": {
"$id": "#/properties/forms/items/properties/schema", "$id": "#/properties/forms/items/properties/form",
"type": "array", "type": "array",
"title": "Form component array", "title": "Form component array",
"items": { "items": {
"$id": "#/properties/forms/items/properties/schema/items", "$id": "#/properties/forms/items/properties/form/items",
"type": "object", "type": "object",
"title": "A form component", "title": "A form component",
"required": [ "required": [
@ -125,7 +125,7 @@
], ],
"properties": { "properties": {
"fieldType": { "fieldType": {
"$id": "#/properties/forms/items/properties/schema/items/properties/fieldType", "$id": "#/properties/forms/items/properties/form/items/properties/fieldType",
"type": "string", "type": "string",
"title": "Component type", "title": "Component type",
"default": "", "default": "",
@ -143,7 +143,7 @@
"pattern": "^(.*)$" "pattern": "^(.*)$"
}, },
"placeholder": { "placeholder": {
"$id": "#/properties/forms/items/properties/schema/items/properties/placeholder", "$id": "#/properties/forms/items/properties/form/items/properties/placeholder",
"type": "string", "type": "string",
"title": "Component placeholder value", "title": "Component placeholder value",
"default": "", "default": "",
@ -153,7 +153,7 @@
"pattern": "^(.*)$" "pattern": "^(.*)$"
}, },
"name": { "name": {
"$id": "#/properties/forms/items/properties/schema/items/properties/name", "$id": "#/properties/forms/items/properties/form/items/properties/name",
"type": "string", "type": "string",
"title": "Unique component name", "title": "Unique component name",
"examples": [ "examples": [
@ -162,7 +162,7 @@
"pattern": "^(.*)$" "pattern": "^(.*)$"
}, },
"label": { "label": {
"$id": "#/properties/forms/items/properties/schema/items/properties/label", "$id": "#/properties/forms/items/properties/form/items/properties/label",
"type": "string", "type": "string",
"title": "Component label (if applicable)", "title": "Component label (if applicable)",
"default": "", "default": "",
@ -172,7 +172,7 @@
"pattern": "^(.*)$" "pattern": "^(.*)$"
}, },
"value": { "value": {
"$id": "#/properties/forms/items/properties/schema/items/properties/value", "$id": "#/properties/forms/items/properties/form/items/properties/value",
"type": ["array", "boolean", "integer", "number", "object", "string"], "type": ["array", "boolean", "integer", "number", "object", "string"],
"title": "Component value", "title": "Component value",
"pattern": "^(.*)$" "pattern": "^(.*)$"

View file

@ -6,21 +6,21 @@ from openflexure_microscope.api.v1.views import MicroscopeView
import logging import logging
import warnings import warnings
class PluginSchemaAPI(MicroscopeView): class PluginFormAPI(MicroscopeView):
def get(self): def get(self):
""" """
Return the current plugin schemas Return the current plugin forms
.. :quickref: Plugin; Get schemas .. :quickref: Plugin; Get forms
Returns an array of present plugin schemas (describing plugin user interfaces.) 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 Please note, this is *not* a list of all enabled plugins, only those with associated
user interface forms. user interface forms.
A complete list of enabled plugins can be found in the microscope state. A complete list of enabled plugins can be found in the microscope state.
""" """
out = self.microscope.plugin.schemas out = self.microscope.plugin.forms
return jsonify(out) return jsonify(out)
@ -28,10 +28,10 @@ def construct_blueprint(microscope_obj):
blueprint = Blueprint('plugin_blueprint', __name__) blueprint = Blueprint('plugin_blueprint', __name__)
# Create a base route to return plugin API schemas, if any exist # Create a base route to return plugin API forms, if any exist
blueprint.add_url_rule( blueprint.add_url_rule(
'/', '/',
view_func=PluginSchemaAPI.as_view('plugin_api_schema', microscope=microscope_obj) view_func=PluginFormAPI.as_view('plugin_api_form', microscope=microscope_obj)
) )
all_routes = [] all_routes = []
@ -86,20 +86,21 @@ def construct_blueprint(microscope_obj):
) )
) )
# If plugin includes an API schema # If plugin includes an API form
if hasattr(plugin_obj, 'api_schema') and isinstance(plugin_obj.api_schema, dict): if hasattr(plugin_obj, 'api_form') and isinstance(plugin_obj.api_form, dict):
schema = plugin_obj.api_schema api_form_info = plugin_obj.api_form
# TODO: Validate schema? We need to make sure no single plugin can break all plugins. # TODO: Validate form? We need to make sure no single plugin can break all plugins.
schema['id'] = plugin_name api_form_info['id'] = plugin_name
if 'forms' in schema and isinstance(schema['forms'], list): if 'forms' in api_form_info and isinstance(api_form_info['forms'], list):
for form in schema['forms']: for form in api_form_info['forms']:
if 'route' in form and form['route'] in expanded_routes.keys(): if 'route' in form and form['route'] in expanded_routes.keys():
form['route'] = expanded_routes[form['route']] form['route'] = expanded_routes[form['route']]
else: else:
logging.warn("No valid expandable route found for {}".format(form['route'])) logging.warn("No valid expandable route found for {}".format(form['route']))
# Store the complete schema in Microscope().plugin.schema # Store the complete form in Microscope().plugin.form
microscope_obj.plugin.schemas.append(schema) microscope_obj.plugin.forms.append(api_form_info)
print(microscope_obj.plugin.forms)
else: else:
warnings.warn( warnings.warn(

View file

@ -107,7 +107,6 @@ class MockStreamer(BaseCamera):
# If stream was paused to update config, unpause # If stream was paused to update config, unpause
if paused_stream: if paused_stream:
logging.info("Resuming stream.") logging.info("Resuming stream.")
self.start_stream_recording()
else: else:
raise Exception( raise Exception(

View file

@ -94,26 +94,32 @@ class Microscope:
# Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists # Maybe even attach dummy hardware at __init__, and replace with real hardware if it exists
logging.debug("Attaching camera...") logging.debug("Attaching camera...")
self.camera = camera #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object self.camera = (
camera
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object
if not self.camera: if not self.camera:
logging.info("No camera attached.") logging.info("No camera attached.")
else: else:
logging.info("Attached camera {}".format(camera)) logging.info("Attached camera {}".format(camera))
if hasattr(self.camera, 'lock'): # If camera has a lock if hasattr(self.camera, "lock"): # If camera has a lock
logging.info("Attaching {} to composite lock.".format(self.camera.lock)) logging.info("Attaching {} to composite lock.".format(self.camera.lock))
# Add the lock to the microscope composite lock # Add the lock to the microscope composite lock
self.lock.locks.append(self.camera.lock) self.lock.locks.append(self.camera.lock)
logging.debug("Attaching stage...") logging.debug("Attaching stage...")
self.stage = stage #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object self.stage = (
stage
) #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object
if not self.stage: if not self.stage:
logging.info("No stage attached.") logging.info("No stage attached.")
else: else:
logging.info("Attached stage {}".format(stage)) logging.info("Attached stage {}".format(stage))
if hasattr(self.stage, 'lock'): # If stage object has a lock if hasattr(self.stage, "lock"): # If stage object has a lock
logging.info("Attaching lock {} to composite lock.".format(self.stage.lock)) logging.info(
"Attaching lock {} to composite lock.".format(self.stage.lock)
)
# Add the lock to the microscope composite lock # Add the lock to the microscope composite lock
self.lock.locks.append(self.stage.lock) self.lock.locks.append(self.stage.lock)
@ -149,10 +155,10 @@ class Microscope:
and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state` and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state`
""" """
state = { state = {
'camera': self.camera.state, "camera": self.camera.state,
'stage': self.stage.state, "stage": self.stage.state,
'plugin': self.plugin.state, "plugin": self.plugin.state,
'version': pkg_resources.get_distribution('openflexure_microscope').version "version": pkg_resources.get_distribution("openflexure_microscope").version,
} }
return state return state
@ -163,22 +169,22 @@ class Microscope:
logging.debug("Microscope: Applying config: {}".format(config)) logging.debug("Microscope: Applying config: {}".format(config))
# If attached to a camera # If attached to a camera
if ('camera_settings' in config) and self.camera: if ("camera_settings" in config) and self.camera:
self.camera.apply_config(config['camera_settings']) self.camera.apply_config(config["camera_settings"])
# If attached to a stage # If attached to a stage
if ('stage_settings' in config) and self.stage: if ("stage_settings" in config) and self.stage:
self.stage.apply_config(config['stage_settings']) self.stage.apply_config(config["stage_settings"])
# Todo: tidy up with some loopy goodness # Todo: tidy up with some loopy goodness
if 'id' in config: if "id" in config:
self.id = config['id'] self.id = config["id"]
if 'name' in config: if "name" in config:
self.name = config['name'] self.name = config["name"]
if 'fov' in config: if "fov" in config:
self.fov = config['fov'] self.fov = config["fov"]
if 'plugins' in config: if "plugins" in config:
self.plugin_maps = config['plugins'] self.plugin_maps = config["plugins"]
def read_config(self, json_safe=False): def read_config(self, json_safe=False):
""" """
@ -192,21 +198,21 @@ class Microscope:
""" """
settings_current = { settings_current = {
'id': self.id, "id": self.id,
'name': self.name, "name": self.name,
'fov': self.fov, "fov": self.fov,
'plugins': self.plugin_maps "plugins": self.plugin_maps,
} }
# If attached to a camera # If attached to a camera
if self.camera: if self.camera:
settings_current_camera = self.camera.read_config() settings_current_camera = self.camera.read_config()
settings_current['camera_settings'] = settings_current_camera settings_current["camera_settings"] = settings_current_camera
# If attached to a stage # If attached to a stage
if self.stage: if self.stage:
settings_current_stage = self.stage.read_config() settings_current_stage = self.stage.read_config()
settings_current['stage_settings'] = settings_current_stage settings_current["stage_settings"] = settings_current_stage
settings_full = self.settings_file.merge(settings_current) settings_full = self.settings_file.merge(settings_current)
@ -222,7 +228,9 @@ class Microscope:
# Read curent config # Read curent config
current_config = self.read_config() current_config = self.read_config()
# Merge in server version responsible for saving the config file # Merge in server version responsible for saving the config file
current_config['server_version'] = pkg_resources.get_distribution('openflexure_microscope').version current_config["server_version"] = pkg_resources.get_distribution(
"openflexure_microscope"
).version
# Save config to file # Save config to file
self.settings_file.save(current_config, backup=True) self.settings_file.save(current_config, backup=True)

View file

@ -132,7 +132,7 @@ class PluginMount(object):
def __init__(self, parent): def __init__(self, parent):
self.parent = parent self.parent = parent
self.plugins = [] # List of plugin objects self.plugins = [] # List of plugin objects
self.schemas = [] # List of plugin schemas self.forms = [] # List of plugin forms
logging.info("Creating plugin mount") logging.info("Creating plugin mount")
@property @property
@ -182,7 +182,7 @@ class PluginMount(object):
logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC) logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
else: else:
logging.error("Error loading plugin. Moving on.") logging.error(f"Error loading plugin {plugin_map}. Moving on.")
class MicroscopePlugin: class MicroscopePlugin:
""" """

View file

@ -9,7 +9,7 @@
"selfUpdate": true, "selfUpdate": true,
"route": "/do", "route": "/do",
"submitLabel": "Do things", "submitLabel": "Do things",
"schema": [ "form": [
[ [
{ {
"fieldType": "numberInput", "fieldType": "numberInput",
@ -66,7 +66,7 @@
"selfUpdate": true, "selfUpdate": true,
"route": "/task", "route": "/task",
"submitLabel": "Start task", "submitLabel": "Start task",
"schema": [ "form": [
{ {
"fieldType": "numberInput", "fieldType": "numberInput",
"placeholder": "", "placeholder": "",

View file

@ -7,16 +7,16 @@ from openflexure_microscope.devel import MicroscopePlugin
from .api import DoAPI, TaskAPI from .api import DoAPI, TaskAPI
HERE = os.path.dirname(os.path.realpath(__file__)) HERE = os.path.dirname(os.path.realpath(__file__))
SCHEMA_PATH = os.path.join(HERE, "schema.json") FORM_PATH = os.path.join(HERE, "forms.json")
class ExamplePlugin(MicroscopePlugin): class ExamplePlugin(MicroscopePlugin):
""" """
An example plugin using a comprehensive schema An example plugin using a comprehensive form
""" """
global SCHEMA_PATH global FORM_PATH
with open(SCHEMA_PATH, 'r') as sc: with open(FORM_PATH, 'r') as sc:
api_schema = json.load(sc) api_form = json.load(sc)
api_views = { api_views = {
'/do': DoAPI, '/do': DoAPI,
@ -35,7 +35,7 @@ class ExamplePlugin(MicroscopePlugin):
def set_values(self, val_int, val_str, val_radio, val_check, val_select, val_disposable): def set_values(self, val_int, val_str, val_radio, val_check, val_select, val_disposable):
""" """
Demonstrate a plugin with schema Demonstrate a plugin with form
""" """
if val_int: if val_int:
self.val_int = int(val_int) self.val_int = int(val_int)