Commit cb0e0886 authored by Joel Collins's avatar Joel Collins
Browse files

Added JSON form docs

parent b7ffab68
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -64,7 +64,7 @@ extensions = [
    'sphinx.ext.ifconfig',
    'sphinxcontrib.httpdomain',
    'sphinxcontrib.autohttp.flask',
    'sphinxcontrib.autohttp.flaskqref',
    'sphinxcontrib.autohttp.flaskqref'
]

# Override ordering
+10 −125
Original line number Diff line number Diff line
Example plugin
--------------

.. code-block:: python
plugin.py
+++++++++

    from openflexure_microscope.plugins import MicroscopePlugin
    from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
    from openflexure_microscope.api.utilities import JsonPayload
.. literalinclude:: example/plugin.py
  :language: python

    from flask import request, Response, escape
schema.json
+++++++++++

.. literalinclude:: example/schema.json
  :language: JSON

    ### MICROSCOPE PLUGIN ###

    class MyPluginClass(MicroscopePlugin):
        """
        A set of default plugins
        """

        api_views = {
            '/identify': IdentifyAPI,
            '/hello': HelloWorldAPI,
            '/timelapse': TimelapseAPI,
        }

        def identify(self):
            """
            Demonstrate access to Microscope.camera, and Microscope.stage
            """

            response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, 
                                                                                   self.microscope.stage)
            return response

        def hello_world(self):
            """
            Demonstrate passive method
            """

            return "Hello world!"

        def timelapse(self, n_images):
            """
            Demonstrate a long-running method that requires microscope hardware
            """
            print("Starting timelapse...")
            capture_array = []  # Empty list to store captures in

            # Acquire locks. Exception is raised if lock is in use by another thread.
            with self.microscope.camera.lock, self.microscope.stage.lock:
                for _ in range(n_images):

                    # Create a data stream to capture to
                    capture_data = self.microscope.camera.new_image(
                        write_to_file=True,
                        temporary=False)

                    # Capture a still image from the Pi camera, into the data stream
                    self.microscope.camera.capture(
                        capture_data,
                        use_video_port=True)
                    
                    # Append the capture data to our list
                    capture_array.append(capture_data)

                    # Wait for 1 minute
                    time.sleep(60)  


    ### API VIEWS ###

    class IdentifyAPI(MicroscopeViewPlugin):
        """
        A simple example API plugin, attached through the main microscope plugin.
        """
        def get(self):
            """
            Method to call when an HTTP GET request is made.
            """
            # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
            data = self.plugin.identify()  
            return Response(escape(data))


    class HelloWorldAPI(MicroscopeViewPlugin):
        """
        A method to create, set, and return a new microscope parameter.
        """

        def get(self):
            """
            Method to call when an HTTP GET request is made.
            """

            # If the microscope does not already contain our plugin_string attribute
            if not hasattr(self.microscope, 'plugin_string'):
                # Make a string, using the MicroscopeViewPlugin.plugin shortcut
                self.microscope.plugin_string = self.plugin.hello_world()

            return Response(self.microscope.plugin_string)

        def post(self):
            """
            Method to call when an HTTP POST request is made. 
            Assumes request will include a JSON payload.
            """
            # Get payload JSON
            payload = JsonPayload(request)

            # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
            new_plugin_string = payload.param('plugin_string', default='', convert=str)

            if new_plugin_string:  # If not None or empty
                # Set microscope attribute to the specified string
                self.microscope.plugin_string = new_plugin_string  

            return Response(self.microscope.plugin_string)

    class TimelapseAPI(MicroscopeViewPlugin):
        def post(self):

            # Get any JSON data in the body of the POST request
            payload = JsonPayload(request)

            # Extract the "n_images" parameter if it was passed. Otherwise, default to 10.
            n_images = payload.param('n_images', default=10, convert=int)

            # Attach the long-running method as a microscope task
            self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images)

            # Return the state of the task (will show ID, start time, and status before the task has finished)
            return jsonify(self.timelapse_task.state), 202
Notes
+++++

In this example, if the package or file were named ``my_plugin``, the three microscope plugin methods would be accessible from ``<microscope_object>.plugin.my_plugin.identify()``, ``<microscope_object>.plugin.my_plugin.timelapse()``, and ``<microscope_object>.plugin.my_plugin.hello_world()``.

+146 −0
Original line number Diff line number Diff line
from openflexure_microscope.plugins import MicroscopePlugin
from openflexure_microscope.api.v1.views import MicroscopeViewPlugin
from openflexure_microscope.api.utilities import JsonPayload

import os
import time
import json

from flask import request, Response, escape, jsonify

HERE = os.path.dirname(os.path.realpath(__file__))
SCHEMA_PATH = os.path.join(HERE, "schema.json")

### MICROSCOPE PLUGIN ###

class MyPluginClass(MicroscopePlugin):
    """
    A set of default plugins
    """

    global SCHEMA_PATH

    with open(SCHEMA_PATH, 'r') as sc:
        api_schema = json.load(sc)

    api_views = {
        '/identify': IdentifyAPI,
        '/hello': HelloWorldAPI,
        '/timelapse': TimelapseAPI,
    }

    def identify(self):
        """
        Demonstrate access to Microscope.camera, and Microscope.stage
        """

        response = "My parent camera is {}, and my parent stage is {}.".format(self.microscope.camera, 
                                                                                self.microscope.stage)
        return response

    def hello_world(self):
        """
        Demonstrate passive method
        """

        return "Hello world!"

    def timelapse(self, n_images):
        """
        Demonstrate a long-running method that requires microscope hardware
        """
        print("Starting timelapse...")
        capture_array = []  # Empty list to store captures in

        # Acquire locks. Exception is raised if lock is in use by another thread.
        with self.microscope.camera.lock, self.microscope.stage.lock:
            for _ in range(n_images):

                # Create a data stream to capture to
                capture_data = self.microscope.camera.new_image(
                    write_to_file=True,
                    temporary=False)

                # Capture a still image from the Pi camera, into the data stream
                self.microscope.camera.capture(
                    capture_data,
                    use_video_port=True)
                
                # Append the capture data to our list
                capture_array.append(capture_data)

                # Wait for 1 minute
                time.sleep(60)  


### API VIEWS ###

class IdentifyAPI(MicroscopeViewPlugin):
    """
    A simple example API plugin, attached through the main microscope plugin.
    """
    def get(self):
        """
        Method to call when an HTTP GET request is made.
        """
        # Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
        data = self.plugin.identify()  
        return Response(escape(data))


class HelloWorldAPI(MicroscopeViewPlugin):
    """
    A method to create, set, and return a new microscope parameter.
    """

    def get(self):
        """
        Method to call when an HTTP GET request is made.
        """

        # If the microscope does not already contain our plugin_string attribute
        if not hasattr(self.microscope, 'plugin_string'):
            # Make a string, using the MicroscopeViewPlugin.plugin shortcut
            self.microscope.plugin_string = self.plugin.hello_world()

        json_response = jsonify({
            'plugin_string': self.microscope.plugin_string
        })

        return Response(json_response)

    def post(self):
        """
        Method to call when an HTTP POST request is made. 
        Assumes request will include a JSON payload.
        """
        # Get payload JSON
        payload = JsonPayload(request)

        # Extract a value from the JSON key 'plugin_string', and convert to a string. If no value is given, default to empty.
        new_plugin_string = payload.param('plugin_string', default='', convert=str)

        if new_plugin_string:  # If not None or empty
            # Set microscope attribute to the specified string
            self.microscope.plugin_string = new_plugin_string  

        json_response = jsonify({
            'plugin_string': self.microscope.plugin_string
        })

        return Response(json_response)

class TimelapseAPI(MicroscopeViewPlugin):
    def post(self):

        # Get any JSON data in the body of the POST request
        payload = JsonPayload(request)

        # Extract the "n_images" parameter if it was passed. Otherwise, default to 10.
        n_images = payload.param('n_images', default=10, convert=int)

        # Attach the long-running method as a microscope task
        self.timelapse_task = self.microscope.task.start(self.plugin.timelapse, n_images)

        # Return the state of the task (will show ID, start time, and status before the task has finished)
        return jsonify(self.timelapse_task.state), 202
 No newline at end of file
+33 −0
Original line number Diff line number Diff line
{
    "id": "example-plugin",
    "icon": "pets",
    "forms": [
      {
        "name": "Hello world",
        "isTask": false,
        "selfUpdate": true,
        "route": "/hello",
        "schema": [
          {
            "fieldType": "textInput",
            "placeholder": "Enter a string",
            "label": "Plugin string",
            "name": "plugin_string"
          }
        ]
      },
      {
        "name": "Timelapse",
        "isTask": true,
        "route": "/timelapse",
        "submitLabel": "Start timelapse",
        "schema": [
            {
              "fieldType": "numberInput",
              "name": "n_images",
              "label": "Number of images"
            }
        ]
      }
    ] 
  }
 No newline at end of file
+92 −7
Original line number Diff line number Diff line
@@ -2,18 +2,97 @@ Adding a GUI
=====================

.. toctree::
   :maxdepth: 2
   :caption: Contents:
   :maxdepth: 1

Components
----------
   ./schema/json_schema.rst

.. list-table:: Summary of form components
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``.

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:

.. code-block:: python

    import json

    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

    class MyPluginClass(MicroscopePlugin):

        with open(SCHEMA_PATH, 'r') as sc:  # Open the JSON file
            api_schema = json.load(sc)  # Load the JSON file into the api_schema 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.


Forms from ``api_schema``
-------------------------

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*.

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``
---------------------------

Root level
++++++++++

The root of your ``api_schema`` expects 3 properties:

``id`` - A unique ID to give your client-side plugin

``icon`` - The name of a Material Design icon to use for your plugin

``forms`` - An array of forms as described below

Form level
++++++++++

Your plugin can contain multiple forms. For example, if your plugin creates several API routes, you will need a separate form for each route.

Each form is described by a JSON object, with the following properties:

``name`` - A human-readable name for the form 

``route`` - String of the corresponding API route. *Must* match a route defined in your ``api_views`` dictionary

``selfUpdate`` *(optional)* - Whether the client should automatically update form component values with a GET request to your API route

``isTask`` *(optional)* - Whether the client should treat your API route as a long-running task

``submitLabel`` *(optional)* - String to place inside of the form's submit button

``schema`` - An array of form components as described below

Component level
+++++++++++++++

Each form can (and probably should) contain multiple components. For example, if your API route expects several properties in POST requests, each property can be bound to a form component.

Upon form submission, the form data will be converted into a JSON object of key-value pairs, where the key is the components ``name``, and the value is it's current value.

On load, the form will make a GET request to it's API route, an expects a JSON object of key-value pairs in the same format (keys will be matched to component names).

An overview of available components, and their properties, can be found below.

Arranging components
^^^^^^^^^^^^^^^^^^^^

You can request that the client render several components in a horizontal grid by placing them in an array. You cannot nest arrays however. Each component in the array will be rendered with equal width as far as possible.

Overview of components
----------------------

.. list-table::
    :widths: 10 10 40 20
    :header-rows: 1
    :stub-columns: 1

    * - Name
    * - fieldType
      - Data type
      - Properties
      - Example
@@ -104,3 +183,9 @@ Components
        **placeholder** (str) Placeholder value

      - .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/textInput.png 

JSON form example
-----------------

.. literalinclude:: schema_example.json
  :language: JSON
Loading