From 1115d00a184d95ff635607854f0c5924f6c416cc Mon Sep 17 00:00:00 2001 From: jtc42 Date: Wed, 22 Jan 2020 15:17:31 +0000 Subject: [PATCH] Moved example extension and switched to includes --- .../example_extension}/01_basic_structure.py | 0 .../example_extension}/02_adding_views.py | 0 .../example_extension}/03_marshaling_data.py | 0 .../example_extension}/04_properties.py | 0 .../example_extension}/05_actions.py | 0 .../example_extension/06_tasks_locks.py | 108 ++++++++++ .../example_extension}/07_ev_gui.py | 2 +- docs/source/extensions/marshaling.rst | 78 +------ docs/source/extensions/structure.rst | 39 +--- docs/source/extensions/views.rst | 79 +------ docs/source/plugins.rst | 5 +- .../doc_example/06_tasks_locks.py | 203 ------------------ 12 files changed, 117 insertions(+), 397 deletions(-) rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/01_basic_structure.py (100%) rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/02_adding_views.py (100%) rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/03_marshaling_data.py (100%) rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/04_properties.py (100%) rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/05_actions.py (100%) create mode 100644 docs/source/extensions/example_extension/06_tasks_locks.py rename {openflexure_microscope/api/example_extensions/doc_example => docs/source/extensions/example_extension}/07_ev_gui.py (98%) delete mode 100644 openflexure_microscope/api/example_extensions/doc_example/06_tasks_locks.py diff --git a/openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py b/docs/source/extensions/example_extension/01_basic_structure.py similarity index 100% rename from openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py rename to docs/source/extensions/example_extension/01_basic_structure.py diff --git a/openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py b/docs/source/extensions/example_extension/02_adding_views.py similarity index 100% rename from openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py rename to docs/source/extensions/example_extension/02_adding_views.py diff --git a/openflexure_microscope/api/example_extensions/doc_example/03_marshaling_data.py b/docs/source/extensions/example_extension/03_marshaling_data.py similarity index 100% rename from openflexure_microscope/api/example_extensions/doc_example/03_marshaling_data.py rename to docs/source/extensions/example_extension/03_marshaling_data.py diff --git a/openflexure_microscope/api/example_extensions/doc_example/04_properties.py b/docs/source/extensions/example_extension/04_properties.py similarity index 100% rename from openflexure_microscope/api/example_extensions/doc_example/04_properties.py rename to docs/source/extensions/example_extension/04_properties.py diff --git a/openflexure_microscope/api/example_extensions/doc_example/05_actions.py b/docs/source/extensions/example_extension/05_actions.py similarity index 100% rename from openflexure_microscope/api/example_extensions/doc_example/05_actions.py rename to docs/source/extensions/example_extension/05_actions.py diff --git a/docs/source/extensions/example_extension/06_tasks_locks.py b/docs/source/extensions/example_extension/06_tasks_locks.py new file mode 100644 index 00000000..755158b5 --- /dev/null +++ b/docs/source/extensions/example_extension/06_tasks_locks.py @@ -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") diff --git a/openflexure_microscope/api/example_extensions/doc_example/07_ev_gui.py b/docs/source/extensions/example_extension/07_ev_gui.py similarity index 98% rename from openflexure_microscope/api/example_extensions/doc_example/07_ev_gui.py rename to docs/source/extensions/example_extension/07_ev_gui.py index 7563c80b..08bfd90b 100644 --- a/openflexure_microscope/api/example_extensions/doc_example/07_ev_gui.py +++ b/docs/source/extensions/example_extension/07_ev_gui.py @@ -134,7 +134,7 @@ extension_gui = { ## Create extension # Create your extension object -my_extension = BaseExtension("com.myname.myextension", version="0.0.0") +my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0") # Add methods to your extension my_extension.add_method(timelapse, "timelapse") diff --git a/docs/source/extensions/marshaling.rst b/docs/source/extensions/marshaling.rst index c5f66ef3..10ab188e 100644 --- a/docs/source/extensions/marshaling.rst +++ b/docs/source/extensions/marshaling.rst @@ -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 \ No newline at end of file diff --git a/docs/source/extensions/structure.rst b/docs/source/extensions/structure.rst index 302fc206..ee2bc478 100644 --- a/docs/source/extensions/structure.rst +++ b/docs/source/extensions/structure.rst @@ -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: diff --git a/docs/source/extensions/views.rst b/docs/source/extensions/views.rst index 682e14dc..7d40b82b 100644 --- a/docs/source/extensions/views.rst +++ b/docs/source/extensions/views.rst @@ -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 +++++++++++++++++++++ diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index e6a75963..2a72f0bd 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -7,4 +7,7 @@ Developing API Extensions ./extensions/introduction.rst ./extensions/structure.rst ./extensions/views.rst - ./extensions/marshaling.rst \ No newline at end of file + ./extensions/marshaling.rst + ./extensions/properties.rst + ./extensions/actions.rst + ./extensions/tasks_locks.rst \ No newline at end of file diff --git a/openflexure_microscope/api/example_extensions/doc_example/06_tasks_locks.py b/openflexure_microscope/api/example_extensions/doc_example/06_tasks_locks.py deleted file mode 100644 index ea119019..00000000 --- a/openflexure_microscope/api/example_extensions/doc_example/06_tasks_locks.py +++ /dev/null @@ -1,203 +0,0 @@ -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 - -## 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() - - -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.file) - - # Add system metadata - output.put_metadata(microscope.metadata, system=True) - - # Wait for the specified time - time.sleep(t_between) - - -## 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") - - -@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.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") -my_extension.add_view(TimelapseAPI, "/timelapse")