Moved example extension and switched to includes

This commit is contained in:
jtc42 2020-01-22 15:17:31 +00:00
parent d2780f5541
commit 1115d00a18
12 changed files with 117 additions and 397 deletions

View file

@ -0,0 +1,36 @@
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")

View file

@ -0,0 +1,74 @@
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")

View file

@ -0,0 +1,76 @@
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, marshal_with
from labthings.server.schema import Schema
from labthings.server import fields
## Extension methods
# Define which properties of a Microscope object we care about,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
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
@marshal_with(MicroscopeIdentifySchema())
def get(self):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our microscope object,
# let @marshal_with handle formatting the output
return microscope
class ExampleRenameView(View):
# Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema())
# 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")
# 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 microscope object,
# let @marshal_with 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")

View file

@ -0,0 +1,97 @@
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,
marshal_with,
ThingProperty,
PropertySchema,
)
from labthings.server.schema import Schema
from labthings.server import fields
## Extension methods
# Define which properties of a Microscope object we care about,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
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
@ThingProperty
class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema())
def get(self):
"""
Show identifying information about the current microscope object
"""
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our microscope object,
# let @marshal_with handle formatting the output
return microscope
@ThingProperty
# 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
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
class ExampleRenameView(View):
def get(self):
"""
Show the current microscope name
"""
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
return microscope
def post(self, args):
"""
Change the current microscope name
"""
# Look for our "name" parameter in the request arguments
new_name = args.get("name")
# 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 microscope object,
# let @marshal_with 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")

View file

@ -0,0 +1,133 @@
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,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
)
from labthings.server.schema import Schema
from labthings.server import fields
from flask import send_file # Used to send images from our server
import io # Used in our capture action
## Extension methods
# Define which properties of a Microscope object we care about,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
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
@ThingProperty
class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema())
def get(self):
"""
Show identifying information about the current microscope object
"""
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our microscope object,
# let @marshal_with handle formatting the output
return microscope
@ThingProperty
# 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
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
class ExampleRenameView(View):
def get(self):
"""
Show the current microscope name
"""
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
return microscope
def post(self, args):
"""
Change the current microscope name
"""
# Look for our "name" parameter in the request arguments
new_name = args.get("name")
# 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 microscope object,
# let @marshal_with handle formatting the output
return microscope
@ThingAction
class QuickCaptureAPI(View):
"""
Take an image capture and return it without saving
"""
# Expect a "use_video_port" boolean, which defaults to True if none is given
@use_args({"use_video_port": fields.Boolean(missing=True)})
# Our success response (200) returns an image (image/jpeg mimetype)
@doc_response(200, mimetype="image/jpeg")
def post(self, args):
"""
Take a non-persistant image capture.
"""
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Open a BytesIO stream to be destroyed once request has returned
with io.BytesIO() as stream:
# Capture to our stream object
microscope.camera.capture(stream, use_video_port=args.get("use_video_port"))
# Rewind the stream
stream.seek(0)
# Return our image data using Flasks send_file function
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")

View file

@ -0,0 +1,108 @@
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,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
marshal_task,
)
from labthings.server.schema import Schema
from labthings.server import fields
from flask import send_file # Used to send images from our server
import io # Used in our capture action
import time # Used in our timelapse function
# Used in our timelapse function
from openflexure_microscope.camera.base import generate_basename
# Used to run our timelapse in a background thread
from labthings.core.tasks import taskify, update_task_progress
## Extension methods
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a 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)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# 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
)
# 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_task_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
@ThingAction
class TimelapseAPI(View):
"""
Take a series of images in a timelapse, running as a background task
"""
@marshal_task # Shorthand for marshaling with a pre-made Task object schema
@use_args(
{
"n_images": fields.Integer(
required=True, example=5, description="Number of images"
),
"t_between": fields.Number(
missing=1, example=1, description="Time (seconds) between images"
),
}
)
def post(self, args):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Create and start "timelapse", running in a background task
task = taskify(timelapse)(
microscope, args.get("n_images"), args.get("t_between")
)
return task
## 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")

View file

@ -0,0 +1,146 @@
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,
marshal_with,
ThingProperty,
PropertySchema,
ThingAction,
doc_response,
marshal_task,
)
from labthings.server.schema import Schema
from labthings.server import fields
from flask import send_file # Used to send images from our server
import io # Used in our capture action
import time # Used in our timelapse function
# Used in our timelapse function
from openflexure_microscope.camera.base import generate_basename
# Used to run our timelapse in a background thread
from labthings.core.tasks import taskify, update_task_progress
# Used to convert our GUI dictionary into a complete eV extension GUI
from openflexure_microscope.api.utilities.gui import build_gui
## Extension methods
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a 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)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# 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
)
# 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_task_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
@ThingAction
class TimelapseAPI(View):
"""
Take a series of images in a timelapse, running as a background task
"""
@marshal_task # Shorthand for marshaling with a pre-made Task object schema
@use_args(
{
"n_images": fields.Integer(
required=True, example=5, description="Number of images"
),
"t_between": fields.Number(
missing=1, example=1, description="Time (seconds) between images"
),
}
)
def post(self, args):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Create and start "timelapse", running in a background task
task = taskify(timelapse)(
microscope, args.get("n_images"), args.get("t_between")
)
return task
## 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
"isCollapsible": False, # This form cannot be collapsed into an accordion
"isTask": True, # This forms submission starts a background task
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"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))

View file

@ -160,80 +160,4 @@ Complete example
Combining both of these into our example extension, we now have:
.. 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_args, marshal_with
from labthings.server.schema import Schema
from labthings.server import fields
## Extension methods
# Define which properties of a Microscope object we care about,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string)
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
@marshal_with(MicroscopeIdentifySchema())
def get(self):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our microscope object,
# let @marshal_with handle formatting the output
return microscope
class ExampleRenameView(View):
# Format our returned object using MicroscopeIdentifySchema
@marshal_with(MicroscopeIdentifySchema())
# 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")
# 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 microscope object,
# let @marshal_with 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")
.. literalinclude:: ./example_extension/03_marshaling_data.py

View file

@ -8,44 +8,7 @@ In order to access the currently running microscope object, use the :py:func:`la
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")
.. literalinclude:: ./example_extension/01_basic_structure.py
Once this extension is loaded, any other extensions will have access to your methods:

View file

@ -9,82 +9,7 @@ As with most HTTP APIs, we make use of basic HTTP request methods. GET requests
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")
.. literalinclude:: ./example_extension/02_adding_views.py
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.
@ -99,7 +24,7 @@ For POST and PUT requests, data usually needs to be provided to the view in orde
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.
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
+++++++++++++++++++++