labthings_fastapi
LabThings-FastAPI.
This is the top level module for LabThings-FastAPI, a library for building Web of Things Core Concepts devices using Python. There is documentation on readthedocs, and the recommended place to start is labthings_fastapi.
This module contains a number of convenience imports and is intended to be imported using:
import labthings_fastapi as lt
The example code elsewhere in the documentation generally follows this convention. Symbols in the top-level module mostly exist elsewhere in the package, but should be imported from here as a preference, to ensure code does not break if modules are rearranged.
Submodules
- labthings_fastapi.actions
- labthings_fastapi.base_descriptor
- labthings_fastapi.client
- labthings_fastapi.endpoints
- labthings_fastapi.example_things
- labthings_fastapi.exceptions
- labthings_fastapi.invocation_contexts
- labthings_fastapi.invocations
- labthings_fastapi.logs
- labthings_fastapi.middleware
- labthings_fastapi.notifications
- labthings_fastapi.outputs
- labthings_fastapi.properties
- labthings_fastapi.server
- labthings_fastapi.testing
- labthings_fastapi.thing
- labthings_fastapi.thing_description
- labthings_fastapi.thing_server_interface
- labthings_fastapi.thing_slots
- labthings_fastapi.types
- labthings_fastapi.utilities
- labthings_fastapi.websockets
Classes
Represents a Thing, as defined by the Web of Things standard. |
|
An interface for Things to interact with their server. |
|
A Property descriptor that acts like a regular variable. |
|
A |
|
Use FastAPI to serve |
|
The information needed to add a |
|
The configuration parameters for a |
|
A client for a LabThings-FastAPI Thing. |
|
A thread that sets a new invocation ID. |
Functions
|
Declare a connection to another |
|
Define a Property on a |
|
Define a Setting on a |
|
Mark a method of a |
|
Mark a function as a FastAPI endpoint without making it an action. |
|
Sleep for a specified time, allowing cancellation. |
|
Raise an exception if the current invocation has been cancelled. |
Package Contents
- class labthings_fastapi.Thing(thing_server_interface: labthings_fastapi.thing_server_interface.ThingServerInterface)
Represents a Thing, as defined by the Web of Things standard.
This class should encapsulate the code that runs a piece of hardware, or provides a particular function - it will correspond to a path on the server, and a Thing Description document.
Subclassing Notes
__init__: You should accept any arguments you need to configure the Thing in__init__. Don’t initialise any hardware at this time, as your Thing may be instantiated quite early, or even at import time. You must make sure to callsuper().__init__(thing_server_interface).__enter__(self)and__exit__(self, exc_t, exc_v, exc_tb)are where you should start and stop communications with the hardware. This is Python’s “context manager” protocol. The arguments of__exit__will beNoneexcept after errors. You should be safe to ignore them, and just include code that will close down your hardware, which is equivalent to afinally:block.Properties and Actions are defined using decorators: the
@actiondecorator declares a method to be an action, which will run when it’s triggered, and the@propertydecorator does the same for a property.Properties may also be defined using dataclass-style syntax, if they do not need getter and setter functions.
See the documentation on those functions for more detail.
titlewill be used in various places as the human-readable name of your Thing, so it makes sense to set this in a subclass.
There are various LabThings methods that you should avoid overriding unless you know what you are doing: anything not mentioned above that’s defined in
Thingis probably best left alone. They may in time be collected together into a single object to avoid namespace clashes.Initialise a Thing.
The most important function of
__init__is attaching the thing_server_interface, and setting the path. Note thatThinginstances are usually created by aThingServerand not instantiated directly: if you do make aThingdirectly, you will need to supply aThingServerInterfacethat is connected to aThingServeror a suitable mock object.- param thing_server_interface:
The interface to the server that is hosting this Thing. It will be supplied when the
Thingis instantiated by theThingServeror bycreate_thing_without_serverwhich generates a mock interface.
- _thing_server_interface: labthings_fastapi.thing_server_interface.ThingServerInterface
Provide access to features of the server that this
Thingis attached to.
- property logger: logging.Logger
A logger, named after this Thing.
- async __aenter__() typing_extensions.Self
Context management is used to set up/close the thing.
As things (currently) do everything with threaded code, we define async
__aenter__and__aexit__wrappers to call the synchronous code, if it exists.- Returns:
this object.
- async __aexit__(exc_t: Any | None, exc_v: Any | None, exc_tb: Any) None
Wrap context management functions, if they exist.
See
__aenter__for more details.- Parameters:
exc_t – The type of the exception, or
None.exc_v – The exception that occurred, or
None.exc_tb – The traceback for the exception, or
None.
- attach_to_server(server: labthings_fastapi.server.ThingServer) None
Attach this thing to the server.
Things need to be attached to a server before use to function correctly.
- Parameters:
server – The server to attach this Thing to.
Attaching the
Thingto aThingServerallows theThingto start actions, load its settings from the correct place, and create HTTP endpoints to allow it to be accessed from the HTTP API.We create HTTP endpoints for all Interaction Affordances on the
Thing, as well as anyEndpointDescriptordescriptors.
- _read_settings_file() collections.abc.Mapping[str, Any] | None
Read the settings file and return a mapping of saved settings or None.
This function handles reading the settings from the disk. It is designed to be called by
load_settings. Any exceptions caused by file handling or file corruption are caught and logged as warnings.- Returns:
A Mapping of setting name to setting value, or None if no settings could be read from file.
- load_settings() None
Load settings from json.
Read the JSON file and use it to populate settings.
Note
Settings are loaded when the Thing is added to a server, so they will not be available while the
__init__method is run.Note that no notifications will be triggered when the settings are set, so if action is needed (e.g. updating hardware with the loaded settings) it should be taken in
__enter__.
- save_settings() None
Save settings to JSON.
This is called whenever a setting is updated. All settings are written to the settings file every time.
- properties: labthings_fastapi.base_descriptor.OptionallyBoundDescriptor[Thing, labthings_fastapi.properties.PropertyCollection]
Access to metadata and functions of this
Thing‘s properties.Thing.propertiesis a mapping of names toPropertyInfoobjects, which allows convenient access to the metadata related to its properties. Note that this includes settings, as they are a subclass of properties.
- settings: labthings_fastapi.base_descriptor.OptionallyBoundDescriptor[Thing, labthings_fastapi.properties.SettingCollection]
Access to settings-related metadata and functions.
Thing.settingsis a mapping of names toSettingInfoobjects that allows convenient access to metadata of the settings of thisThing.
- actions: labthings_fastapi.base_descriptor.OptionallyBoundDescriptor[Thing, labthings_fastapi.actions.ActionCollection]
Access to metadata for the actions of this
Thing.Thing.actionsis a mapping of names toActionInfoobjects that allows convenient access to metadata of each action.
- property thing_state: collections.abc.Mapping
Return a dictionary summarising our current state.
This is intended to be an easy way to collect metadata from a Thing that summarises its state. It might be used, for example, to record metadata along with each reading/image/etc. when an instrument is saving data.
It’s best to populate this automatically so it can always be accessed. If it requires calls e.g. to a serial instrument, bear in mind it may be called quite often and shouldn’t take too long.
Some measure of caching here is a nice aim for the future, but not yet implemented.
- _cached_thing_description: tuple[str | None, str | None, labthings_fastapi.thing_description._model.ThingDescription] | None = None
- thing_description(path: str | None = None, base: str | None = None) labthings_fastapi.thing_description._model.ThingDescription
Generate a w3c Thing Description representing this thing.
The w3c Web of Things working group defined a standard representation of a Thing, which provides a high-level description of the actions, properties, and events that it exposes. This endpoint delivers a JSON representation of the Thing Description for this Thing.
- Parameters:
path – the URL pointing to this Thing.
base – the base URL for all URLs in the thing description.
- Returns:
a Thing Description.
- thing_description_dict(path: str | None = None, base: str | None = None) dict
Describe this Thing with a Thing Description as a simple dict.
See
Thing.thing_description. This function converts the return value of that function into a simple dictionary.- Parameters:
path – the URL pointing to this Thing.
base – the base URL for all URLs in the thing description.
- Returns:
a Thing Description.
- observe_property(property_name: str, stream: anyio.abc.ObjectSendStream) None
Register a stream to receive property change notifications.
- Parameters:
property_name – the property to register for.
stream – the stream used to send events.
- Raises:
KeyError – if the requested name is not defined on this Thing.
PropertyNotObservableError – if the property is not observable.
- observe_action(action_name: str, stream: anyio.abc.ObjectSendStream) None
Register a stream to receive action status change notifications.
- Parameters:
action_name – the action to register for.
stream – the stream used to send events.
- Raises:
KeyError – if the requested name is not defined on this Thing.
- get_current_invocation_logs() list[logging.LogRecord]
Get the log records for an on going action.
This is useful if an action wishes to save its logs alongside any data.
Note that only the last 1000 logs are returned so for long running tasks that log frequently this may want to be read periodically.
This will error if it is called outside an action invocation.
- Returns:
a list of all logs from this action.
- Raises:
RuntimeError – If the server cannot be retrieved. This should never happen.
- labthings_fastapi.thing_slot(default: str | collections.abc.Iterable[str] | None | types.EllipsisType = ...) Any
Declare a connection to another
Thingin the same server.lt.thing_slotmarks a class attribute as a connection to anotherThingon the same server. This will be automatically supplied when the server is started, based on the type hint and default value.In keeping with
propertyandsetting, the type of the attribute should be the type of the connectedThing. For example:import labthings_fastapi as lt class ThingA(lt.Thing): ... class ThingB(lt.Thing): "A class that relies on ThingA." thing_a: ThingA = lt.thing_slot()
This function is a convenience wrapper around the
ThingSlotdescriptor class, and should be used in preference to using the descriptor directly. The main reason to use the function is that it suppresses type errors when using static type checkers such asmypyorpyright(see note below).The type hint of a Thing Connection should be one of the following:
- A
Thingsubclass. An instance of this subclass will be returned when the attribute is accessed.
- A
- An optional
Thingsubclass (e.g.MyThing | None). This will either return a
MyThinginstance orNone.
- An optional
Example:
import labthings_fastapi as lt class ThingA(lt.Thing): "An example Thing." class ThingB(lt.Thing): "An example Thing with connections." thing_a: ThingA = lt.thing_slot() maybe_thing_a: ThingA | None = lt.thing_slot() all_things_a: Mapping[str, ThingA] = lt.thing_slot() @lt.action def show_connections(self) -> str: "Tell someone about our connections." self.thing_a # should always evaluate to a ThingA instance self.maybe_thing_a # will be a ThingA instance or None self.all_things_a # will a mapping of names to ThingA instances return f"{self.thing_a=}, {self.maybe_thing_a=}, {self.all_things_a=}"
The example above is very contrived, but shows how to apply the different types.
If no default value is supplied, and no value is configured for the connection, the server will attempt to find a
Thingthat matches the specified type when the server is started. If no matchingThinginstances are found, the descriptor will returnNoneor an empty mapping. If that is not allowed by the type hint, the server will fail to start with an error.The default value may be a string specifying a
Thingname, or a sequence of strings (for connections that return mappings). In those cases, the relevantThingwill be returned from the server. If a name is given that either doesn’t correspond to aThingon the server, or is aThingthat doesn’t match the type of this connection, the server will fail to start with an error.The default may also be
Nonewhich is appropriate when the type is optional or a mapping. If the type is aThingsubclass, a default value ofNoneforces the connection to be specified in configuration.- Parameters:
default – The name(s) of the Thing(s) that will be connected by default. If the default is omitted or set to
...the server will attempt to find a matchingThinginstance (or instances). A default value ofNoneis allowed if the connection is type hinted as optional.- Returns:
A
ThingSlotdescriptor.
Typing notes:
In the example above, using
ThingSlotdirectly would assign an object with typeThingSlot[ThingA]to the attributething_a, which is typed asThingA. This would cause a type error. Usingthing_slotsuppresses this error, as its return type is a`Any``.The use of
Anyor an alternative type-checking exemption seems to be inevitable when implementing descriptors that are typed via attribute annotations, and it is done by established libraries such aspydantic.
- class labthings_fastapi.ThingServerInterface(server: labthings_fastapi.server.ThingServer, name: str)
An interface for Things to interact with their server.
This is added to every
Thingduring__init__and is available asself._thing_server_interface.Initialise a ThingServerInterface.
The ThingServerInterface sits between a Thing and its ThingServer, with the intention of providing a useful set of functions, without exposing too much of the server to the Thing.
One reason for using this intermediary class is to make it easier to mock the server during testing: only functions provided here need be mocked, not the whole functionality of the server.
- Parameters:
server – the
ThingServerinstance we’re connected to. This will be retained as a weak reference.name – the name of the
Thinginstance this interface is provided for.
- _server: weakref.ReferenceType[labthings_fastapi.server.ThingServer]
- _get_server() labthings_fastapi.server.ThingServer
Return a live reference to the ThingServer.
This will evaluate the weak reference to the ThingServer, and will raise an exception if the server has been garbage collected.
The server is, in practice, not going to be finalized before the Things, so this should not be a problem.
- Returns:
the ThingServer.
- Raises:
ThingServerMissingError – if the
ThingServeris no longer available.
- start_async_task_soon(async_function: Callable[Params, Awaitable[ReturnType]], *args: Any) concurrent.futures.Future[ReturnType]
Run an asynchronous task in the server’s event loop.
This function wraps
anyio.from_thread.BlockingPortal.start_task_soonto provide a way of calling asynchronous code from threaded code. It will call the provided async function in the server’s event loop, without any guarantee of exactly when it will happen. This means we will return immediately, and the return value of this function will be aconcurrent.futures.Futureobject that may resolve to the async function’s return value.- Parameters:
async_function – the asynchronous function to call.
*args – positional arguments to be provided to the function.
- Returns:
an
asyncio.Futureobject wrapping the return value.- Raises:
ServerNotRunningError – if the server is not running (i.e. there is no event loop).
- call_async_task(async_function: Callable[Params, Awaitable[ReturnType]], *args: Any) ReturnType
Run an asynchronous task in the server’s event loop in a blocking manner.
This function wraps
anyio.from_thread.BlockingPortal.callto provide a way of calling asynchronous code from threaded code. It will block the current thread while it calls the provided async function in the server’s event loop.Do not call this from the event loop or it may lead to a deadlock.
- Parameters:
async_function – the asynchronous function to call.
*args – positional arguments to be provided to the function.
- Returns:
The return value from the asynchronous function.
- Raises:
ServerNotRunningError – if the server is not running (i.e. there is no event loop).
- property path: str
The path, relative to the server’s base URL, of the Thing.
A ThingServerInterface is specific to one Thing, so this path points to the base URL of the Thing, i.e. the Thing Description’s endpoint.
- property application_config: Mapping[str, Any] | None
The custom application configuration options from configuration.
- get_thing_states() Mapping[str, Any]
Retrieve metadata from all Things on the server.
This function will retrieve the
Thing.thing_stateproperty from eachThingon the server, and return it as a dictionary. It is intended to make it easy to add metadata to the results of actions, for example to embed in an image.- Returns:
a dictionary of metadata, with the
Thingnames as keys.
- property _action_manager: labthings_fastapi.actions.ActionManager
The ActionManager for the Thing attached to this interface.
This property may be removed in future, and is for internal use only.
- labthings_fastapi.property(getter: Callable[[Owner], Value]) FunctionalProperty[Owner, Value]
- labthings_fastapi.property(*, default: Value, readonly: bool = False, **constraints: Any) Value
- labthings_fastapi.property(*, default_factory: Callable[[], Value], readonly: bool = False, **constraints: Any) Value
Define a Property on a
Thing.This function may be used to define Properties in two ways, as either a decorator or a field specifier. See the examples in the
propertydocumentation.Properties should always have a type annotation. This type annotation will be used in automatic documentation and also to serialise the value to JSON when it is sent over the network. This mean that the type of your property should either be JSON serialisable (i.e. simple built-in types) or a subclass of
pydantic.BaseModel.- Parameters:
getter – is a method of a class that returns the value of this property. This is usually supplied by using
propertyas a decorator.default – is the default value. Either this,
getterordefault_factorymust be specified. Specifying both or neither will raise an exception.default_factory – should return your default value. This may be used as an alternative to
defaultif you need to use a mutable datatype. For example, it would be better to specifydefault_factory=listthandefault=[]because the second form would be shared between allThings with this property.readonly – whether the property should be read-only via the
ThingClientinterface (i.e. over HTTP or via aDirectThingClient). This is automatically true ifpropertyis used as a decorator and no setter is specified.**constraints – additional keyword arguments are passed to
pydantic.Fieldand allow constraints to be added to the property. For example,ge=0constrains a numeric property to be non-negative. Seepydantic.Fieldfor the full range of constraint arguments.
- Returns:
a property descriptor, either a
FunctionalPropertyif used as a decorator, or aDataPropertyif used as a field.- Raises:
MissingDefaultError – if no valid default value is supplied, and a getter is not in use.
OverspecifiedDefaultError – if the default is specified more than once (e.g.
default,default_factory, orgetter).
Typing Notes
This function has somewhat complicated type hints, for two reasons. Firstly, it may be used either as a decorator or as a field specifier, so
defaultperforms double duty as a default value or a getter. Secondly, when used as a field specifier the type hint for the property is attached to the attribute of the class to which the function’s output is assigned. This meanspropertydoes not know its type hint until after it’s been called.When used as a field specifier,
propertyreturns a genericDataPropertydescriptor instance, which will determine its type when it is attached to theThing. The type hint on the return value ofpropertyin that situation is a “white lie”: we annotate the return as having the same type as thedefaultvalue (or thedefault_factoryreturn value). This means that type checkers such asmypywill check that the default is valid for the type of the field, and won’t raise an error about assigning, for example, an instance ofDataProperty[int]to a field annotated asint.Finally, the type of the
defaultargument includesEllipsisTypeso that we can use...as its default value. This allows us to distinguish betweendefaultnot being set (...) and a desired default value ofNone. Similarly,...is the default value forgetterso we can raise a more helpful error if a non-callable value is passed as the first argument.
- labthings_fastapi.setting(getter: Callable[[Owner], Value]) FunctionalSetting[Owner, Value]
- labthings_fastapi.setting(*, default: Value, readonly: bool = False, **constraints: Any) Value
- labthings_fastapi.setting(*, default_factory: Callable[[], Value], readonly: bool = False, **constraints: Any) Value
Define a Setting on a
Thing.A setting is a property that is saved to disk.
This function defines a setting, which is a special Property that will be saved to disk, so it persists even when the LabThings server is restarted. It is otherwise very similar to
property.A type annotation is required, and should follow the same constraints as for
@property.Every
settingon aThingwill be read each time the settings are saved, which may be quite frequent. This means your getter must not take too long to run, or have side-effects. Settings that use getters and setters may be removed in the future pending the outcome of #159.If the type is a pydantic BaseModel, then the setter must also be able to accept the dictionary representation of this BaseModel as this is what will be used to set the Setting when loading from disk on starting the server.
Note
If a setting is mutated rather than set, this will not trigger saving. For example: if a Thing has a setting called
dictsettingholding the dictionary{"a": 1, "b": 2}thenself.dictsetting = {"a": 2, "b": 2}would trigger saving butself.dictsetting[a] = 2would not, as the setter fordictsettingis never called.- Parameters:
getter – is a method of a class that returns the value of this property. This is usually supplied by using
propertyas a decorator.default – is the default value. Either this,
getterordefault_factorymust be specified. Specifying both or neither will raise an exception.default_factory – should return your default value. This may be used as an alternative to
defaultif you need to use a mutable datatype. For example, it would be better to specifydefault_factory=listthandefault=[]because the second form would be shared between allThings with this setting.readonly – whether the setting should be read-only via the
ThingClientinterface (i.e. over HTTP or via aDirectThingClient).**constraints – additional keyword arguments are passed to
pydantic.Fieldand allow constraints to be added to the property. For example,ge=0constrains a numeric property to be non-negative. Seepydantic.Fieldfor the full range of constraint arguments.
- Returns:
a setting descriptor.
- Raises:
MissingDefaultError – if no valid default or getter is supplied.
OverspecifiedDefaultError – if the default is specified more than once (e.g.
default,default_factory, orgetter).
Typing Notes
See the typing notes on
propertyas they all apply tosettingas well.
- class labthings_fastapi.DataProperty(default: Value, *, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None)
- class labthings_fastapi.DataProperty(*, default_factory: Callable[[], Value], readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
BaseProperty[Owner,Value],Generic[Owner,Value]A Property descriptor that acts like a regular variable.
DataPropertydescriptors remember their value, and can be read and written to like a regular Python variable.Create a property that acts like a regular variable.
DataPropertydescriptors function just like variables, in that they can be read and written to as attributes of theThingand their value will be the same every time it is read (i.e. it changes only when it is set). This differs fromFunctionalPropertywhich uses a “getter” function just likebuiltins.propertyand may return a different value each time.DataPropertyinstances may always be set, when they are accessed as an attribute of theThinginstance. Thereadonlyparameter applies only to client code, whether it is remote or aDirectThingClientwrapper.The type of the property’s value will be inferred either from the type subscript or from an annotation on the class attribute. This is done in
__get_name__because neither is available during__init__.- Parameters:
default – the default value. This or
default_factorymust be provided. Note that, asNoneis a valid default value, this uses...instead as a way of checking whetherdefaulthas been set.default_factory – a function that returns the default value. This is appropriate for datatypes such as lists, where using a mutable default value can lead to odd behaviour.
readonly – if
True, the property may not be written to via HTTP, or viaDirectThingClientobjects, i.e. it may only be set as an attribute of theThingand not from a client.constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details.
- _default_factory
- readonly = False
- instance_get(obj: Owner) Value
Return the property’s value.
This will supply a default if the property has not yet been set.
- Parameters:
obj – The
Thingon which the property is being accessed.- Returns:
the value of the property.
- __set__(obj: Owner, value: Value, emit_changed_event: bool = True) None
Set the property’s value.
This sets the property’s value, and notifies any observers.
- Parameters:
obj – the
Thingto which we are attached.value – the new value for the property.
emit_changed_event – whether to emit a changed event.
- _observers_set(obj: labthings_fastapi.thing.Thing) weakref.WeakSet
Return the observers of this property.
Each observer in this set will be notified when the property is changed. See
.DataProperty.emit_changed_event- Parameters:
obj – the
Thingto which we are attached.- Returns:
the set of observers corresponding to
obj.
- emit_changed_event(obj: labthings_fastapi.thing.Thing, value: Value) None
Notify subscribers that the property has changed.
This function is run when properties are updated. It must be run from within a thread. This could be the
Invocationthread of a running action, or the property should be updated over via a client/http. It must be run from a thread as it is communicating with the event loop via anasyncioblocking portal and can cause deadlock if run in the event loop.This method will raise a
ServerNotRunningErrorif the event loop is not running, and should only be called after the server has started.- Parameters:
obj – the
Thingto which we are attached.value – the new property value, to be sent to observers.
- async emit_changed_event_async(obj: labthings_fastapi.thing.Thing, value: Value) None
Notify subscribers that the property has changed.
This function may only be run in the
anyioevent loop. SeeDataProperty.emit_changed_event.- Parameters:
obj – the
Thingto which we are attached.value – the new property value, to be sent to observers.
- class labthings_fastapi.DataSetting(default: Value, *, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None)
- class labthings_fastapi.DataSetting(*, default_factory: Callable[[], Value], readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None)
Bases:
DataProperty[Owner,Value],BaseSetting[Owner,Value],Generic[Owner,Value]A
DataPropertythat persists on disk.A setting can be accessed via the HTTP API and is persistent between sessions.
A
DataSettingis aDataPropertywith extra functionality for triggering aThingto save its settings.Note: If a setting is mutated rather than assigned to, this will not trigger saving. For example: if a Thing has a setting called
dictsettingholding the dictionary{"a": 1, "b": 2}thenself.dictsetting = {"a": 2, "b": 2}would trigger saving butself.dictsetting[a] = 2would not, as the setter fordictsettingis never called.The setting otherwise acts just like a normal variable.
Create a property that acts like a regular variable.
DataPropertydescriptors function just like variables, in that they can be read and written to as attributes of theThingand their value will be the same every time it is read (i.e. it changes only when it is set). This differs fromFunctionalPropertywhich uses a “getter” function just likebuiltins.propertyand may return a different value each time.DataPropertyinstances may always be set, when they are accessed as an attribute of theThinginstance. Thereadonlyparameter applies only to client code, whether it is remote or aDirectThingClientwrapper.The type of the property’s value will be inferred either from the type subscript or from an annotation on the class attribute. This is done in
__get_name__because neither is available during__init__.- Parameters:
default – the default value. This or
default_factorymust be provided. Note that, asNoneis a valid default value, this uses...instead as a way of checking whetherdefaulthas been set.default_factory – a function that returns the default value. This is appropriate for datatypes such as lists, where using a mutable default value can lead to odd behaviour.
readonly – if
True, the property may not be written to via HTTP, or viaDirectThingClientobjects, i.e. it may only be set as an attribute of theThingand not from a client.constraints – is passed as keyword arguments to
pydantic.Fieldto add validation constraints to the property. Seepydantic.Fieldfor details.
- __set__(obj: Owner, value: Value, emit_changed_event: bool = True) None
Set the setting’s value.
This will cause the settings to be saved to disk.
- Parameters:
obj – the
Thingto which we are attached.value – the new value of the setting.
emit_changed_event – whether to emit a changed event.
- set_without_emit(obj: Owner, value: Value) None
Set the property’s value, but do not emit event to notify the server.
This function is not expected to be used externally. It is called during initial setup so that the setting can be set from disk before the server is fully started.
- Parameters:
obj – the
Thingto which we are attached.value – the new value of the setting.
- labthings_fastapi.action(func: Callable[Concatenate[OwnerT, ActionParams], ActionReturn], **kwargs: Any) ActionDescriptor[ActionParams, ActionReturn, OwnerT]
- labthings_fastapi.action(**kwargs: Any) Callable[[Callable[Concatenate[OwnerT, ActionParams], ActionReturn]], ActionDescriptor[ActionParams, ActionReturn, OwnerT]]
Mark a method of a
Thingas a LabThings Action.Methods decorated with
@actionwill be available to call over HTTP as actions. See Actions for an introduction to the concept of actions.This decorator may be used with or without arguments.
- Parameters:
func – The method to be decorated as an action.
**kwargs – Keyword arguments are passed to the constructor of
ActionDescriptor.
- Returns:
Whether used with or without arguments, the result is that the method is wrapped in an
ActionDescriptor, so it can be called as usual, but will also be exposed over HTTP.
- labthings_fastapi.endpoint(method: HTTPMethod, path: str | None = None, **kwargs: Any) Callable[[Callable], EndpointDescriptor]
Mark a function as a FastAPI endpoint without making it an action.
This decorator will cause a method of a
Thingto be directly added to the HTTP API, bypassing the machinery underlying Action and Property affordances. Such endpoints will not be documented in the Thing Description but may be used as the target of links. For example, this could allow a file to be downloaded from theThingat a known URL, or serve a video stream that wouldn’t be supported as aBlob.The majority of
Thingimplementations won’t need this decorator, but it is here to enable flexibility when it’s needed.This decorator always takes arguments; in particular,
methodis required. It should be used as:class DownloadThing(Thing): @endpoint("get") def plain_text_response(self) -> str: return "example string"
This decorator is intended to work very similarly to the
fastapidecorators@app.get,@app.post, etc., with two changes:- The path is relative to the host
Thingand will default to the name of the method.
- The path is relative to the host
- The method will be called with the host
Thingas its first argument, i.e. it will be bound to the class as usua.
- The method will be called with the host
- Parameters:
method – The HTTP verb this endpoint responds to.
path – The path, relative to the host
Thingbase URL.**kwargs – Additional keyword arguments are passed to the
fastapi.FastAPI.getdecorator ifmethodisget, or to the equivalent decorator for other HTTP verbs.
- Returns:
When used as intended, the result is an
EndpointDescriptor.
- class labthings_fastapi.ThingServer(things: config_model.ThingsConfig, settings_folder: str | None = None, application_config: collections.abc.Mapping[str, Any] | None = None, debug: bool = False)
Use FastAPI to serve
Thinginstances.The
ThingServersets up afastapi.FastAPIapplication and uses it to expose the capabilities ofThinginstances over HTTP.There are several functions of a
ThingServer:Manage where settings are stored, to allow
Thinginstances to load and save their settings from disk.Configure the server to allow cross-origin requests (required if we use a web app that is not served from the
ThingServer).Manage the threads used to run Actions.
Manage Blob input/output to allow binary data to be returned.
Allow threaded code to call functions in the event loop, by providing an
anyio.from_thread.BlockingPortal.
Initialise a LabThings server.
Setting up the
ThingServerinvolves creating the underlyingfastapi.FastAPIapp, setting its lifespan function (used to set up and shut down theThinginstances), and configuring it to allow cross-origin requests.We also create the
ActionManagerto manage Actions and theBlobManagerto manage the downloading of Blob input/output.- Parameters:
things – A mapping of Thing names to
Thingsubclasses, orThingConfigobjects specifying the subclass, its initialisation arguments, and any connections to otherThings.settings_folder – the location on disk where
Thingsettings will be saved.application_config – A mapping containing custom configuration for the application. This is not processed by LabThings. Each
Thingcan access application. This is not processed by LabThings. EachThingcan access this via the Thing-Server interface.debug – If
True, set the log level forThinginstances to DEBUG.
- _config
- app
- settings_folder = './settings'
- action_manager
- blocking_portal: anyio.from_thread.BlockingPortal | None = None
- _things
- classmethod from_config(config: config_model.ThingServerConfig, debug: bool = False) typing_extensions.Self
Create a ThingServer from a configuration model.
This is equivalent to
ThingServer(**dict(config)).- Parameters:
config – The configuration parameters for the server.
debug – If
True, set the log level forThinginstances to DEBUG.
- Returns:
A
ThingServerconfigured as per the model.
- _set_cors_middleware() None
Configure the server to allow requests from other origins.
This is required to allow web applications access to the HTTP API, if they are not served from the same origin (i.e. if they are not served as part of the
ThingServer.).This is usually needed during development, and may be needed at other times depending on how you are using LabThings.
- _set_url_for_middleware() None
Add middleware to support
url_forin Pydantic models.This middleware adds a request state variable that allows
labthings_fastapi.server.URLForinstances to be serialised using FastAPI’surl_forfunction.
- property things: collections.abc.Mapping[str, labthings_fastapi.thing.Thing]
Return a dictionary of all the things.
- Returns:
a dictionary mapping thing paths to
Thinginstances.
- property application_config: collections.abc.Mapping[str, Any] | None
Return the application configuration from the config file.
- Returns:
The custom configuration as specified in the configuration file.
- ThingInstance
- things_by_class(cls: type[ThingInstance]) collections.abc.Sequence[ThingInstance]
Return all Things attached to this server matching a class.
Return all instances of
clsattached to this server.- Parameters:
cls – A
Thingsubclass.- Returns:
all instances of
clsthat have been added to this server.
- thing_by_class(cls: type[ThingInstance]) ThingInstance
Return the instance of
clsattached to this server.This function calls
ThingServer.things_by_class, but asserts that there is exactly one match.- Parameters:
cls – a
Thingsubclass.- Returns:
the instance of
clsattached to this server.- Raises:
RuntimeError – if there is not exactly one matching Thing.
- path_for_thing(name: str) str
Return the path for a thing with the given name.
- Parameters:
name – The name of the thing.
- Returns:
The path at which the thing is served.
- Raises:
KeyError – if no thing with the given name has been added.
- _create_things() collections.abc.Mapping[str, labthings_fastapi.thing.Thing]
Create the Things, add them to the server, and connect them up if needed.
This method is responsible for creating instances of
Thingsubclasses and adding them to the server. It also ensures theThings are connected together if required.The Things are defined in
self._config.thing_configswhich in turn is generated from thethingsargument to__init__.
- _connect_things() None
Connect the
thing_slotattributes of Things.A
Thingmay have attributes defined aslt.thing_slot(), which will be populated after allThinginstances are loaded on the server.This function is responsible for supplying the
Thinginstances required for each connection. This will be done by using the name specified either in the connection’s default, or in the configuration of the server.ThingSlotErrorwill be raised by code called by this method if the connection cannot be provided. SeeThingSlot.connectfor more details.
- _attach_things_to_server() None
Add the Things to the FastAPI App.
This calls
Thing.attach_to_serveron eachThingthat is a part of thisThingServerin order to add the HTTP endpoints and load settings.
- async lifespan(app: fastapi.FastAPI) AsyncGenerator[None, None]
Manage set up and tear down of the server and Things.
This method is used as a lifespan function for the FastAPI app. See the lifespan page in FastAPI’s documentation.
This does two important things:
It sets up the blocking portal so background threads can run async code (this is required for events, streams, etc.).
It runs setup/teardown code for Things by calling them as context managers.
- Parameters:
app – The FastAPI application wrapped by the server.
- Yield:
no value. The FastAPI application will serve requests while this function yields.
- Raises:
BaseException – Reraises any errors that are caught when calling
__enter__on each Thing. The error is also saved toself.startup_failurefor post mortem, as otherwise uvicorn will swallow it and replace it with SystemExit(3) and no traceback.
- class labthings_fastapi.ThingConfig(/, **data: Any)
Bases:
pydantic.BaseModelThe information needed to add a
Thingto aThingServer.Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.- cls: ThingImportString = None
- args: collections.abc.Sequence[Any] = None
- kwargs: collections.abc.Mapping[str, Any] = None
- thing_slots: collections.abc.Mapping[str, str | collections.abc.Iterable[str] | None] = None
- class labthings_fastapi.ThingServerConfig(/, **data: Any)
Bases:
pydantic.BaseModelThe configuration parameters for a
ThingServer.Create a new model by parsing and validating input data from keyword arguments.
Raises [
ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.selfis explicitly positional-only to allowselfas a field name.- things: ThingsConfig = None
- classmethod check_things(things: ThingsConfig) ThingsConfig
Check that the thing configurations can be normalised.
It’s possible to specify the things as a mapping from names to classes. We use
pydantic.ImportStringas the type of the classes: this takes a string, and imports the corresponding Python object. When loading config from JSON, this does the right thing - but when loading from Python objects it will accept any Python object.This validator runs
normalise_thing_configto check each value is either a validThingConfigor a type or a mapping. If it’s a mapping, we will attempt to make aThingConfigfrom it. If it’s atypewe will create aThingConfigusing that type as the class. We don’t check forThingsubclasses in this module to avoid a dependency loop.- Parameters:
things – The validated value of the field.
- Returns:
A copy of the input, with all values converted to
ThingConfiginstances.
- property thing_configs: collections.abc.Mapping[ThingName, ThingConfig]
A copy of the
thingsfield where every value is a.ThingConfig.The field validator on
thingsalready ensures it returns a mapping, but it’s not typed strictly, to allow Things to be specified with just a class.This property returns the list of
ThingConfigobjects, and is typed strictly.
- class labthings_fastapi.ThingClient(base_url: str, client: httpx.Client | None = None)
A client for a LabThings-FastAPI Thing.
Note
ThingClient must be subclassed to add actions/properties, so this class will be minimally useful on its own.
The best way to get a client for a particular Thing is currently
ThingClient.from_url, which dynamically creates a subclass with the right attributes.Create a ThingClient connected to a remote Thing.
- Parameters:
base_url – the base URL of the Thing. This should be the URL of the Thing Description document.
client – an optional
httpx.Clientobject to use for all HTTP requests. This may be afastapi.TestClientobject for testing purposes.
- server
- path
- client
- get_property(path: str) Any
Make a GET request to retrieve the value of a property.
- Parameters:
path – the URI of the
getpropertyendpoint, relative to thebase_url.- Returns:
the property’s value, as deserialised from JSON.
- Raises:
ClientPropertyError – is raised the property cannot be read.
- set_property(path: str, value: Any) None
Make a PUT request to set the value of a property.
- Parameters:
path – the URI of the
getpropertyendpoint, relative to thebase_url.value – the property’s value. Currently this must be serialisable to JSON.
- Raises:
ClientPropertyError – is raised the property cannot be set.
- invoke_action(path: str, **kwargs: Any) Any
Invoke an action on the Thing.
This method will make the initial POST request to invoke an action, then poll the resulting invocation until it completes. If successful, the action’s output will be returned directly.
While the action is running, log messages will be re-logged locally. If you have enabled logging to the console, these should be visible.
- Parameters:
path – the URI of the
invokeactionendpoint, relative to thebase_url**kwargs – Additional arguments will be combined into the JSON body of the
POSTrequest and sent as input to the action. These will be validated on the server.
- Returns:
the output value of the action.
- Raises:
FailedToInvokeActionError – if the action fails to start.
ServerActionError – is raised if the action does not complete successfully.
- follow_link(response: dict, rel: str) httpx.Response
Follow a link in a response object, by its
relattribute.- Parameters:
response – is the dictionary returned by e.g.
poll_invocation.rel – picks the link to follow by matching its
relitem.
- Returns:
the response to making a
GETrequest to the link.
- classmethod from_url(thing_url: str, client: httpx.Client | None = None) typing_extensions.Self
Create a ThingClient from a URL.
This will dynamically create a subclass with properties and actions, and return an instance of that subclass pointing at the Thing URL.
- Parameters:
thing_url – The base URL of the Thing, which should also be the URL of its Thing Description.
client – is an optional
httpx.Clientobject. If not present, one will be created. This is particularly useful if you need to set HTTP options, or if you want to work with a local server object for testing purposes (seefastapi.TestClient).
- Returns:
a
ThingClientsubclass with properties and methods that match the retrieved Thing Description (see Thing).
- classmethod subclass_from_td(thing_description: dict) type[typing_extensions.Self]
Create a ThingClient subclass from a Thing Description.
Dynamically subclass
ThingClientto add properties and methods for each property and action in the Thing Description.- Parameters:
thing_description – A Thing Description as a dictionary, which will be used to construct the class.
- Returns:
a
ThingClientsubclass with the right properties and methods.
- labthings_fastapi.cancellable_sleep(interval: float) None
Sleep for a specified time, allowing cancellation.
This function should be called from action functions instead of
time.sleepto allow them to be cancelled. Usually, this function is equivalent totime.sleep(it waits the specified number of seconds). If the action is cancelled during the sleep, it will raise anInvocationCancelledErrorto signal that the action should finish.Warning
This function uses
Event.waitinternally, which suffers from timing errors on some platforms: it may have error of around 10-20ms. If that’s a problem, consider usingtime.sleepinstead.lt.raise_if_cancelled()may then be used to allow cancellation.If this function is called from outside of an action thread, it will revert to
time.sleep.- Parameters:
interval – The length of time to wait for, in seconds.
- labthings_fastapi.raise_if_cancelled() None
Raise an exception if the current invocation has been cancelled.
This function checks for cancellation events and, if the current action invocation has been cancelled, it will raise an
InvocationCancelledErrorto signal the thread to terminate. It is equivalent tocancellable_sleepbut without waiting any time.If called outside of an invocation context, this function does nothing, and will not raise an error.
- class labthings_fastapi.ThreadWithInvocationID(target: Callable, args: collections.abc.Sequence[Any] | None = None, kwargs: collections.abc.Mapping[str, Any] | None = None, *super_args: Any, **super_kwargs: Any)
Bases:
threading.ThreadA thread that sets a new invocation ID.
This is a subclass of
threading.Threadand works very much the same way. It implements its functionality by overriding therunmethod, so this should not be overridden again - you should instead specify the code to run using thetargetargument.This function enables an action to be run in a thread, which gets its own invocation ID and cancel hook. This means logs will not be interleaved with the calling action, and the thread may be cancelled just like an action started over HTTP, by calling its
cancelmethod.The thread also remembers the return value of the target function in the property
resultand stores any exception raised in theexceptionproperty.A final LabThings-specific feature is cancellation propagation. If the thread is started from an action that may be cancelled, it may be joined with
join_and_propagate_cancel. This is intended to be equivalent to callingjoinbut with the added feature that, if the parent thread is cancelled while waiting for the child thread to join, the child thread will also be cancelled.Initialise a thread with invocation ID.
- Parameters:
target – the function to call in the thread.
args – positional arguments to
target.kwargs – keyword arguments to
target.*super_args – arguments passed to
threading.Thread.**super_kwargs – keyword arguments passed to
threading.Thread.
- _target
- _args = []
- _kwargs
- _result: Any = None
- _exception: BaseException | None = None
- _cancel_event
- property result: Any
The return value of the target function.
- property exception: BaseException | None
The exception raised by the target function, or None.
- join_and_propagate_cancel(poll_interval: float = 0.2) None
Wait for the thread to finish, and propagate cancellation.
This function wraps
threading.Thread.joinbut periodically checks if the calling thread has been cancelled. If it has, it will cancel the thread, before attempting tojoinit again.Note that, if the invocation that calls this function is cancelled while the function is running, the exception will propagate, i.e. you should handle
InvocationCancelledErrorunless you wish your invocation to terminate if it is cancelled.- Parameters:
poll_interval – How often to check for cancellation of the calling thread, in seconds.
- Raises:
InvocationCancelledError – if this invocation is cancelled while waiting for the thread to join.