Added new documentation sections
This commit is contained in:
parent
03d6a70237
commit
aeff3d14cf
8 changed files with 280 additions and 21 deletions
21
docs/source/extensions/introduction.rst
Normal file
21
docs/source/extensions/introduction.rst
Normal 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.
|
||||
67
docs/source/extensions/marshaling.rst
Normal file
67
docs/source/extensions/marshaling.rst
Normal 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"
|
||||
}
|
||||
63
docs/source/extensions/structure.rst
Normal file
63
docs/source/extensions/structure.rst
Normal 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()
|
||||
109
docs/source/extensions/views.rst
Normal file
109
docs/source/extensions/views.rst
Normal 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``.
|
||||
|
|
@ -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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ def identify():
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
parent_camera = microscope.camera
|
||||
parent_stage = microscope.stage
|
||||
|
||||
response = "My parent camera is {}, and my parent stage is {}.".format(
|
||||
parent_camera, parent_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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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_args
|
||||
from labthings.server.decorators import use_body
|
||||
from labthings.server import fields
|
||||
|
||||
## Extension methods
|
||||
|
|
@ -45,10 +45,10 @@ class ExampleIdentifyView(View):
|
|||
|
||||
class ExampleRenameView(View):
|
||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||
@use_args({"name": fields.String(required=True, example="My Example Microscope")})
|
||||
def post(self, args):
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
@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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue