From aeff3d14cf096a148e77b73bd98969f22f6dc414 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Tue, 21 Jan 2020 17:05:31 +0000 Subject: [PATCH] Added new documentation sections --- docs/source/extensions/introduction.rst | 21 ++++ docs/source/extensions/marshaling.rst | 67 +++++++++++ docs/source/extensions/structure.rst | 63 ++++++++++ docs/source/extensions/views.rst | 109 ++++++++++++++++++ docs/source/plugins.rst | 11 +- docs/source/plugins/structure.rst | 10 +- .../doc_example/01_basic_structure.py | 10 +- .../doc_example/02_adding_views.py | 10 +- 8 files changed, 280 insertions(+), 21 deletions(-) create mode 100644 docs/source/extensions/introduction.rst create mode 100644 docs/source/extensions/marshaling.rst create mode 100644 docs/source/extensions/structure.rst create mode 100644 docs/source/extensions/views.rst diff --git a/docs/source/extensions/introduction.rst b/docs/source/extensions/introduction.rst new file mode 100644 index 00000000..f8cf1f18 --- /dev/null +++ b/docs/source/extensions/introduction.rst @@ -0,0 +1,21 @@ +Introduction +============ + +Extensions allow functionality to be added to the OpenFlexure Microscope web API without having to modify the base code. +They have full access to the :py:class:`openflexure_microscope.Microscope` object, +including direct access to any attached :py:class:`openflexure_microscope.camera.base.BaseCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects. +This also allows access to the :py:class:`picamera.PiCamera` object. + +Extensions can either be loaded from a single Python file, or as a Python package installed to the environment being used. + +Single-file extensions +---------------------------- +For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a extension. This Python file must contain all of your extension objects, and be located in the applications extensions directory (by default ``~/.openflexure/microscope_extensions``). + +Package extensions +--------------- +Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions `_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension. + +The main restriction is that the extension package must be importable using an absolute import from within the Python environment being used to load your microscope. + +In order to enable your packaged extension, create a file in the applications extensions directory (by default ``~/.openflexure/microscope_extensions``) which imports your extension object(s) from your module. diff --git a/docs/source/extensions/marshaling.rst b/docs/source/extensions/marshaling.rst new file mode 100644 index 00000000..daf71ccd --- /dev/null +++ b/docs/source/extensions/marshaling.rst @@ -0,0 +1,67 @@ +Marshaling data +=============== + +Introduction +------------ + +- Define marshaling +- Why marshal objects? + +- Marshmallow library +- Marshmallow schema and fields + - Schema class + - Schema dictionaries + +- Using schema in ``@marshal_with`` +- Using schema in ``@use_args`` + +In the previous section we saw how to use fields to document the expected request body for simple requests, in which a single argument is required. By making use of Marshmallow schemas, we can allow more complex requests containing many parameters of different types. The parsed request parameters are then passed to the view function as a positional argument (as before), in the form of a dictionary. + +For example, your ``@use_args`` decorator may look like: + +.. code-block:: python + + @use_args({ + "name": fields.String(required=True), + "age": fields.Integer(required=True), + "job": fields.String(required=False, missing="Unknown") + }) + +A compatible request body, in JSON format, may look like: + +.. code-block:: json + + { + "name": "John Doe", + "age": 45, + "job": "Python developer" + } + +This JSON data is the parsed, converted into a Python dictionary, and passed as an argument. Retreiving the data from within your view function may therefore look like: + +.. code-block:: python + + @use_args({ + "name": fields.String(required=True), + "age": fields.Integer(required=True), + "job": fields.String(required=False, missing="Unknown") + }) + def post(self, args): + name = args.get("name") # Returns "John Doe", type str + age = args.get("age") # Returns 45, type int + job = args.get("job") # Returns "Python developer", type str + + +- Example (from ``03_marshaling_data.py``) + + +Scraps +++++++ + +An example request body that would be valid for this view is: + +.. code-block:: json + + { + "name": "My microscope name" + } \ No newline at end of file diff --git a/docs/source/extensions/structure.rst b/docs/source/extensions/structure.rst new file mode 100644 index 00000000..302fc206 --- /dev/null +++ b/docs/source/extensions/structure.rst @@ -0,0 +1,63 @@ +Basic extension structure +========================= + +An extension starts as a simple instance of :py:class:`labthings.server.extensions.BaseExtension`. +Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components. + +In order to access the currently running microscope object, use the :py:func:`labthings.server.find` 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: + +.. 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") + + +Once this extension is loaded, any other extensions will have access to your methods: + +.. code-block:: python + + from labthings.server.find import find_extension + + def test_extension_method(): + # Find your extension. Returns None if it hasn't been found. + my_found_extension = find_extension("com.myname.myextension") + + # Call a function from your extension + if my_found_extension: + my_found_extension.identify() \ No newline at end of file diff --git a/docs/source/extensions/views.rst b/docs/source/extensions/views.rst new file mode 100644 index 00000000..682e14dc --- /dev/null +++ b/docs/source/extensions/views.rst @@ -0,0 +1,109 @@ +Adding web API 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. + +As with most HTTP APIs, we make use of basic HTTP request methods. GET requests return data without modifying any state. POST requests completely replace data with data passed as request arguments. PUT requests update data with new data passed as request arguments. DELETE requests delete a particular object from the server. Your API views need not implement all of these methods. + +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") + +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. + +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 ++++++++++++++++++ + +For POST and PUT requests, data usually needs to be provided to the view in order to perform its function. In this example, our ``rename`` view requires a new microscope name to be passed. We make use of the ``@use_body`` decorator to provide this functionality. + +``@use_body`` defines the type of data expected in the request body. In this example, we ``String`` type data. The arguments of ``fields.String`` allow us to provide additional information, such as the parameter being required, and example values to appear in API documentation. + +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. + +Swagger documentation ++++++++++++++++++++++ + +At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/swagger-ui`` (or replace ``microscope.local`` with your microscopes IP address on incompatible systems). + +This page uses `SwaggerUI `_ to provide visual, interactive API documentation. Find your extensions URL in the documentation under the ``extensions`` group. Basic documentation about the parameters required for your POST method should be visible, as well as an interactive example filled out with the example request given in ``@use_body``. \ No newline at end of file diff --git a/docs/source/plugins.rst b/docs/source/plugins.rst index 7ffc17a7..e6a75963 100644 --- a/docs/source/plugins.rst +++ b/docs/source/plugins.rst @@ -4,10 +4,7 @@ Developing API Extensions .. toctree:: :maxdepth: 2 - ./plugins/introduction.rst - ./plugins/structure.rst - ./plugins/routes.rst - ./plugins/hardware.rst - ./plugins/forms.rst - ./plugins/example.rst - ./plugins/class.rst \ No newline at end of file + ./extensions/introduction.rst + ./extensions/structure.rst + ./extensions/views.rst + ./extensions/marshaling.rst \ No newline at end of file diff --git a/docs/source/plugins/structure.rst b/docs/source/plugins/structure.rst index ab04412d..a02d40ce 100644 --- a/docs/source/plugins/structure.rst +++ b/docs/source/plugins/structure.rst @@ -23,10 +23,12 @@ A simple extension file, with no API views but application-available methods may parent_camera = microscope.camera parent_stage = microscope.stage - response = "My parent camera is {}, and my parent stage is {}.".format(parent_camera, - parent_stage) + response = "My parent camera is {}, and my parent stage is {}.".format( + parent_camera, parent_stage + ) return response + def rename(new_name): """ Rename the microscope @@ -36,14 +38,14 @@ A simple extension file, with no API views but application-available methods may 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(hello_world, "rename") + my_extension.add_method(rename, "rename") Once this extension is loaded, any other extensions will have access to your methods: diff --git a/openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py b/openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py index 6aab7f7c..92496691 100644 --- a/openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py +++ b/openflexure_microscope/api/example_extensions/doc_example/01_basic_structure.py @@ -8,12 +8,12 @@ def identify(): """ microscope = find_component("org.openflexure.microscope") - parent_camera = microscope.camera - parent_stage = microscope.stage - - response = "My parent camera is {}, and my parent stage is {}.".format( - parent_camera, parent_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 diff --git a/openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py b/openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py index 1155eb94..ce815d55 100644 --- a/openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py +++ b/openflexure_microscope/api/example_extensions/doc_example/02_adding_views.py @@ -2,7 +2,7 @@ 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 +from labthings.server.decorators import use_body from labthings.server import fields ## Extension methods @@ -45,10 +45,10 @@ class ExampleIdentifyView(View): class ExampleRenameView(View): # 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") + @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")