Updated documentation to subclass-based extensions

This commit is contained in:
Joel Collins 2020-11-30 16:23:28 +00:00
parent 7a849c725a
commit bacc111a87
10 changed files with 249 additions and 318 deletions

View file

@ -2,35 +2,35 @@ from labthings import find_component
from labthings.extensions import BaseExtension
def identify():
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
def identify(self):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
return response
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(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
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")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -1,41 +0,0 @@
from labthings import find_component
from labthings.extensions import BaseExtension
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Create some instance variable
self.state_variable = "An example of a persistant instance variable"
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(self):
"""
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(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = MyExtension()

View file

@ -2,42 +2,44 @@ from labthings import fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def identify(microscope):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
def identify(self, 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}."
)
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
return response
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
def rename(self, 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)
return self.extension.identify(microscope)
class ExampleRenameView(View):
@ -53,21 +55,10 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our identify function's output
return identify(microscope)
return self.extension.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")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -2,7 +2,22 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -15,18 +30,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema
schema = MicroscopeIdentifySchema()
@ -54,21 +58,11 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
return 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(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -2,7 +2,22 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -15,16 +30,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
## Extension viewss
# Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView):
@ -68,21 +74,11 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
return 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(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -5,7 +5,22 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -18,15 +33,6 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
# Since we only have a GET method here, it'll register as a read-only property
@ -71,7 +77,7 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
@ -109,15 +115,4 @@ class QuickCaptureAPI(ActionView):
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
## 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(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
my_extension.add_view(QuickCaptureAPI, "/quick-capture")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -7,52 +7,59 @@ from labthings.views import ActionView
# Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Add our API views
self.add_view(TimelapseAPIView, "/timelapse")
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
def timelapse(self, microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Capture
microscope.camera.capture(output)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Capture
microscope.camera.capture(output)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Wait for the specified time
time.sleep(t_between)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
class TimelapseAPI(ActionView):
class TimelapseAPIView(ActionView):
"""
Take a series of images in a timelapse
"""
@ -71,16 +78,9 @@ class TimelapseAPI(ActionView):
microscope = find_component("org.openflexure.microscope")
# Start "timelapse"
return timelapse(microscope, args.get("n_images"), args.get("t_between"))
return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")
LABTHINGS_EXTENSIONS = (TimelapseExtension,)

View file

@ -4,60 +4,97 @@ from labthings import current_action, fields, find_component, update_action_prog
from labthings.extensions import BaseExtension
from labthings.views import ActionView
# Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui
# Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Add our API views
self.add_view(TimelapseAPIView, "/timelapse")
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Add our GUI description
gui_description = {
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
"forms": [ # List of forms. Each form is a collapsible accordion panel
{
"name": "Start a timelapse", # Form title
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
self.add_meta("gui", build_gui(gui_description, self))
# Capture
microscope.camera.capture(output)
def timelapse(self, microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Wait for the specified time
time.sleep(t_between)
# Capture
microscope.camera.capture(output)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
class TimelapseAPI(ActionView):
class TimelapseAPIView(ActionView):
"""
Take a series of images in a timelapse, running as a background task
Take a series of images in a timelapse
"""
args = {
@ -73,53 +110,10 @@ class TimelapseAPI(ActionView):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Create and start "timelapse", running in a background task
return timelapse(microscope, args.get("n_images"), args.get("t_between"))
# Start "timelapse"
return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Extension GUI (OpenFlexure eV)
# Alternate form without any dynamic parts
extension_gui = {
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
"forms": [ # List of forms. Each form is a collapsible accordion panel
{
"name": "Start a timelapse", # Form title
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")
# Add OpenFlexure eV GUI to your extension
my_extension.add_meta("gui", build_gui(extension_gui, my_extension))
LABTHINGS_EXTENSIONS = (TimelapseExtension,)

View file

@ -1,9 +1,11 @@
Basic extension structure
=========================
An extension starts as a simple instance of :py:class:`labthings.extensions.BaseExtension`.
An extension starts as a subclass of :py:class:`labthings.extensions.BaseExtension`.
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
You will build your extension by subclassing :py:class:`labthings.extensions.BaseExtension`, and adding the class to a top-level `LABTHINGS_EXTENSIONS` list.
In order to access the currently running microscope object, use the :py:func:`labthings.find_component` 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:
@ -24,13 +26,3 @@ Once this extension is loaded, any other extensions will have access to your met
# Call a function from your extension
if my_found_extension:
my_found_extension.identify()
Subclassing ``BaseExtension``
-------------------------------
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.extensions.BaseExtension` is recommended.
The same simple extension as seen above can be written using subclassing:
.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py

View file

@ -1,6 +1,14 @@
Adding web API views
====================
Key terminology
---------------
API View (or View)
++++++++++++++++++
*"A view function is the code you write to respond to requests to your application [...] For RESTful APIs its especially helpful to execute a different function for each HTTP method. With the [View class] you can easily do that. Each HTTP method maps to a function with the same name (just in lowercase)"* - `Flask documentation <https://flask.palletsprojects.com/en/1.1.x/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.
@ -13,6 +21,8 @@ Continuing our example on the previous page, and discussed below, adding API vie
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.
Your extension functions can be accessed from within an API View by using ``self.extension``. Once your view has been added to your extension, this will point to the extension object, allowing your API views to use your extension functionality.
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