labthings_fastapi ================= .. py:module:: labthings_fastapi .. autoapi-nested-parse:: LabThings-FastAPI. This is the top level module for LabThings-FastAPI, a library for building :ref:`wot_cc` devices using Python. There is documentation on readthedocs_, and the recommended place to start is :doc:`index`\ . .. _readthedocs: https://labthings-fastapi.readthedocs.io/ This module contains a number of convenience imports and is intended to be imported using: .. code-block:: python 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 ---------- .. toctree:: :maxdepth: 1 /autoapi/labthings_fastapi/actions/index /autoapi/labthings_fastapi/base_descriptor/index /autoapi/labthings_fastapi/client/index /autoapi/labthings_fastapi/dependencies/index /autoapi/labthings_fastapi/deps/index /autoapi/labthings_fastapi/endpoints/index /autoapi/labthings_fastapi/example_things/index /autoapi/labthings_fastapi/exceptions/index /autoapi/labthings_fastapi/invocation_contexts/index /autoapi/labthings_fastapi/invocations/index /autoapi/labthings_fastapi/logs/index /autoapi/labthings_fastapi/notifications/index /autoapi/labthings_fastapi/outputs/index /autoapi/labthings_fastapi/properties/index /autoapi/labthings_fastapi/server/index /autoapi/labthings_fastapi/testing/index /autoapi/labthings_fastapi/thing/index /autoapi/labthings_fastapi/thing_description/index /autoapi/labthings_fastapi/thing_server_interface/index /autoapi/labthings_fastapi/thing_slots/index /autoapi/labthings_fastapi/types/index /autoapi/labthings_fastapi/utilities/index /autoapi/labthings_fastapi/websockets/index Classes ------- .. autoapisummary:: labthings_fastapi.Thing labthings_fastapi.ThingServerInterface labthings_fastapi.DataProperty labthings_fastapi.DataSetting labthings_fastapi.ThingServer labthings_fastapi.ThingConfig labthings_fastapi.ThingServerConfig labthings_fastapi.ThingClient labthings_fastapi.ThreadWithInvocationID Functions --------- .. autoapisummary:: labthings_fastapi.thing_slot labthings_fastapi.property labthings_fastapi.setting labthings_fastapi.action labthings_fastapi.endpoint labthings_fastapi.cancellable_sleep labthings_fastapi.raise_if_cancelled Package Contents ---------------- .. py:class:: 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 call ``super().__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 be ``None`` except after errors. You should be safe to ignore them, and just include code that will close down your hardware, which is equivalent to a ``finally:`` block. * Properties and Actions are defined using decorators: the :deco:`.action` decorator declares a method to be an action, which will run when it's triggered, and the :deco:`.property` decorator 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. * `title` will 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 `.Thing` is 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 that `.Thing` instances are usually created by a `.ThingServer` and not instantiated directly: if you do make a `.Thing` directly, you will need to supply a `.ThingServerInterface` that is connected to a `.ThingServer` or a suitable mock object. :param thing_server_interface: The interface to the server that is hosting this Thing. It will be supplied when the `.Thing` is instantiated by the `.ThingServer` or by `.create_thing_without_server` which generates a mock interface. .. py:attribute:: title :type: str A human-readable description of the Thing .. py:attribute:: _thing_server_interface :type: labthings_fastapi.thing_server_interface.ThingServerInterface Provide access to features of the server that this `.Thing` is attached to. .. py:attribute:: _disable_saving_settings :type: bool :value: False .. py:property:: path :type: str The path at which the `.Thing` is exposed over HTTP. .. py:property:: name :type: str The name of this Thing, as known to the server. .. py:property:: logger :type: logging.Logger A logger, named after this Thing. .. py:method:: __aenter__() -> typing_extensions.Self :async: 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. :return: this object. .. py:method:: __aexit__(exc_t: Any | None, exc_v: Any | None, exc_tb: Any) -> None :async: Wrap context management functions, if they exist. See ``__aenter__`` for more details. :param exc_t: The type of the exception, or ``None``. :param exc_v: The exception that occurred, or ``None``. :param exc_tb: The traceback for the exception, or ``None``. .. py:method:: 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. :param server: The server to attach this Thing to. Attaching the `.Thing` to a `.ThingServer` allows the `.Thing` to 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 :ref:`wot_affordances` on the `.Thing`, as well as any `.EndpointDescriptor` descriptors. .. py:attribute:: _settings_store :type: Optional[dict[str, labthings_fastapi.properties.BaseSetting]] :value: None .. py:property:: _settings :type: dict[str, labthings_fastapi.properties.BaseSetting] A private property that returns a dict of all settings for this Thing. Each dict key is the name of the setting, the corresponding value is the BaseSetting class (a descriptor). This can be used to directly get the descriptor so that the value can be set without emitting signals, such as on startup. .. py:method:: 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__``. .. py:method:: 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. .. py:attribute:: _labthings_thing_state :type: Optional[dict] :value: None .. py:property:: thing_state :type: 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. .. py:method:: validate_thing_description() -> None Raise an exception if the thing description is not valid. .. py:attribute:: _cached_thing_description :type: Optional[tuple[Optional[str], Optional[str], labthings_fastapi.thing_description._model.ThingDescription]] :value: None .. py:method:: thing_description(path: Optional[str] = None, base: Optional[str] = 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 :ref:`wot_td` for this Thing. :param path: the URL pointing to this Thing. :param base: the base URL for all URLs in the thing description. :return: a Thing Description. .. py:method:: thing_description_dict(path: Optional[str] = None, base: Optional[str] = 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. :param path: the URL pointing to this Thing. :param base: the base URL for all URLs in the thing description. :return: a Thing Description. .. py:method:: observe_property(property_name: str, stream: anyio.abc.ObjectSendStream) -> None Register a stream to receive property change notifications. :param property_name: the property to register for. :param stream: the stream used to send events. :raise KeyError: if the requested name is not defined on this Thing. :raise PropertyNotObservableError: if the property is not observable. .. py:method:: observe_action(action_name: str, stream: anyio.abc.ObjectSendStream) -> None Register a stream to receive action status change notifications. :param action_name: the action to register for. :param stream: the stream used to send events. :raise KeyError: if the requested name is not defined on this Thing. .. py:function:: thing_slot(default: str | collections.abc.Iterable[str] | None | types.EllipsisType = ...) -> Any Declare a connection to another `.Thing` in the same server. ``lt.thing_slot`` marks a class attribute as a connection to another `.Thing` on the same server. This will be automatically supplied when the server is started, based on the type hint and default value. In keeping with `.property` and `.setting`, the type of the attribute should be the type of the connected `.Thing`\ . For example: .. code-block:: python 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 `.ThingSlot` descriptor 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 as `mypy` or `pyright` (see note below). The type hint of a Thing Connection should be one of the following: * A `.Thing` subclass. An instance of this subclass will be returned when the attribute is accessed. * An optional `.Thing` subclass (e.g. ``MyThing | None``). This will either return a ``MyThing`` instance or ``None``\ . * A mapping of `str` to `.Thing` (e.g. ``Mapping[str, MyThing]``). This will return a mapping of `.Thing` names to `.Thing` instances. The mapping may be empty. Example: .. code-block:: python 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 `.Thing` that matches the specified type when the server is started. If no matching `.Thing` instances are found, the descriptor will return ``None`` or 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 `.Thing` name, or a sequence of strings (for connections that return mappings). In those cases, the relevant `.Thing` will be returned from the server. If a name is given that either doesn't correspond to a `.Thing` on the server, or is a `.Thing` that doesn't match the type of this connection, the server will fail to start with an error. The default may also be ``None`` which is appropriate when the type is optional or a mapping. If the type is a `.Thing` subclass, a default value of ``None`` forces the connection to be specified in configuration. :param 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 matching `.Thing` instance (or instances). A default value of `None` is allowed if the connection is type hinted as optional. :return: A `.ThingSlot` descriptor. Typing notes: In the example above, using `.ThingSlot` directly would assign an object with type ``ThingSlot[ThingA]`` to the attribute ``thing_a``, which is typed as ``ThingA``\ . This would cause a type error. Using `.thing_slot` suppresses this error, as its return type is a`Any``\ . The use of ``Any`` or 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 as `pydantic`\ . .. py:class:: ThingServerInterface(server: labthings_fastapi.server.ThingServer, name: str) An interface for Things to interact with their server. This is added to every `.Thing` during ``__init__`` and is available as ``self._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. :param server: the `.ThingServer` instance we're connected to. This will be retained as a weak reference. :param name: the name of the `.Thing` instance this interface is provided for. .. py:attribute:: _name :type: str .. py:attribute:: _server :type: weakref.ReferenceType[labthings_fastapi.server.ThingServer] .. py:method:: _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 `ThingServer` is no longer available. .. py:method:: 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_soon` to 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 a `concurrent.futures.Future` object that may resolve to the async function's return value. :param async_function: the asynchronous function to call. :param \*args: positional arguments to be provided to the function. :returns: an `asyncio.Future` object wrapping the return value. :raises ServerNotRunningError: if the server is not running (i.e. there is no event loop). .. py:method:: 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.call` to 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. :param async_function: the asynchronous function to call. :param \*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). .. py:property:: settings_folder :type: str The path to a folder where persistent files may be saved. .. py:property:: settings_file_path :type: str The path where settings should be loaded and saved as JSON. .. py:property:: name :type: str The name of the Thing attached to this interface. .. py:property:: path :type: 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. .. py:method:: get_thing_states() -> Mapping[str, Any] Retrieve metadata from all Things on the server. This function will retrieve the `.Thing.thing_state` property from each `.Thing` on 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. :return: a dictionary of metadata, with the `.Thing` names as keys. .. py:property:: _action_manager :type: 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. .. py:function:: property(getter: Callable[[Any], Value]) -> FunctionalProperty[Value] property(*, default: Value, readonly: bool = False, **constraints: Any) -> Value property(*, default_factory: Callable[[], Value], readonly: bool = False, **constraints: Any) -> Value Define a Property on a `.Thing`\ . This function may be used to define :ref:`properties` in two ways, as either a decorator or a field specifier. See the examples in the :mod:`.property` documentation. 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`. :param getter: is a method of a class that returns the value of this property. This is usually supplied by using ``property`` as a decorator. :param default: is the default value. Either this, ``getter`` or ``default_factory`` must be specified. Specifying both or neither will raise an exception. :param default_factory: should return your default value. This may be used as an alternative to ``default`` if you need to use a mutable datatype. For example, it would be better to specify ``default_factory=list`` than ``default=[]`` because the second form would be shared between all `.Thing`\ s with this property. :param readonly: whether the property should be read-only via the `.ThingClient` interface (i.e. over HTTP or via a `.DirectThingClient`). This is automatically true if ``property`` is used as a decorator and no setter is specified. :param \**constraints: additional keyword arguments are passed to `pydantic.Field` and allow constraints to be added to the property. For example, ``ge=0`` constrains a numeric property to be non-negative. See `pydantic.Field` for the full range of constraint arguments. :return: a property descriptor, either a `.FunctionalProperty` if used as a decorator, or a `.DataProperty` if used as a field. :raises MissingDefaultError: if no valid default value is supplied, and a getter is not in use. :raises OverspecifiedDefaultError: if the default is specified more than once (e.g. ``default``, ``default_factory``, or ``getter``). **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 ``default`` performs 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 means ``property`` does not know its type hint until after it's been called. When used as a field specifier, ``property`` returns a generic `.DataProperty` descriptor instance, which will determine its type when it is attached to the `.Thing`. The type hint on the return value of ``property`` in that situation is a "white lie": we annotate the return as having the same type as the ``default`` value (or the ``default_factory`` return value). This means that type checkers such as ``mypy`` will check that the default is valid for the type of the field, and won't raise an error about assigning, for example, an instance of ``DataProperty[int]`` to a field annotated as ``int``. Finally, the type of the ``default`` argument includes `.EllipsisType` so that we can use ``...`` as its default value. This allows us to distinguish between ``default`` not being set (``...``) and a desired default value of ``None``. Similarly, ``...`` is the default value for ``getter`` so we can raise a more helpful error if a non-callable value is passed as the first argument. .. py:function:: setting(getter: Callable[[Any], Value]) -> FunctionalSetting[Value] setting(*, default: Value, readonly: bool = False, **constraints: Any) -> Value 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 :deco:`.property`. Every ``setting`` on a `.Thing` will 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`_. .. _`#159`: https://github.com/labthings/labthings-fastapi/issues/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 ``dictsetting`` holding the dictionary ``{"a": 1, "b": 2}`` then ``self.dictsetting = {"a": 2, "b": 2}`` would trigger saving but ``self.dictsetting[a] = 2`` would not, as the setter for ``dictsetting`` is never called. :param getter: is a method of a class that returns the value of this property. This is usually supplied by using ``property`` as a decorator. :param default: is the default value. Either this, ``getter`` or ``default_factory`` must be specified. Specifying both or neither will raise an exception. :param default_factory: should return your default value. This may be used as an alternative to ``default`` if you need to use a mutable datatype. For example, it would be better to specify ``default_factory=list`` than ``default=[]`` because the second form would be shared between all `.Thing`\ s with this setting. :param readonly: whether the setting should be read-only via the `.ThingClient` interface (i.e. over HTTP or via a `.DirectThingClient`). :param \**constraints: additional keyword arguments are passed to `pydantic.Field` and allow constraints to be added to the property. For example, ``ge=0`` constrains a numeric property to be non-negative. See `pydantic.Field` for the full range of constraint arguments. :return: a setting descriptor. :raises MissingDefaultError: if no valid default or getter is supplied. :raises OverspecifiedDefaultError: if the default is specified more than once (e.g. ``default``, ``default_factory``, or ``getter``). **Typing Notes** See the typing notes on `.property` as they all apply to `.setting` as well. .. py:class:: DataProperty(default: Value, *, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None) DataProperty(*, default_factory: ValueFactory, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`BaseProperty`\ [\ :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Value`\ ] A Property descriptor that acts like a regular variable. `.DataProperty` descriptors remember their value, and can be read and written to like a regular Python variable. Create a property that acts like a regular variable. `.DataProperty` descriptors function just like variables, in that they can be read and written to as attributes of the `.Thing` and their value will be the same every time it is read (i.e. it changes only when it is set). This differs from `.FunctionalProperty` which uses a "getter" function just like `builtins.property` and may return a different value each time. `.DataProperty` instances may always be set, when they are accessed as an attribute of the `.Thing` instance. The ``readonly`` parameter applies only to client code, whether it is remote or a `.DirectThingClient` wrapper. 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__``. :param default: the default value. This or ``default_factory`` must be provided. Note that, as ``None`` is a valid default value, this uses ``...`` instead as a way of checking whether ``default`` has been set. :param 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. :param readonly: if ``True``, the property may not be written to via HTTP, or via `.DirectThingClient` objects, i.e. it may only be set as an attribute of the `.Thing` and not from a client. :param constraints: is passed as keyword arguments to `pydantic.Field` to add validation constraints to the property. See `pydantic.Field` for details. .. py:attribute:: _default_factory :value: None .. py:attribute:: readonly :value: False .. py:method:: instance_get(obj: labthings_fastapi.thing.Thing) -> Value Return the property's value. This will supply a default if the property has not yet been set. :param obj: The `.Thing` on which the property is being accessed. :return: the value of the property. .. py:method:: __set__(obj: labthings_fastapi.thing.Thing, value: Value, emit_changed_event: bool = True) -> None Set the property's value. This sets the property's value, and notifies any observers. :param obj: the `.Thing` to which we are attached. :param value: the new value for the property. :param emit_changed_event: whether to emit a changed event. .. py:method:: _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`` :param obj: the `.Thing` to which we are attached. :return: the set of observers corresponding to ``obj``. .. py:method:: 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 `Invocation` thread 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 an `asyncio` blocking portal and can cause deadlock if run in the event loop. This method will raise a `.ServerNotRunningError` if the event loop is not running, and should only be called after the server has started. :param obj: the `.Thing` to which we are attached. :param value: the new property value, to be sent to observers. .. py:method:: emit_changed_event_async(obj: labthings_fastapi.thing.Thing, value: Value) -> None :async: Notify subscribers that the property has changed. This function may only be run in the `anyio` event loop. See `.DataProperty.emit_changed_event`. :param obj: the `.Thing` to which we are attached. :param value: the new property value, to be sent to observers. .. py:class:: DataSetting(default: Value, *, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None) DataSetting(*, default_factory: ValueFactory, readonly: bool = False, constraints: collections.abc.Mapping[str, Any] | None = None) Bases: :py:obj:`DataProperty`\ [\ :py:obj:`Value`\ ], :py:obj:`BaseSetting`\ [\ :py:obj:`Value`\ ], :py:obj:`Generic`\ [\ :py:obj:`Value`\ ] A `.DataProperty` that persists on disk. A setting can be accessed via the HTTP API and is persistent between sessions. A `.DataSetting` is a `.DataProperty` with extra functionality for triggering a `.Thing` to 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 `dictsetting` holding the dictionary `{"a": 1, "b": 2}` then `self.dictsetting = {"a": 2, "b": 2}` would trigger saving but `self.dictsetting[a] = 2` would not, as the setter for `dictsetting` is never called. The setting otherwise acts just like a normal variable. Create a property that acts like a regular variable. `.DataProperty` descriptors function just like variables, in that they can be read and written to as attributes of the `.Thing` and their value will be the same every time it is read (i.e. it changes only when it is set). This differs from `.FunctionalProperty` which uses a "getter" function just like `builtins.property` and may return a different value each time. `.DataProperty` instances may always be set, when they are accessed as an attribute of the `.Thing` instance. The ``readonly`` parameter applies only to client code, whether it is remote or a `.DirectThingClient` wrapper. 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__``. :param default: the default value. This or ``default_factory`` must be provided. Note that, as ``None`` is a valid default value, this uses ``...`` instead as a way of checking whether ``default`` has been set. :param 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. :param readonly: if ``True``, the property may not be written to via HTTP, or via `.DirectThingClient` objects, i.e. it may only be set as an attribute of the `.Thing` and not from a client. :param constraints: is passed as keyword arguments to `pydantic.Field` to add validation constraints to the property. See `pydantic.Field` for details. .. py:method:: __set__(obj: labthings_fastapi.thing.Thing, value: Value, emit_changed_event: bool = True) -> None Set the setting's value. This will cause the settings to be saved to disk. :param obj: the `.Thing` to which we are attached. :param value: the new value of the setting. :param emit_changed_event: whether to emit a changed event. .. py:method:: set_without_emit(obj: labthings_fastapi.thing.Thing, 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. :param obj: the `.Thing` to which we are attached. :param value: the new value of the setting. .. py:function:: action(func: Callable[Concatenate[OwnerT, ActionParams], ActionReturn], **kwargs: Any) -> ActionDescriptor[ActionParams, ActionReturn, OwnerT] action(**kwargs: Any) -> Callable[[Callable[Concatenate[OwnerT, ActionParams], ActionReturn]], ActionDescriptor[ActionParams, ActionReturn, OwnerT]] Mark a method of a `.Thing` as a LabThings Action. Methods decorated with :deco:`action` will be available to call over HTTP as actions. See :ref:`actions` for an introduction to the concept of actions. This decorator may be used with or without arguments. :param func: The method to be decorated as an action. :param \**kwargs: Keyword arguments are passed to the constructor of `.ActionDescriptor`. :return: 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. .. py:function:: endpoint(method: HTTPMethod, path: Optional[str] = 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 `.Thing` to be directly added to the HTTP API, bypassing the machinery underlying Action and Property affordances. Such endpoints will not be documented in the :ref:`wot_td` but may be used as the target of links. For example, this could allow a file to be downloaded from the `.Thing` at a known URL, or serve a video stream that wouldn't be supported as a `.Blob`\ . The majority of `.Thing` implementations won't need this decorator, but it is here to enable flexibility when it's needed. This decorator always takes arguments; in particular, ``method`` is required. It should be used as: .. code-block:: python class DownloadThing(Thing): @endpoint("get") def plain_text_response(self) -> str: return "example string" This decorator is intended to work very similarly to the `fastapi` decorators ``@app.get``, ``@app.post``, etc., with two changes: 1. The path is relative to the host `.Thing` and will default to the name of the method. 2. The method will be called with the host `.Thing` as its first argument, i.e. it will be bound to the class as usua. :param method: The HTTP verb this endpoint responds to. :param path: The path, relative to the host `.Thing` base URL. :param \**kwargs: Additional keyword arguments are passed to the `fastapi.FastAPI.get` decorator if ``method`` is ``get``, or to the equivalent decorator for other HTTP verbs. :return: When used as intended, the result is an `.EndpointDescriptor`. .. py:class:: ThingServer(things: config_model.ThingsConfig, settings_folder: Optional[str] = None) Use FastAPI to serve `.Thing` instances. The `.ThingServer` sets up a `fastapi.FastAPI` application and uses it to expose the capabilities of `.Thing` instances over HTTP. There are several functions of a `.ThingServer`: * Manage where settings are stored, to allow `.Thing` instances 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 :ref:`actions`. * Manage :ref:`blobs` 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 `.ThingServer` involves creating the underlying `fastapi.FastAPI` app, setting its lifespan function (used to set up and shut down the `.Thing` instances), and configuring it to allow cross-origin requests. We also create the `.ActionManager` to manage :ref:`actions` and the `.BlobManager` to manage the downloading of :ref:`blobs`. :param things: A mapping of Thing names to `.Thing` subclasses, or `.ThingConfig` objects specifying the subclass, its initialisation arguments, and any connections to other `.Thing`\ s. :param settings_folder: the location on disk where `.Thing` settings will be saved. .. py:attribute:: startup_failure :type: dict | None :value: None .. py:attribute:: _config .. py:attribute:: app .. py:attribute:: settings_folder :value: './settings' .. py:attribute:: action_manager .. py:attribute:: blocking_portal :type: Optional[anyio.from_thread.BlockingPortal] :value: None .. py:attribute:: startup_status :type: dict[str, str | dict] .. py:attribute:: _things .. py:method:: from_config(config: config_model.ThingServerConfig) -> typing_extensions.Self :classmethod: Create a ThingServer from a configuration model. This is equivalent to ``ThingServer(**dict(config))``\ . :param config: The configuration parameters for the server. :return: A `.ThingServer` configured as per the model. .. py:method:: _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. .. py:property:: things :type: collections.abc.Mapping[str, labthings_fastapi.thing.Thing] Return a dictionary of all the things. :return: a dictionary mapping thing paths to `.Thing` instances. .. py:attribute:: ThingInstance .. py:method:: things_by_class(cls: type[ThingInstance]) -> collections.abc.Sequence[ThingInstance] Return all Things attached to this server matching a class. Return all instances of ``cls`` attached to this server. :param cls: A `.Thing` subclass. :return: all instances of ``cls`` that have been added to this server. .. py:method:: thing_by_class(cls: type[ThingInstance]) -> ThingInstance Return the instance of ``cls`` attached to this server. This function calls `.ThingServer.things_by_class`, but asserts that there is exactly one match. :param cls: a `.Thing` subclass. :return: the instance of ``cls`` attached to this server. :raise RuntimeError: if there is not exactly one matching Thing. .. py:method:: path_for_thing(name: str) -> str Return the path for a thing with the given name. :param name: The name of the thing. :return: The path at which the thing is served. :raise KeyError: if no thing with the given name has been added. .. py:method:: _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 `.Thing` subclasses and adding them to the server. It also ensures the `.Thing`\ s are connected together if required. The Things are defined in ``self._config.thing_configs`` which in turn is generated from the ``things`` argument to ``__init__``\ . :return: A mapping of names to `.Thing` instances. :raise TypeError: if ``cls`` is not a subclass of `.Thing`. .. py:method:: _connect_things() -> None Connect the `thing_slot` attributes of Things. A `.Thing` may have attributes defined as ``lt.thing_slot()``, which will be populated after all `.Thing` instances are loaded on the server. This function is responsible for supplying the `.Thing` instances 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. `.ThingSlotError` will be raised by code called by this method if the connection cannot be provided. See `.ThingSlot.connect` for more details. .. py:method:: _attach_things_to_server() -> None Add the Things to the FastAPI App. This calls `.Thing.attach_to_server` on each `.Thing` that is a part of this `.ThingServer` in order to add the HTTP endpoints and load settings. .. py:method:: lifespan(app: fastapi.FastAPI) -> AsyncGenerator[None, None] :async: 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. .. _lifespan: https://fastapi.tiangolo.com/advanced/events/#lifespan-function 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. :param 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 to ``self.startup_failure`` for post mortem, as otherwise uvicorn will swallow it and replace it with SystemExit(3) and no traceback. .. py:method:: _add_things_view_to_app() -> None Add an endpoint that shows the list of attached things. .. py:class:: ThingConfig(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` The information needed to add a `.Thing` to 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. `self` is explicitly positional-only to allow `self` as a field name. .. py:attribute:: cls :type: ThingImportString :value: None .. py:attribute:: args :type: collections.abc.Sequence[Any] :value: None .. py:attribute:: kwargs :type: collections.abc.Mapping[str, Any] :value: None .. py:attribute:: thing_slots :type: collections.abc.Mapping[str, str | collections.abc.Iterable[str] | None] :value: None .. py:class:: ThingServerConfig(/, **data: Any) Bases: :py:obj:`pydantic.BaseModel` The 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. `self` is explicitly positional-only to allow `self` as a field name. .. py:attribute:: things :type: ThingsConfig :value: None .. py:method:: check_things(things: ThingsConfig) -> ThingsConfig :classmethod: 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.ImportString` as 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_config` to check each value is either a valid `.ThingConfig` or a type or a mapping. If it's a mapping, we will attempt to make a `.ThingConfig` from it. If it's a `type` we will create a `.ThingConfig` using that type as the class. We don't check for `.Thing` subclasses in this module to avoid a dependency loop. :param things: The validated value of the field. :return: A copy of the input, with all values converted to `.ThingConfig` instances. .. py:property:: thing_configs :type: collections.abc.Mapping[ThingName, ThingConfig] A copy of the ``things`` field where every value is a ``.ThingConfig``\ . The field validator on ``things`` already 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 `.ThingConfig` objects, and is typed strictly. .. py:attribute:: settings_folder :type: str | None :value: None .. py:class:: ThingClient(base_url: str, client: Optional[httpx.Client] = 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. :param base_url: the base URL of the Thing. This should be the URL of the Thing Description document. :param client: an optional `httpx.Client` object to use for all HTTP requests. This may be a `fastapi.TestClient` object for testing purposes. .. py:attribute:: server .. py:attribute:: path .. py:attribute:: client .. py:method:: get_property(path: str) -> Any Make a GET request to retrieve the value of a property. :param path: the URI of the ``getproperty`` endpoint, relative to the ``base_url``. :return: the property's value, as deserialised from JSON. :raise ClientPropertyError: is raised the property cannot be read. .. py:method:: set_property(path: str, value: Any) -> None Make a PUT request to set the value of a property. :param path: the URI of the ``getproperty`` endpoint, relative to the ``base_url``. :param value: the property's value. Currently this must be serialisable to JSON. :raise ClientPropertyError: is raised the property cannot be set. .. py:method:: 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. :param path: the URI of the ``invokeaction`` endpoint, relative to the ``base_url`` :param \**kwargs: Additional arguments will be combined into the JSON body of the ``POST`` request and sent as input to the action. These will be validated on the server. :return: the output value of the action. :raise FailedToInvokeActionError: if the action fails to start. :raise ServerActionError: is raised if the action does not complete successfully. .. py:method:: follow_link(response: dict, rel: str) -> httpx.Response Follow a link in a response object, by its `rel` attribute. :param response: is the dictionary returned by e.g. `.poll_invocation`. :param rel: picks the link to follow by matching its ``rel`` item. :return: the response to making a ``GET`` request to the link. .. py:method:: from_url(thing_url: str, client: Optional[httpx.Client] = None) -> typing_extensions.Self :classmethod: 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. :param thing_url: The base URL of the Thing, which should also be the URL of its Thing Description. :param client: is an optional `httpx.Client` object. 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 (see `fastapi.TestClient`). :return: a `.ThingClient` subclass with properties and methods that match the retrieved Thing Description (see :ref:`wot_thing`). .. py:method:: subclass_from_td(thing_description: dict) -> type[typing_extensions.Self] :classmethod: Create a ThingClient subclass from a Thing Description. Dynamically subclass `.ThingClient` to add properties and methods for each property and action in the Thing Description. :param thing_description: A :ref:`wot_td` as a dictionary, which will be used to construct the class. :return: a `.ThingClient` subclass with the right properties and methods. .. py:function:: cancellable_sleep(interval: float) -> None Sleep for a specified time, allowing cancellation. This function should be called from action functions instead of `time.sleep` to allow them to be cancelled. Usually, this function is equivalent to `time.sleep` (it waits the specified number of seconds). If the action is cancelled during the sleep, it will raise an `.InvocationCancelledError` to signal that the action should finish. .. warning:: This function uses `.Event.wait` internally, which suffers from timing errors on some platforms: it may have error of around 10-20ms. If that's a problem, consider using `time.sleep` instead. ``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`\ . :param interval: The length of time to wait for, in seconds. .. py:function:: 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 `.InvocationCancelledError` to signal the thread to terminate. It is equivalent to `.cancellable_sleep` but without waiting any time. If called outside of an invocation context, this function does nothing, and will not raise an error. .. py:class:: 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: :py:obj:`threading.Thread` A thread that sets a new invocation ID. This is a subclass of `threading.Thread` and works very much the same way. It implements its functionality by overriding the ``run`` method, so this should not be overridden again - you should instead specify the code to run using the ``target`` argument. 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 ``cancel`` method. The thread also remembers the return value of the target function in the property ``result`` and stores any exception raised in the ``exception`` property. 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 calling ``join`` but 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. :param target: the function to call in the thread. :param args: positional arguments to ``target``\ . :param kwargs: keyword arguments to ``target``\ . :param \*super_args: arguments passed to `threading.Thread`\ . :param \*\*super_kwargs: keyword arguments passed to `threading.Thread`\ . .. py:attribute:: _target .. py:attribute:: _args :value: [] .. py:attribute:: _kwargs .. py:attribute:: _invocation_id :type: uuid.UUID .. py:attribute:: _result :type: Any :value: None .. py:attribute:: _exception :type: BaseException | None :value: None .. py:attribute:: _cancel_event .. py:property:: invocation_id :type: uuid.UUID The InvocationID of this thread. .. py:property:: result :type: Any The return value of the target function. .. py:property:: exception :type: BaseException | None The exception raised by the target function, or None. .. py:method:: cancel() -> None Set the cancel event to tell the code to terminate. .. py:method:: join_and_propagate_cancel(poll_interval: float = 0.2) -> None Wait for the thread to finish, and propagate cancellation. This function wraps `threading.Thread.join` but periodically checks if the calling thread has been cancelled. If it has, it will cancel the thread, before attempting to ``join`` it 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 `.InvocationCancelledError` unless you wish your invocation to terminate if it is cancelled. :param 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. .. py:method:: run() -> None Run the target function, with invocation ID set in the context variable.