Added new documentation sections

This commit is contained in:
Joel Collins 2020-01-21 17:05:31 +00:00
parent 03d6a70237
commit aeff3d14cf
8 changed files with 280 additions and 21 deletions

View file

@ -0,0 +1,21 @@
Introduction
============
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 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.
Extensions can either be loaded from a single Python file, or as a Python package installed to the environment being used.
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 extension. This Python file must contain all of your extension objects, and be located in the applications extensions directory (by default ``~/.openflexure/microscope_extensions``).
Package extensions
---------------
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 extension package must be importable using an absolute import from within the Python environment being used to load your microscope.
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.

View file

@ -0,0 +1,67 @@
Marshaling data
===============
Introduction
------------
- Define marshaling
- Why marshal objects?
- Marshmallow library
- Marshmallow schema and fields
- Schema class
- Schema dictionaries
- Using schema in ``@marshal_with``
- Using schema in ``@use_args``
In the previous section we saw how to use fields to document the expected request body for simple requests, in which a single argument is required. By making use of Marshmallow schemas, we can allow more complex requests containing many parameters of different types. The parsed request parameters are then passed to the view function as a positional argument (as before), in the form of a dictionary.
For example, your ``@use_args`` decorator may look like:
.. code-block:: python
@use_args({
"name": fields.String(required=True),
"age": fields.Integer(required=True),
"job": fields.String(required=False, missing="Unknown")
})
A compatible request body, in JSON format, may look like:
.. code-block:: json
{
"name": "John Doe",
"age": 45,
"job": "Python developer"
}
This JSON data is the parsed, converted into a Python dictionary, and passed as an argument. Retreiving the data from within your view function may therefore look like:
.. code-block:: python
@use_args({
"name": fields.String(required=True),
"age": fields.Integer(required=True),
"job": fields.String(required=False, missing="Unknown")
})
def post(self, args):
name = args.get("name") # Returns "John Doe", type str
age = args.get("age") # Returns 45, type int
job = args.get("job") # Returns "Python developer", type str
- Example (from ``03_marshaling_data.py``)
Scraps
++++++
An example request body that would be valid for this view is:
.. code-block:: json
{
"name": "My microscope name"
}

View file

@ -0,0 +1,63 @@
Basic extension structure
=========================
An extension starts as a simple instance of :py:class:`labthings.server.extensions.BaseExtension`.
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
In order to access the currently running microscope object, use the :py:func:`labthings.server.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 labthings.server.extensions import BaseExtension
from labthings.server.find import find_component
def identify():
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# 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(rename, "rename")
Once this extension is loaded, any other extensions will have access to your methods:
.. code-block:: python
from labthings.server.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()

View file

@ -0,0 +1,109 @@
Adding web API views
====================
Introduction
------------
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.
As with most HTTP APIs, we make use of basic HTTP request methods. GET requests return data without modifying any state. POST requests completely replace data with data passed as request arguments. PUT requests update data with new data passed as request arguments. DELETE requests delete a particular object from the server. Your API views need not implement all of these methods.
Continuing our example on the previous page, and discussed below, adding API views may look like:
.. code-block:: python
from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component
from labthings.server.view import View
from labthings.server.decorators import use_body
from labthings.server import fields
## Extension methods
def identify(microscope):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
class ExampleIdentifyView(View):
def get(self):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our identify function's output
return identify(microscope)
class ExampleRenameView(View):
# Expect a request parameter called "name", which is a string. Pass to argument "args".
@use_body(fields.String(required=True, example="My Example Microscope"))
def post(self, body):
# Look for our new name in the request body
new_name = body
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
# Return our identify function's output
return identify(microscope)
## Create extension
# 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(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions.
In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests.
Request arguments
+++++++++++++++++
For POST and PUT requests, data usually needs to be provided to the view in order to perform its function. In this example, our ``rename`` view requires a new microscope name to be passed. We make use of the ``@use_body`` decorator to provide this functionality.
``@use_body`` defines the type of data expected in the request body. In this example, we ``String`` type data. The arguments of ``fields.String`` allow us to provide additional information, such as the parameter being required, and example values to appear in API documentation.
Adding additional fields, and the meaning of the field types, will be discussed further in the next section.
When a POST request is made to our API view, the request body is converted to processed by `@use_body``, and passed as a positional argument to our ``post`` function.
Swagger documentation
+++++++++++++++++++++
At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/swagger-ui`` (or replace ``microscope.local`` with your microscopes IP address on incompatible systems).
This page uses `SwaggerUI <https://swagger.io/tools/swagger-ui/>`_ to provide visual, interactive API documentation. Find your extensions URL in the documentation under the ``extensions`` group. Basic documentation about the parameters required for your POST method should be visible, as well as an interactive example filled out with the example request given in ``@use_body``.

View file

@ -4,10 +4,7 @@ Developing API Extensions
.. toctree::
:maxdepth: 2
./plugins/introduction.rst
./plugins/structure.rst
./plugins/routes.rst
./plugins/hardware.rst
./plugins/forms.rst
./plugins/example.rst
./plugins/class.rst
./extensions/introduction.rst
./extensions/structure.rst
./extensions/views.rst
./extensions/marshaling.rst

View file

@ -23,10 +23,12 @@ A simple extension file, with no API views but application-available methods may
parent_camera = microscope.camera
parent_stage = microscope.stage
response = "My parent camera is {}, and my parent stage is {}.".format(parent_camera,
parent_stage)
response = "My parent camera is {}, and my parent stage is {}.".format(
parent_camera, parent_stage
)
return response
def rename(new_name):
"""
Rename the microscope
@ -36,14 +38,14 @@ A simple extension file, with no API views but application-available methods may
microscope.name = new_name
microscope.save_settings()
# 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, "rename")
my_extension.add_method(rename, "rename")
Once this extension is loaded, any other extensions will have access to your methods: