diff --git a/docs/source/extensions/example_extension/01_basic_structure.py b/docs/source/extensions/example_extension/01_basic_structure.py index eec8f362..21301820 100644 --- a/docs/source/extensions/example_extension/01_basic_structure.py +++ b/docs/source/extensions/example_extension/01_basic_structure.py @@ -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,) diff --git a/docs/source/extensions/example_extension/01b_basic_structure_subclass.py b/docs/source/extensions/example_extension/01b_basic_structure_subclass.py deleted file mode 100644 index 9d1b93f3..00000000 --- a/docs/source/extensions/example_extension/01b_basic_structure_subclass.py +++ /dev/null @@ -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() diff --git a/docs/source/extensions/example_extension/02_adding_views.py b/docs/source/extensions/example_extension/02_adding_views.py index d22a9ae2..69aa6d6f 100644 --- a/docs/source/extensions/example_extension/02_adding_views.py +++ b/docs/source/extensions/example_extension/02_adding_views.py @@ -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,) diff --git a/docs/source/extensions/example_extension/03_marshaling_data.py b/docs/source/extensions/example_extension/03_marshaling_data.py index aceefcea..63a73575 100644 --- a/docs/source/extensions/example_extension/03_marshaling_data.py +++ b/docs/source/extensions/example_extension/03_marshaling_data.py @@ -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,) diff --git a/docs/source/extensions/example_extension/04_properties.py b/docs/source/extensions/example_extension/04_properties.py index 3bedbad0..9fc7862d 100644 --- a/docs/source/extensions/example_extension/04_properties.py +++ b/docs/source/extensions/example_extension/04_properties.py @@ -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,) diff --git a/docs/source/extensions/example_extension/05_actions.py b/docs/source/extensions/example_extension/05_actions.py index 52b1b024..dcee9ad8 100644 --- a/docs/source/extensions/example_extension/05_actions.py +++ b/docs/source/extensions/example_extension/05_actions.py @@ -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,) diff --git a/docs/source/extensions/example_extension/06_tasks_locks.py b/docs/source/extensions/example_extension/06_tasks_locks.py index 710b4e73..79f9b1ef 100644 --- a/docs/source/extensions/example_extension/06_tasks_locks.py +++ b/docs/source/extensions/example_extension/06_tasks_locks.py @@ -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,) diff --git a/docs/source/extensions/example_extension/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py index a71d9053..0782af73 100644 --- a/docs/source/extensions/example_extension/07_ev_gui.py +++ b/docs/source/extensions/example_extension/07_ev_gui.py @@ -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,) diff --git a/docs/source/extensions/structure.rst b/docs/source/extensions/structure.rst index ee3ee0ce..c05091f3 100644 --- a/docs/source/extensions/structure.rst +++ b/docs/source/extensions/structure.rst @@ -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 diff --git a/docs/source/extensions/views.rst b/docs/source/extensions/views.rst index be8364fb..ff2e5f57 100644 --- a/docs/source/extensions/views.rst +++ b/docs/source/extensions/views.rst @@ -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 it’s 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 `_ + 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