From 6520db75f6c56916ef7df4837831941adffac285 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Wed, 22 Jan 2020 15:17:40 +0000 Subject: [PATCH] Wrote W3C sections --- docs/source/extensions/actions.rst | 54 ++++++++++++ docs/source/extensions/properties.rst | 117 ++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 docs/source/extensions/actions.rst create mode 100644 docs/source/extensions/properties.rst diff --git a/docs/source/extensions/actions.rst b/docs/source/extensions/actions.rst new file mode 100644 index 00000000..4a446340 --- /dev/null +++ b/docs/source/extensions/actions.rst @@ -0,0 +1,54 @@ +Thing Actions +============= + +Introduction +------------ + +As well as properties, the OpenFlexure Microscope Server also supports Thing Actions. +Thing Actions "invoke a function of the Thing, which manipulates state (e.g., toggling a lamp on or off) or triggers a process on the Thing (e.g., dim a lamp over time)." For the microscope, this would include moving the stage or taking a capture. Both of these require internal logic, and cannot be performed by changing a simple property. + +Actions should be *triggered* with POST requests *only*. Ideally, a view corresponding to an action should only support POST requests. + +Like properties, we use a class decorator to identify a view as an action: ``@ThingAction``. For example, a view to perform a "quick-capture" action may look like: + +.. code-block:: python + + @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") + +In this example, we are also making use of the ``@doc_response`` decorator, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``@use_args`` to accept optional parameters with POST requests. + +Again like properties, the ``@ThingAction`` decorator serves only to add documentation. + +Complete example +---------------- + +Adding this new view into our example extension, we now have: + +.. literalinclude:: ./example_extension/05_actions.py diff --git a/docs/source/extensions/properties.rst b/docs/source/extensions/properties.rst new file mode 100644 index 00000000..2e84a5e6 --- /dev/null +++ b/docs/source/extensions/properties.rst @@ -0,0 +1,117 @@ +Thing Properties +================ + +Introduction +------------ + +As well as generating Swagger documentation, the server will generate a draft `W3C Thing Description `_ . This description allows the microscope's features to be understood in a common "Web of Things" language. + +Thing Properties "expose state of the Thing. This state can then be retrieved (read) and optionally updated (write)." For the microscope, this includes the current read-only state, such as if the microscope has real camera or stage hardware attached, as well as read-write states like camera settings, and the microscope name. + +The property description for a view will be generated automatically from your available view methods, any schema decorators used, and any docstrings added to the view. + + +Defining Thing Properties +------------------------- + +In order to register a view as a Thing property, we use the ``@ThingProperty`` decorator, like so: + +.. code-block:: python + + # 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 + +This decorator serves only to add documentation: The view will be tagged with ``property`` in Swagger documentation, and will appear as a property in the microscopes Thing Description. + +However, the additional ``@PropertySchema`` decorator allows for simplified argument parsing and object marshaling. + +Property schema +--------------- + +For read-write properties, it is best practice for the expected request arguments, and the views responses, to follow the same format. In this way, by looking at the response of a GET request, one can know the type of data expected in by a PUT request. + +For example, if your GET request returns the JSON: + +.. code-block:: json + + { + "name": "John Doe", + "age": 45, + "job": "Python developer" + } + +and your property supports PUT requests (for updating data), then a valid PUT request could contain the data: + +.. code-block:: json + + { + "age": 46, + "job": "Landscape gardener" + } + +This request would update the property, such that a GET request would *now* return: + +.. code-block:: json + + { + "name": "John Doe", + "age": 46, + "job": "Landscape gardener" + } + +The ``@PropertySchema`` decorator can be applied to your view *class*, and essentially acts as shorthand for applying ``@marshal_with`` to all methods, and ``@use_args`` to all POST and PUT methods (or ``@use_body`` if only a single field is given). + +We will implement the ``@PropertySchema`` decorator in our ``ExampleRenameView`` view from our previous example: + +.. code-block:: python + + @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 + +Complete example +---------------- + +Combining these into our example extension, we now have: + +.. literalinclude:: ./example_extension/04_properties.py