Updated documentation to LT070 style

This commit is contained in:
Joel Collins 2020-06-29 17:47:51 +01:00
parent 83b3ea1d1d
commit 9ae2ae06d0
12 changed files with 114 additions and 127 deletions

View file

@ -1,11 +1,11 @@
Capture Object Capture Object
======================================================= =======================================================
By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.camera.capture.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory. By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.captures.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory.
The class also includes some convenience features such as handling metadata tags and file names, and generating image thumbnails. Additionally, the class handles storing capture metadata to Exif tags in supported formats. The class also includes some convenience features such as handling metadata tags and file names, and generating image thumbnails. Additionally, the class handles storing capture metadata to Exif tags in supported formats.
Below are details of available methods and attributes. Below are details of available methods and attributes.
.. automodule:: openflexure_microscope.camera.capture .. automodule:: openflexure_microscope.captures.capture
:members: :members:

View file

@ -17,11 +17,14 @@ Like properties, we use a special view class to identify a view as an action: ``
""" """
Take an image capture and return it without saving Take an image capture and return it without saving
""" """
# Expect a "use_video_port" boolean, which defaults to True if none is given # Expect a "use_video_port" boolean, which defaults to True if none is given
@use_args({"use_video_port": fields.Boolean(missing=True)}) args = {"use_video_port": fields.Boolean(missing=True)}
# Our success response (200) returns an image (image/jpeg mimetype) # Our success response (200) returns an image (image/jpeg mimetype)
@doc_response(200, mimetype="image/jpeg") responses = {
200: {"content_type": "image/jpeg"}
}
def post(self, args): def post(self, args):
""" """
Take a non-persistant image capture. Take a non-persistant image capture.
@ -41,7 +44,7 @@ Like properties, we use a special view class to identify a view as an action: ``
# Return our image data using Flasks send_file function # Return our image data using Flasks send_file function
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg") return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
In this example, we are also making use of the ``@doc_response`` decorator, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``@use_args`` to accept optional parameters with POST requests. In this example, we are also making use of the ``responses`` attribute, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``args`` to accept optional parameters with POST requests.
Complete example Complete example
---------------- ----------------

View file

@ -2,7 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View from labthings.server.view import View
from labthings.server.decorators import use_body
from labthings.server import fields from labthings.server import fields
## Extension methods ## Extension methods
@ -44,11 +43,13 @@ class ExampleIdentifyView(View):
class ExampleRenameView(View): class ExampleRenameView(View):
# Expect a request parameter called "name", which is a string. Pass to argument "args". # Expect a request parameter called "name", which is a string.
@use_body(fields.String(required=True, example="My Example Microscope")) # Passed to the argument "args".
def post(self, body): args = fields.String(required=True, example="My Example Microscope")
def post(self, args):
# Look for our new name in the request body # Look for our new name in the request body
new_name = body new_name = args
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")

View file

@ -2,7 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View from labthings.server.view import View
from labthings.server.decorators import use_args, marshal_with
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
@ -30,10 +29,10 @@ def rename(microscope, new_name):
## Extension views ## Extension views
class ExampleIdentifyView(View): class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
def get(self): def get(self):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
@ -45,9 +44,10 @@ class ExampleIdentifyView(View):
class ExampleRenameView(View): class ExampleRenameView(View):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
# Expect a request parameter called "name", which is a string. Pass to argument "args". # 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")}) args = {"name": fields.String(required=True, example="My Example Microscope")}
def post(self, args): def post(self, args):
# Look for our "name" parameter in the request arguments # Look for our "name" parameter in the request arguments
new_name = args.get("name") new_name = args.get("name")

View file

@ -2,12 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View, PropertyView from labthings.server.view import View, PropertyView
from labthings.server.decorators import (
use_args,
marshal_with,
ThingProperty,
PropertySchema,
)
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
@ -38,7 +32,8 @@ def rename(microscope, new_name):
# Since we only have a GET method here, it'll register as a read-only property # Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView): class ExampleIdentifyView(PropertyView):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
def get(self): def get(self):
""" """
Show identifying information about the current microscope object Show identifying information about the current microscope object
@ -47,14 +42,15 @@ class ExampleIdentifyView(PropertyView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope
# We can use a single schema for all methods if the input and output will be formatted identically # We can use a single schema as the input and output will be formatted identically
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute # Eg. We always expect a "name" string argument, and always return a "name" string attribute
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
class ExampleRenameView(PropertyView): class ExampleRenameView(PropertyView):
schema = {"name": fields.String(required=True, example="My Example Microscope")}
def get(self): def get(self):
""" """
Show the current microscope name Show the current microscope name
@ -78,7 +74,7 @@ class ExampleRenameView(PropertyView):
rename(microscope, new_name) rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope

View file

@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View, ActionView, PropertyView from labthings.server.view import View, ActionView, PropertyView
from labthings.server.decorators import (
use_args,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
)
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
@ -43,7 +35,8 @@ def rename(microscope, new_name):
# Since we only have a GET method here, it'll register as a read-only property # Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView): class ExampleIdentifyView(PropertyView):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
def get(self): def get(self):
""" """
Show identifying information about the current microscope object Show identifying information about the current microscope object
@ -52,14 +45,16 @@ class ExampleIdentifyView(PropertyView):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope
# We can use a single schema for all methods if the input and output will be formatted identically
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute # We can use a single schema as the input and output will be formatted identically
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")}) # Eg. We always expect a "name" string argument, and always return a "name" string attribute
class ExampleRenameView(PropertyView): class ExampleRenameView(PropertyView):
schema = {"name": fields.String(required=True, example="My Example Microscope")}
def get(self): def get(self):
""" """
Show the current microscope name Show the current microscope name
@ -83,7 +78,7 @@ class ExampleRenameView(PropertyView):
rename(microscope, new_name) rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope
@ -91,11 +86,14 @@ class QuickCaptureAPI(ActionView):
""" """
Take an image capture and return it without saving Take an image capture and return it without saving
""" """
# Expect a "use_video_port" boolean, which defaults to True if none is given # Expect a "use_video_port" boolean, which defaults to True if none is given
@use_args({"use_video_port": fields.Boolean(missing=True)}) args = {"use_video_port": fields.Boolean(missing=True)}
# Our success response (200) returns an image (image/jpeg mimetype) # Our success response (200) returns an image (image/jpeg mimetype)
@doc_response(200, mimetype="image/jpeg") responses = {
200: {"content_type": "image/jpeg"}
}
def post(self, args): def post(self, args):
""" """
Take a non-persistant image capture. Take a non-persistant image capture.

View file

@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View, ActionView from labthings.server.view import View, ActionView
from labthings.server.decorators import (
use_args,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
)
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
@ -18,10 +10,10 @@ import io # Used in our capture action
import time # Used in our timelapse function import time # Used in our timelapse function
# Used in our timelapse function # Used in our timelapse function
from openflexure_microscope.camera.base import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename
# Used to run our timelapse in a background thread # Used to run our timelapse in a background thread
from labthings.core.tasks import taskify, update_task_progress from labthings.core.tasks import update_task_progress
## Extension methods ## Extension methods
@ -70,16 +62,15 @@ class TimelapseAPI(ActionView):
""" """
Take a series of images in a timelapse Take a series of images in a timelapse
""" """
@use_args( args = {
{ "n_images": fields.Integer(
"n_images": fields.Integer( required=True, example=5, description="Number of images"
required=True, example=5, description="Number of images" ),
), "t_between": fields.Number(
"t_between": fields.Number( missing=1, example=1, description="Time (seconds) between images"
missing=1, example=1, description="Time (seconds) between images" ),
), }
}
)
def post(self, args): def post(self, args):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")

View file

@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.view import View, ActionView from labthings.server.view import View, ActionView
from labthings.server.decorators import (
use_args,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
)
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
@ -18,10 +10,10 @@ import io # Used in our capture action
import time # Used in our timelapse function import time # Used in our timelapse function
# Used in our timelapse function # Used in our timelapse function
from openflexure_microscope.camera.base import generate_basename from openflexure_microscope.captures.capture_manager import generate_basename
# Used to run our timelapse in a background thread # Used to run our timelapse in a background thread
from labthings.core.tasks import taskify, update_task_progress from labthings.core.tasks import update_task_progress
# Used to convert our GUI dictionary into a complete eV extension GUI # Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui from openflexure_microscope.api.utilities.gui import build_gui
@ -72,16 +64,15 @@ class TimelapseAPI(ActionView):
""" """
Take a series of images in a timelapse, running as a background task Take a series of images in a timelapse, running as a background task
""" """
@use_args( args = {
{ "n_images": fields.Integer(
"n_images": fields.Integer( required=True, example=5, description="Number of images"
required=True, example=5, description="Number of images" ),
), "t_between": fields.Number(
"t_between": fields.Number( missing=1, example=1, description="Time (seconds) between images"
missing=1, example=1, description="Time (seconds) between images" ),
), }
}
)
def post(self, args): def post(self, args):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")

View file

@ -14,7 +14,7 @@ The OpenFlexure Microscope Server makes use of the `Marshmallow library <https:/
- **Deserialize** input data to app-level objects. - **Deserialize** input data to app-level objects.
- **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API. - **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API.
When developing extensions, you are encouraged to make use of the ``@marshal_with`` and ``@use_args`` decorators, along with schemas, to handle serialisation of your API responses, and parsing of request parameters respectively. When developing extensions, you are encouraged to make use of your View ``schema`` and ``args`` class attributes to handle serialisation of your API responses, and parsing of request parameters respectively.
Schemas and fields Schemas and fields
++++++++++++++++++ ++++++++++++++++++
@ -31,7 +31,7 @@ A **schema** is a collection of keys and fields describing how an object should
Argument parsing Argument parsing
++++++++++++++++ ++++++++++++++++
In the previous section we saw how to use fields and ``@use_body`` to get simple arguments from requests, in which a single parameter is required. By making use of Marshmallow schemas, and the `Webargs library <https://github.com/marshmallow-code/webargs>`_, we can allow for 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. In the previous section we saw how to use fields and ``args`` to get simple arguments from requests, in which a single parameter is required. By making use of Marshmallow schemas, and the `Webargs library <https://github.com/marshmallow-code/webargs>`_, we can allow for 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, if you are creating an API route, in which you expect parameters ``name``, ``age``, and optionally, ``job``, your schema class may look like: For example, if you are creating an API route, in which you expect parameters ``name``, ``age``, and optionally, ``job``, your schema class may look like:
@ -49,21 +49,25 @@ To inform your POST method to expect these arguments, use the ``@use_args`` deco
.. code-block:: python .. code-block:: python
@use_args(UserSchema()) class MyView(View):
def post(self, args): args = UserSchema()
def post(self, args):
.. ..
Alternatively, if your schema is only used in a single location, it may be simpler to create a dictionary schema only where it is used, for example: Alternatively, if your schema is only used in a single location, it may be simpler to create a dictionary schema only where it is used, for example:
.. code-block:: python .. code-block:: python
@use_args({ class MyView(View):
"name": fields.String(required=True), args = {
"age": fields.Integer(required=True), "name": fields.String(required=True),
"job": fields.String(required=False, missing="Unknown") "age": fields.Integer(required=True),
}) "job": fields.String(required=False, missing="Unknown")
def post(self, args): }
...
def post(self, args):
...
A compatible request body, in JSON format, may look like: A compatible request body, in JSON format, may look like:
@ -80,15 +84,17 @@ This JSON data is the parsed, converted into a Python dictionary, and passed as
.. code-block:: python .. code-block:: python
@use_args({ class MyView(View):
"name": fields.String(required=True), args = {
"age": fields.Integer(required=True), "name": fields.String(required=True),
"job": fields.String(required=False, missing="Unknown") "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 def post(self, args):
job = args.get("job") # Returns "Python developer", type str 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
Object serialization Object serialization
@ -118,16 +124,17 @@ We use this new schema in our ``identify`` view like so:
class ExampleIdentifyView(View): class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
def get(self): def get(self):
# Find our microscope component # Find our microscope component
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope
Note that our ``get`` method now returns the :py:class:`openflexure_microscope.Microscope` object itself. No formatting is done by the function, it is entirely handled by ``@marshal_with(MicroscopeIdentifySchema())``. Additionally, since we defined our schema as a class, it can be re-used elsewhere. Note that our ``get`` method now returns the :py:class:`openflexure_microscope.Microscope` object itself. No formatting is done by the function, it is entirely handled by the view class, and its `schema` attribute. Additionally, since we defined our schema as a class, it can be re-used elsewhere.
For our ``rename`` view, we will use a simpler schema for our input arguments, defined by a dictionary (since we are only expecting a single parameter in, and it will likely not be re-used elsewhere). Our response, however, will use our ``MicroscopeIdentifySchema`` class. This means that the *response* of our ``identify`` and ``rename`` views will be identically formatted. For our ``rename`` view, we will use a simpler schema for our input arguments, defined by a dictionary (since we are only expecting a single parameter in, and it will likely not be re-used elsewhere). Our response, however, will use our ``MicroscopeIdentifySchema`` class. This means that the *response* of our ``identify`` and ``rename`` views will be identically formatted.
@ -137,9 +144,10 @@ Our ``rename`` view class may now look like:
class ExampleRenameView(View): class ExampleRenameView(View):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
# Expect a request parameter called "name", which is a string. Pass to argument "args". # 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")}) args = {"name": fields.String(required=True, example="My Example Microscope")}
def post(self, args): def post(self, args):
# Look for our "name" parameter in the request arguments # Look for our "name" parameter in the request arguments
new_name = args.get("name") new_name = args.get("name")
@ -151,7 +159,7 @@ Our ``rename`` view class may now look like:
rename(microscope, new_name) rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope

View file

@ -21,7 +21,8 @@ In order to register a view as a Thing property, we use the ``PropertyView`` cla
# Since we only have a GET method here, it'll register as a read-only property # Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView): class ExampleIdentifyView(PropertyView):
# Format our returned object using MicroscopeIdentifySchema # Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema()) schema = MicroscopeIdentifySchema()
def get(self): def get(self):
""" """
Show identifying information about the current microscope object Show identifying information about the current microscope object
@ -30,12 +31,9 @@ In order to register a view as a Thing property, we use the ``PropertyView`` cla
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schemah handle formatting the output
return microscope return microscope
This decorator serves only to add documentation: The view will be tagged with ``property`` in Swagger documentation, and will appear as a property in the microscopes Thing Description.
However, the additional ``@PropertySchema`` decorator allows for simplified argument parsing and object marshaling.
Property schema Property schema
--------------- ---------------
@ -71,16 +69,17 @@ This request would update the property, such that a GET request would *now* retu
"job": "Landscape gardener" "job": "Landscape gardener"
} }
The ``@PropertySchema`` decorator can be applied to your view *class*, and essentially acts as shorthand for applying ``@marshal_with`` to all methods, and ``@use_args`` to all POST and PUT methods (or ``@use_body`` if only a single field is given). In Property Views the ``schema`` class attribute acts as the schema for both marshalling responses *and* parsing arguments. This is because property requests and responses should be identically formatted.
We will implement the ``@PropertySchema`` decorator in our ``ExampleRenameView`` view from our previous example: We will implement the ``schema`` attribute in our ``ExampleRenameView`` view from our previous example:
.. code-block:: python .. code-block:: python
# We can use a single schema for all methods if the input and output will be formatted identically # We can use a single schema as the input and output will be formatted identically
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute # Eg. We always expect a "name" string argument, and always return a "name" string attribute
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
class ExampleRenameView(PropertyView): class ExampleRenameView(PropertyView):
schema = {"name": fields.String(required=True, example="My Example Microscope")}
def get(self): def get(self):
""" """
Show the current microscope name Show the current microscope name
@ -104,7 +103,7 @@ We will implement the ``@PropertySchema`` decorator in our ``ExampleRenameView``
rename(microscope, new_name) rename(microscope, new_name)
# Return our microscope object, # Return our microscope object,
# let @marshal_with handle formatting the output # let schema handle formatting the output
return microscope return microscope
Complete example Complete example

View file

@ -18,13 +18,13 @@ In this case, our extension will have two new API views at `/identify` and `/ren
Request arguments 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. 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 ``args`` class attribute to provide this functionality.
``@use_body`` defines the type of data expected in the request body. In this example, we use ``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. ``args`` defines the type of data expected in the request body. In this example, we use ``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. 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 ``@use_body`` converts the body of the request into a ``String``, and passes it as a positional argument to our ``post`` function. When a POST request is made to our API view, the server converts the body of the request into a ``String``, and passes it as a positional argument to our ``post`` function.
Swagger documentation Swagger documentation
+++++++++++++++++++++ +++++++++++++++++++++

View file

@ -7,5 +7,5 @@ The main microscope class handles microscope settings, passing these between the
:maxdepth: 2 :maxdepth: 2
:caption: Contents: :caption: Contents:
.. automodule:: openflexure_microscope .. automodule:: openflexure_microscope.microscope
:members: :members: