Started updating documentation
This commit is contained in:
parent
be74b92bd7
commit
802f5ba0c2
17 changed files with 248 additions and 368 deletions
|
|
@ -1,42 +1,21 @@
|
|||
Introduction
|
||||
============
|
||||
|
||||
Plugins allow functionality to be added to the OpenFlexure Microscope without having to modify the base code.
|
||||
Extensions allow functionality to be added to the OpenFlexure Microscope web API without having to modify the base code.
|
||||
They have full access to the :py:class:`openflexure_microscope.Microscope` object,
|
||||
including direct access to the attached :py:class:`openflexure_microscope.camera.pi.StreamingCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects.
|
||||
This also allows direct access to the :py:class:`picamera.PiCamera` object.
|
||||
including direct access to any attached :py:class:`openflexure_microscope.camera.base.BaseCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects.
|
||||
This also allows access to the :py:class:`picamera.PiCamera` object.
|
||||
|
||||
All microscope plugins must subclass :py:class:`openflexure_microscope.plugins.MicroscopePlugin` in order to be loaded by the microscopes :py:class:`openflexure_microscope.plugins.PluginMount`.
|
||||
Extensions can either be loaded from a single Python file, or as a Python package installed to the environment being used.
|
||||
|
||||
Once attached, all methods defined within the plugin class will be accessible from ``<microscope_object>.plugin.<plugin_namespace>.<plugin_method(args)>``.
|
||||
Here, ``<microscope_object>`` is an instance of :py:class:`openflexure_microscope.Microscope`. The ``<plugin_method>`` is the method defined within your plugin class. Finally, ``<plugin_namespace>`` is determined differently depending on the type of plugin.
|
||||
|
||||
Plugins can either be loaded as a single Python file located anywhere on disk, or as a Python package installed to the environment being used. If loaded from a single file, the namespace is set to the file name (excluding .py extension) of the plugin file. If loaded from a package, the namespace is set to the name of the top-level module in the package. For example, if your plugin class definition resides within ``my_openflexure_plugins.microscope.mypluginpackage``, the namespace will be set to ``mypluginpackage``. Where possible, try to use descriptive, unique package names for this reason. For example, rather than name your plugin package ``autofocus``, which would like cause namespace clashes, instead name it ``yourname_autofocus``, or similar.
|
||||
|
||||
Module (single-file) plugins
|
||||
Single-file extensions
|
||||
----------------------------
|
||||
For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a plugin.
|
||||
For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a extension. This Python file must contain all of your extension objects, and be located in the applications extensions directory (by default ``~/.openflexure/microscope_extensions``).
|
||||
|
||||
This Python file must contain all of your plugin classes. Relative imports will not work. External modules and packages can be used with absolute imports, however for more complex plugins, it is often worth instead making use of an installable package plugin.
|
||||
|
||||
Package plugins
|
||||
Package extensions
|
||||
---------------
|
||||
Generally, for adding anything other than very simple functionality, plugins should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope plugin.
|
||||
Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension.
|
||||
|
||||
The main restriction is that the plugin package must be importable using an absolute import from within the Python environment being used to load your microscope.
|
||||
The main restriction is that the extension package must be importable using an absolute import from within the Python environment being used to load your microscope.
|
||||
|
||||
Loading plugins with microscope_settings.json
|
||||
---------------------------------------------
|
||||
Both types of plugin are loaded by specifying the plugin class in your :ref:`MicroscopeRC`. In the case of a single-file plugin, specify the path to the plugin file, followed by the name of your :py:class:`openflexure_microscope.plugins.MicroscopePlugin` child class, separated by a colon. For packaged plugins, specify the absolute module name in place of the path.
|
||||
|
||||
For example, the plugins section of your microscope_settings.json file may look like:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
...
|
||||
"plugins": [
|
||||
"openflexure_microscope.plugins.default:Plugin",
|
||||
"~/my_plugins/my_plugin_file.py:MyPluginClass",
|
||||
"my_openflexure_plugins.microscope.mypluginpackage:MyPluginClass"
|
||||
]
|
||||
...
|
||||
In order to enable your packaged extension, create a file in the applications extensions directory (by default ``~/.openflexure/microscope_extensions``) which imports your extension object(s) from your module.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
Adding web API routes
|
||||
=====================
|
||||
Adding web API views
|
||||
====================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
|
@ -7,37 +7,38 @@ Adding web API routes
|
|||
|
||||
Introduction
|
||||
------------
|
||||
Plugins can automatically create routes to expose plugin functionality via the web API. Creating API routes for your plugin is strongly recommended, as this is the primary way we encourage interaction with the microscope device.
|
||||
Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device.
|
||||
|
||||
To create API routes, add a dictionary to your plugin class, named ``api_views``. Within this dictionary, each key should be a string defining the route URL, whose value is a class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`.
|
||||
To create API views, add a dictionary to your extension class, named ``api_views``. Within this dictionary, each key should be a string defining the route URL, whose value is a class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewExtension`.
|
||||
|
||||
For example, your ``api_views`` dictionary may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyPluginClass(MicroscopePlugin):
|
||||
class MyExtensionClass(MicroscopeExtension):
|
||||
|
||||
api_views = {
|
||||
'/myplugin': MyRouteAPI,
|
||||
'/myextension': MyRouteAPI,
|
||||
}
|
||||
|
||||
Here, ``MyRouteAPI`` is a web API plugin class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`. If your plugin package were named ``myplugins.package``, an API route would be automatically added at ``/api/v1/plugin/myplugins/package/myplugin``.
|
||||
Here, ``MyRouteAPI`` is a web API extension class, subclassing :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewExtension`. If your extension package were named ``myextensions.package``, an API route would be automatically added at ``/api/v1/extension/myextensions/package/myextension``.
|
||||
|
||||
The full URL that your plugin will attach to is essentially identical to it's full module path. That is, if your plugin is loaded from ``my_microscope_plugins.mypluginpackage:MyPluginClass``, then your plugin routes will appear at ``<microscope_url>/plugin/my_microscope_plugins/mypluginpackage/<route>``. While this means that plugin routes can get long very quickly, they will generally only ever be accessed by client applications, and so this generally should not be a problem.
|
||||
The full URL that your extension will attach to is essentially identical to it's full module path. That is, if your extension is loaded from ``my_microscope_extensions.myextensionpackage:MyExtensionClass``, then your extension views will appear at ``<microscope_url>/extension/my_microscope_extensions/myextensionpackage/<route>``. While this means that extension views can get long very quickly, they will generally only ever be accessed by client applications, and so this generally should not be a problem.
|
||||
|
||||
The MicroscopeViewPlugin class
|
||||
|
||||
The MicroscopeViewExtension class
|
||||
------------------------------
|
||||
|
||||
All API plugin classes must subclass :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin`, which is itself a subclass of `Flask's MethodView <http://flask.pocoo.org/docs/1.0/api/#flask.views.MethodView>`_. This greatly simplifies defining different functionality associated with different HTTP methods at a single URL route.
|
||||
It is best practice to clearly separate out types of functionality by HTTP method. For example, a GET request should never change the state of the microscope. For this, POST or PUT requests are acceptable. Parameters should be passed to POST and PUT requests as JSON payloads, and your methods should include fallback code for cases where parameters are not passed, or are passed in an invalid format. The DELETE method should only be used in situations where your plugin creates additional URL routes for newly created objects, and should serve only to delete these objects and routes.
|
||||
All API extension classes must subclass :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewExtension`, which is itself a subclass of `Flask's MethodView <http://flask.pocoo.org/docs/1.0/api/#flask.views.MethodView>`_. This greatly simplifies defining different functionality associated with different HTTP methods at a single URL route.
|
||||
It is best practice to clearly separate out types of functionality by HTTP method. For example, a GET request should never change the state of the microscope. For this, POST or PUT requests are acceptable. Parameters should be passed to POST and PUT requests as JSON payloads, and your methods should include fallback code for cases where parameters are not passed, or are passed in an invalid format. The DELETE method should only be used in situations where your extension creates additional URL views for newly created objects, and should serve only to delete these objects and views.
|
||||
|
||||
Each HTTP method maps to a function with the same name, in lowercase. For example, your :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewPlugin` may look like:
|
||||
Each HTTP method maps to a function with the same name, in lowercase. For example, your :py:class:`openflexure_microscope.api.v1.views.MicroscopeViewExtension` may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import jsonify
|
||||
...
|
||||
class MyRouteAPI(MicroscopeViewPlugin):
|
||||
class MyRouteAPI(MicroscopeViewExtension):
|
||||
|
||||
def get(self):
|
||||
# Retrieve some information, without changing the state of the microscope
|
||||
|
|
@ -47,39 +48,39 @@ Each HTTP method maps to a function with the same name, in lowercase. For exampl
|
|||
# Change the state of the microscope based on passed parameters
|
||||
...
|
||||
|
||||
Sometimes you will need to create variable API routes. For example, the built-in routes for managing capture data use capture IDs in the request URL to specify which capture data should be returned. Plugins can also access this functionality. This is done using `Flask variable rules <http://flask.pocoo.org/docs/1.0/quickstart/#variable-rules>`_. Here, variables are added to the URL route string by marking them with ``<variable_name>``, which then passes ``variable_name`` to your request function as a keyword argument.
|
||||
Sometimes you will need to create variable API views. For example, the built-in views for managing capture data use capture IDs in the request URL to specify which capture data should be returned. Extensions can also access this functionality. This is done using `Flask variable rules <http://flask.pocoo.org/docs/1.0/quickstart/#variable-rules>`_. Here, variables are added to the URL route string by marking them with ``<variable_name>``, which then passes ``variable_name`` to your request function as a keyword argument.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyPluginClass(MicroscopePlugin):
|
||||
class MyExtensionClass(MicroscopeExtension):
|
||||
|
||||
api_views = {
|
||||
'/myplugin/<object_id>': MyRouteAPI,
|
||||
'/myextension/<object_id>': MyRouteAPI,
|
||||
}
|
||||
...
|
||||
|
||||
class MyRouteAPI(MicroscopeViewPlugin):
|
||||
class MyRouteAPI(MicroscopeViewExtension):
|
||||
|
||||
def get(self, object_id):
|
||||
# Retrieve some information about object_id
|
||||
this_object = object_dictionary[object_id]
|
||||
...
|
||||
|
||||
Calling plugin methods from routes
|
||||
Calling extension methods from views
|
||||
++++++++++++++++++++++++++++++++++
|
||||
|
||||
Instances of MicroscopeViewPlugin have direct access to their associated microscope plugin methods, without needing to know the plugin namespace in advance. As described earlier in this section, all plugins get attached to the microscope in their own namespace, based on the plugins name. This means there are two equivalent ways to access your plugin methods from a web API plugin:
|
||||
Instances of MicroscopeViewExtension have direct access to their associated microscope extension methods, without needing to know the extension namespace in advance. As described earlier in this section, all extensions get attached to the microscope in their own namespace, based on the extensions name. This means there are two equivalent ways to access your extension methods from a web API extension:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
# Call a method from our plugin, using the MicroscopeViewPlugin.plugin shortcut
|
||||
self.plugin.my_plugin_method()
|
||||
# Call a method from our extension, using the MicroscopeViewExtension.extension shortcut
|
||||
self.extension.my_extension_method()
|
||||
|
||||
# Call a method from our plugin, using the full route
|
||||
self.microscope.my_plugin_name.my_plugin_method()
|
||||
# Call a method from our extension, using the full route
|
||||
self.microscope.my_extension_name.my_extension_method()
|
||||
...
|
||||
|
||||
Building responses
|
||||
|
|
@ -98,14 +99,14 @@ An example web route with simple responses may look like:
|
|||
from flask import jsonify
|
||||
...
|
||||
|
||||
class MyPluginClass(MicroscopePlugin):
|
||||
class MyExtensionClass(MicroscopeExtension):
|
||||
|
||||
api_views = {
|
||||
'/myplugin/<object_id>': MyRouteAPI,
|
||||
'/myextension/<object_id>': MyRouteAPI,
|
||||
}
|
||||
...
|
||||
|
||||
class MyRouteAPI(MicroscopeViewPlugin):
|
||||
class MyRouteAPI(MicroscopeViewExtension):
|
||||
|
||||
def get(self, object_id):
|
||||
|
||||
|
|
@ -135,7 +136,7 @@ To ease obtaining values from a JSON payload attached to an HTTP POST request, y
|
|||
.. code-block:: python
|
||||
|
||||
...
|
||||
class MyRouteAPI(MicroscopeViewPlugin):
|
||||
class MyRouteAPI(MicroscopeViewExtension):
|
||||
|
||||
def post(self):
|
||||
# Get payload JSON from request
|
||||
|
|
@ -144,7 +145,7 @@ To ease obtaining values from a JSON payload attached to an HTTP POST request, y
|
|||
# Try to find value associated with 'my_string' key.
|
||||
# If that key doesn't exist in the payload, return '' instead.
|
||||
# If a value does exist, convert it to a string, regardless of its original type.
|
||||
new_plugin_string = payload.param('my_string', default='', convert=str)
|
||||
new_extension_string = payload.param('my_string', default='', convert=str)
|
||||
...
|
||||
|
||||
.. autoclass:: openflexure_microscope.api.utilities.JsonResponse
|
||||
|
|
|
|||
|
|
@ -1,33 +1,58 @@
|
|||
Basic plugin structure
|
||||
===========================
|
||||
Basic extension structure
|
||||
=========================
|
||||
|
||||
As described earlier, a plugin must subclass :py:class:`openflexure_microscope.plugins.MicroscopePlugin`. Each plugin in described by a single class, containing any number of methods. The methods defined within your plugin class will be attached to the microscope. By subclassing :py:class:`openflexure_microscope.plugins.MicroscopePlugin`, your methods automatically get access to the microscope object your plugin is attached to, through ``self.microscope``. This is an instance of :py:class:`openflexure_microscope.Microscope`, and gives unrestricted access to the attached camera and stage hardware.
|
||||
An extension starts as a simple instance of :py:class:`openflexure_microscope.common.flask_labthings.extensions.BaseExtension`.
|
||||
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
|
||||
|
||||
For example, a simple plugin file named ``myplugin.py``, may look like:
|
||||
In order to access the currently running microscope object, use the :py:func:`openflexure_microscope.common.flask_labthings.find` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
|
||||
|
||||
A simple extension file, with no API views but application-available methods may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openflexure_microscope.plugins import MicroscopePlugin
|
||||
from openflexure_microscope.common.flask_labthings.extensions import BaseExtension
|
||||
from openflexure_microscope.common.flask_labthings.find import find_component
|
||||
|
||||
class MyPlugin(MicroscopePlugin):
|
||||
|
||||
def identify(self):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
def identify():
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
parent_camera = self.microscope.camera
|
||||
parent_stage = self.microscope.stage
|
||||
parent_camera = microscope.camera
|
||||
parent_stage = microscope.stage
|
||||
|
||||
response = "My parent camera is {}, and my parent stage is {}.".format(parent_camera,
|
||||
parent_stage)
|
||||
return response
|
||||
response = "My parent camera is {}, and my parent stage is {}.".format(parent_camera,
|
||||
parent_stage)
|
||||
return response
|
||||
|
||||
def hello_world(self):
|
||||
"""
|
||||
Demonstrate passive method
|
||||
"""
|
||||
def hello_world():
|
||||
"""
|
||||
Demonstrate passive method
|
||||
"""
|
||||
|
||||
return "Hello world!"
|
||||
return "Hello world!"
|
||||
|
||||
|
||||
When this plugin is loaded and attached to a microscope object named ``microscope``, the plugin methods will be available at ``microscope.plugin.myplugin.identify()`` and ``microscope.plugin.myplugin.hello_world()``.
|
||||
# Create your extension object
|
||||
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
|
||||
|
||||
# Add methods to your extension
|
||||
my_extension.add_method(identify, "identify")
|
||||
my_extension.add_method(hello_world, "hello_world")
|
||||
|
||||
|
||||
Once this extension is loaded, any other extensions will have access to your methods:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from openflexure_microscope.common.flask_labthings.find import find_extension
|
||||
|
||||
def test_extension_method():
|
||||
# Find your extension. Returns None if it hasn't been found.
|
||||
my_found_extension = find_extension("com.myname.myextension")
|
||||
|
||||
# Call a function from your extension
|
||||
if my_found_extension:
|
||||
my_found_extension.identify()
|
||||
Loading…
Add table
Add a link
Reference in a new issue