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 PropertyView class, like so:

# Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView):
    # Format our returned object using MicroscopeIdentifySchema
    schema = 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 schemah handle formatting the output
        return microscope

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:

{
    "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:

{
    "age": 46,
    "job": "Landscape gardener"
}

This request would update the property, such that a GET request would now return:

{
    "name": "John Doe",
    "age": 46,
    "job": "Landscape gardener"
}

In Property Views the schema class attribute acts as the schema for both marshalling responses and parsing arguments. This is because property requests and responses should be identically formatted.

We will implement the schema attribute in our ExampleRenameView view from our previous example:

# We can use a single schema as the input and output will be formatted identically
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
class ExampleRenameView(PropertyView):
    schema = {"name": fields.String(required=True, metadata={"example": "My Example Microscope"})}

    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 schema handle formatting the output
        return microscope

Complete example

Combining these into our example extension, we now have:

from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import PropertyView


# 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,
# and what types they should be converted to
class MicroscopeIdentifySchema(Schema):
    name = fields.String()  # Microscopes name
    id = fields.UUID()  # Microscopes unique ID
    state = fields.Dict()  # Status dictionary
    camera = fields.String()  # Camera object (represented as a string)
    stage = fields.String()  # Stage object (represented as a string)


## Extension viewss

# Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView):
    # Format our returned object using MicroscopeIdentifySchema
    schema = 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 schema handle formatting the output
        return microscope


# We can use a single schema as the input and output will be formatted identically
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
class ExampleRenameView(PropertyView):
    schema = {
        "name": fields.String(
            required=True, metadata={"example": "My Example Microscope"}
        )
    }

    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
        self.extension.rename(microscope, new_name)

        # Return our microscope object,
        # let schema handle formatting the output
        return microscope


LABTHINGS_EXTENSIONS = (MyExtension,)