diff --git a/dev-requirements.txt b/dev-requirements.txt
index 1b210b6e..7aa26658 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -10,20 +10,31 @@ anyio==4.9.0
# httpx
# starlette
# watchfiles
+apeye==1.4.1
+ # via sphinx-toolbox
+apeye-core==1.1.5
+ # via apeye
astroid==3.3.11
# via sphinx-autoapi
attrs==25.3.0
# via
# jsonschema
# referencing
+autodocsumm==0.2.14
+ # via sphinx-toolbox
babel==2.17.0
# via sphinx
+beautifulsoup4==4.14.2
+ # via sphinx-toolbox
+cachecontrol==0.14.3
+ # via sphinx-toolbox
certifi==2025.7.14
# via
# httpcore
# httpx
# requests
# sentry-sdk
+ # sphinx-prompt
charset-normalizer==3.4.2
# via requests
click==8.2.1
@@ -42,6 +53,10 @@ colorama==0.4.6
# uvicorn
coverage==7.9.2
# via pytest-cov
+cssutils==2.11.1
+ # via dict2css
+dict2css==0.3.0.post1
+ # via sphinx-toolbox
dnspython==2.7.0
# via email-validator
docstring-parser-fork==0.0.12
@@ -50,7 +65,16 @@ docutils==0.21.2
# via
# restructuredtext-lint
# sphinx
+ # sphinx-prompt
# sphinx-rtd-theme
+ # sphinx-tabs
+ # sphinx-toolbox
+domdf-python-tools==3.10.0
+ # via
+ # apeye
+ # apeye-core
+ # dict2css
+ # sphinx-toolbox
email-validator==2.2.0
# via
# fastapi
@@ -65,6 +89,10 @@ fastapi-cli==0.0.8
# via fastapi
fastapi-cloud-cli==0.1.4
# via fastapi-cli
+filelock==3.20.0
+ # via
+ # cachecontrol
+ # sphinx-toolbox
flake8==7.3.0
# via
# labthings-fastapi (pyproject.toml)
@@ -82,6 +110,8 @@ h11==0.16.0
# via
# httpcore
# uvicorn
+html5lib==1.1
+ # via sphinx-toolbox
httpcore==1.0.9
# via httpx
httptools==0.6.4
@@ -94,9 +124,11 @@ httpx==0.28.1
idna==3.10
# via
# anyio
+ # apeye-core
# email-validator
# httpx
# requests
+ # sphinx-prompt
ifaddr==0.2.0
# via zeroconf
imagesize==1.4.1
@@ -110,6 +142,7 @@ jinja2==3.1.6
# fastapi
# sphinx
# sphinx-autoapi
+ # sphinx-jinja2-compat
jsonschema==4.24.1
# via labthings-fastapi (pyproject.toml)
jsonschema-specifications==2025.4.1
@@ -117,15 +150,23 @@ jsonschema-specifications==2025.4.1
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
- # via jinja2
+ # via
+ # jinja2
+ # sphinx-jinja2-compat
mccabe==0.7.0
# via flake8
mdurl==0.1.2
# via markdown-it-py
+more-itertools==10.8.0
+ # via cssutils
+msgpack==1.1.2
+ # via cachecontrol
mypy==1.17.0
# via labthings-fastapi (pyproject.toml)
mypy-extensions==1.1.0
# via mypy
+natsort==8.4.0
+ # via domdf-python-tools
numpy==2.2.6
# via labthings-fastapi (pyproject.toml)
orjson==3.11.0
@@ -138,6 +179,8 @@ pathspec==0.12.1
# via mypy
pillow==11.3.0
# via labthings-fastapi (pyproject.toml)
+platformdirs==4.5.0
+ # via apeye
pluggy==1.6.0
# via
# pytest
@@ -167,6 +210,8 @@ pygments==2.19.2
# pytest
# rich
# sphinx
+ # sphinx-prompt
+ # sphinx-tabs
pytest==8.4.1
# via
# labthings-fastapi (pyproject.toml)
@@ -193,7 +238,10 @@ referencing==0.36.2
# jsonschema-specifications
# types-jsonschema
requests==2.32.4
- # via sphinx
+ # via
+ # apeye
+ # cachecontrol
+ # sphinx
restructuredtext-lint==1.4.0
# via flake8-rst-docstrings
rich==14.0.0
@@ -210,26 +258,49 @@ rpds-py==0.26.0
# via
# jsonschema
# referencing
+ruamel-yaml==0.18.16
+ # via sphinx-toolbox
+ruamel-yaml-clib==0.2.14
+ # via ruamel-yaml
ruff==0.12.3
# via labthings-fastapi (pyproject.toml)
sentry-sdk==2.33.0
# via fastapi-cloud-cli
shellingham==1.5.4
# via typer
+six==1.17.0
+ # via html5lib
sniffio==1.3.1
# via anyio
snowballstemmer==3.0.1
# via sphinx
+soupsieve==2.8
+ # via beautifulsoup4
sphinx==8.1.3
# via
# labthings-fastapi (pyproject.toml)
+ # autodocsumm
# sphinx-autoapi
+ # sphinx-autodoc-typehints
+ # sphinx-prompt
# sphinx-rtd-theme
+ # sphinx-tabs
+ # sphinx-toolbox
# sphinxcontrib-jquery
sphinx-autoapi==3.6.0
# via labthings-fastapi (pyproject.toml)
+sphinx-autodoc-typehints==3.0.1
+ # via sphinx-toolbox
+sphinx-jinja2-compat==0.4.1
+ # via sphinx-toolbox
+sphinx-prompt==1.9.0
+ # via sphinx-toolbox
sphinx-rtd-theme==3.0.2
# via labthings-fastapi (pyproject.toml)
+sphinx-tabs==3.4.5
+ # via sphinx-toolbox
+sphinx-toolbox==4.0.0
+ # via labthings-fastapi (pyproject.toml)
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
@@ -246,6 +317,8 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
starlette==0.47.1
# via fastapi
+tabulate==0.9.0
+ # via sphinx-toolbox
tomli==2.2.1
# via
# coverage
@@ -265,6 +338,8 @@ typing-extensions==4.14.1
# labthings-fastapi (pyproject.toml)
# anyio
# astroid
+ # beautifulsoup4
+ # domdf-python-tools
# exceptiongroup
# fastapi
# mypy
@@ -274,6 +349,7 @@ typing-extensions==4.14.1
# referencing
# rich
# rich-toolkit
+ # sphinx-toolbox
# starlette
# typer
# typing-inspection
@@ -286,6 +362,7 @@ urllib3==2.5.0
# via
# requests
# sentry-sdk
+ # sphinx-prompt
uvicorn==0.35.0
# via
# fastapi
@@ -293,6 +370,8 @@ uvicorn==0.35.0
# fastapi-cloud-cli
watchfiles==1.1.0
# via uvicorn
+webencodings==0.5.1
+ # via html5lib
websockets==15.0.1
# via uvicorn
zeroconf==0.147.0
diff --git a/docs/source/actions.rst b/docs/source/actions.rst
index 68e1d1b0..dd32e5e3 100644
--- a/docs/source/actions.rst
+++ b/docs/source/actions.rst
@@ -8,7 +8,7 @@ terms, any method of a `.Thing` that we want to be able to call over HTTP
should be decorated as an Action, using `.thing_action`.
This page gives an overview of how actions are implemented in LabThings-FastAPI.
-:ref:`wot_cc` includes a section on :ref:`wot_actions` that introduces the general concept.
+Our implementation should align with :ref:`wot_actions` as defined by the Web of Things standard.
Running actions via HTTP
------------------------
@@ -58,7 +58,8 @@ The first is ``self`` (the first positional argument), which is always the
supply resources needed by the action. Most often, this is a way of accessing
other `.Things` on the same server.
-.. action_logging:
+.. _action_logging:
+
Logging from actions
--------------------
Action code should use `.Thing.logger` to log messages. This will be configured
@@ -70,7 +71,8 @@ a long time to run, or it may simply be a helpful debugging aid.
See :mod:`.logs` for details of how this is implemented.
-.. action_cancellation:
+.. _action_cancellation:
+
Cancelling actions
------------------
If an action could run for a long time, it is useful to be able to cancel it
@@ -82,7 +84,8 @@ is equivalent, but will raise an exception if the action is cancelled.
For more advanced options, see `.invocation_contexts` for detail.
-.. invocation_context:
+.. _invocation_context:
+
Invocation contexts
-------------------
Cancelling actions and capturing their logs requires action code to use a
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 3acee8d0..b640feca 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -1,5 +1,6 @@
import logging
import labthings_fastapi
+import importlib.metadata
# Configuration file for the Sphinx documentation builder.
#
@@ -10,9 +11,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "labthings-fastapi"
-copyright = "2024, Richard Bowman"
+copyright = "2025, Richard Bowman"
author = "Richard Bowman"
-release = "0.0.10"
+release = importlib.metadata.version("labthings-fastapi")
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@@ -23,6 +24,7 @@
# "autodoc2",
"autoapi.extension",
"sphinx_rtd_theme",
+ "sphinx_toolbox.decorators",
]
templates_path = ["_templates"]
diff --git a/docs/source/developer_notes/descriptors.rst b/docs/source/developer_notes/descriptors.rst
new file mode 100644
index 00000000..952f6c7d
--- /dev/null
+++ b/docs/source/developer_notes/descriptors.rst
@@ -0,0 +1,72 @@
+.. _descriptors:
+
+Descriptors
+===========
+
+Descriptors are a way to intercept attribute access on an object, and they are used extensively by LabThings to add functionality to `.Thing` instances, while continuing to look like normal Python objects.
+
+By default, attributes of an object are just variables - so an object called ``foo`` might have an attribute called ``bar``, and you may read its value with ``foo.bar``, write its value with ``foo.bar = "baz"``, and delete the attribute with ``del foo.bar``. If ``foo`` is a descriptor, Python will call the ``__get__`` method of that descriptor when it's read and the ``__set__`` method when it's written to. You have quite probably used a descriptor already, because the built-in `~builtins.property` creates a descriptor object: that's what runs your getter method when the property is accessed. The descriptor protocol is described with plenty of examples in the `Descriptor Guide`_ in the Python documentation.
+
+In LabThings-FastAPI, descriptors are used to implement :ref:`actions` and :ref:`properties` on `.Thing` subclasses. The intention is that these will function like standard Python methods and properties, but will also be available over HTTP, along with :ref:`gen_docs`.
+
+.. _field_typing:
+
+Field typing
+------------
+
+:ref:`properties` and :ref:`settings` in LabThings-FastAPI are implemented using descriptors. The type of these descriptors is usually determined from the type hint on the class attribute to which they are assigned. For example:
+
+.. code-block:: python
+
+ class MyThing(lt.Thing):
+ my_property: int = lt.property(default=0)
+ """An integer property."""
+
+This makes it clear to anyone using ``MyThing`` that ``my_property`` is an integer, and should be picked up by most type checking/autocompletion tools. However, because the annotation is attached to the *class* and not passed to the underlying `.DataProperty` descriptor, we need to use the descriptor protocol to figure it out.
+
+Field typing in LabThings is implemented by `.FieldTypedBaseDescriptor` and there are docstrings on all of the relevant "magic" methods explaining what each one does. Below, there is a brief overview of how these fit together.
+
+* When the descriptor is created, we don't know its name or type. ``__init__`` just stores any parameters that were passed to the descriptor constructor (e.g. ``default``). Some subclasses (in particular `.FunctionalProperty`) may be able to determine the type at this point, in which case it can be assigned to ``self._value_type``, and no errors will be raised in ``__set_name__`` if there is no type hint on the attribute.
+* When the class is created, Python calls the ``__set_name__`` method of the descriptor, passing in the owning class and the descriptor's name. This allows the descriptor to check whether there is a type annotation, but we don't evaluate it yet. Type annotations are deliberately not evaluated until they are needed, to allow forward references to work as intended. If there isn't a type hint, and the type hasn't been specified in some other way, we raise an exception at this point. This will appear to come from the end of the class definition, because `__set_name__` is called after all the class attributes have been created. The exception should contain the name of the attribute that's missing a type hint (and this is tested in our test suite).
+* The first time `.FieldTypedBaseDescriptor.value_type` is accessed, we evaluate the type hint (if any) using `typing.get_type_hints`. This allows forward references to be resolved correctly. The evaluated type is cached so that subsequent accesses are fast.
+* The ``__get__`` and ``__set__`` methods get and set the value of the property. Currently, no run-time type checking is done if the attribute is used from Python. The type hint is used when generating the :ref:`gen_td` and OpenAPI documentation, and is used to validate values that are set over HTTP.
+
+.. _descriptor_implementation:
+
+Descriptor implementation
+-------------------------
+
+There are a few useful notes that relate to many of the descriptors in LabThings-FastAPI:
+
+* Descriptor objects **may have more than one owner**. As a rule, a descriptor object
+ (e.g. an instance of `.DataProperty`) is assigned to an attribute of one `.Thing` subclass. There may, however, be multiple *instances* of that class, so it is not safe to assume that the descriptor object corresponds to only one `.Thing`. This is why the `.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `.Thing` and are not simply stored in the descriptor. Usually, this is done using `.WeakKeyDictionary` objects, which allow us to look up values based on the `.Thing`, without interfering with garbage collection.
+
+ The example below shows how this can go wrong.
+
+ .. code-block:: python
+
+ class BadProperty:
+ "An example of a descriptor that has unwanted behaviour."
+ def __init__(self):
+ self._value = None
+
+ def __get__(self, obj):
+ return self._value
+
+ def __set__(self, obj, val):
+ self._value = val
+
+ class BrokenExample:
+ myprop = BadProperty()
+
+ a = BrokenExample()
+ b = BrokenExample()
+
+ assert a.myprop is None
+ b.myprop = True
+ assert a.myprop is None # FAILS because `myprop` shares values between a and b
+
+* Descriptor objects **may know their name**. Python calls ``__set_name__`` on a descriptor if it is available. This allows the descriptor to know the name of the attribute to which it is assigned. LabThings-FastAPI uses the name in the URL and in the Thing Description. When ``__set_name__`` is called, the descriptor can also access the class that owns it, which we use to implement :ref:`field_typing` above.
+* There is a convention that descriptors return their value when accessed as an instance attribute, but return themselves when accessed as a class attribute (as done by `builtins.property`). All descriptors that inherit from `.BaseDescriptor` adhere to that convention.
+
+.. _`Descriptor Guide`: https://docs.python.org/3/howto/descriptor.html
\ No newline at end of file
diff --git a/docs/source/developer_notes/index.rst b/docs/source/developer_notes/index.rst
new file mode 100644
index 00000000..b7021567
--- /dev/null
+++ b/docs/source/developer_notes/index.rst
@@ -0,0 +1,10 @@
+.. _developer_notes:
+
+Developer Notes
+================
+
+.. toctree::
+
+ descriptors.rst
+
+This section contains notes for developers working on LabThings-FastAPI itself. If you only intend to use LabThings-FastAPI to create your own Things and ThingServers, you probably don't need to read this section.
\ No newline at end of file
diff --git a/docs/source/documentation.rst b/docs/source/documentation.rst
index 5a781435..c7772046 100644
--- a/docs/source/documentation.rst
+++ b/docs/source/documentation.rst
@@ -3,23 +3,23 @@
Generated documentation
=======================
-LabThings describes its HTTP API in two ways: with a :ref:`wot_td` and with an OpenAPI_ document.
+LabThings describes its HTTP API in two ways: with a :ref:`gen_td` and with an OpenAPI_ document.
.. _openapi:
OpenAPI
-------
-OpenAPI_ is a standard way to describe an HTTP interface. It lists all of the possible HTTP requests that may be made, along with a description of each one, and a description of the possible responses.
+`OpenAPI ` is a standard way to describe an HTTP interface. It lists all of the possible HTTP requests that may be made, along with a description of each one, and a description of the possible responses. The OpenAPI document is generated automatically by `fastapi`\ , and may be downloaded at `/openapi.json` or viewed in a web browser at the `/docs` or `/redoc` endpoints of a running LabThings server. Both of the web browser views rely on JavaScript libraries that are not currently bundled with LabThings, so an internet connection is required to view them. The OpenAPI document is available without an internet connection.
.. _gen_td:
Thing Description
-----------------
-Each :ref:`wot_thing` is documented by a Thing Description, which is a JSON document describing all of the ways to interact with that Thing (:ref:`wot_affordances`\ ). The WoT_ standard defines the `Thing Description`_ and includes a JSON Schema against which it may be validated.
+Each :ref:`Thing ` is documented by a :ref:`gen_td`, which is a JSON document describing all of the ways to interact with that Thing (:ref:`wot_affordances`\ ). The WoT_ standard defines the `Thing Description`_ and includes a JSON Schema against which it may be validated.
-Thing Description documents are higher-level than OpenAPI_ and focus on the capabilities of the Thing. For example, they include a list of properties, where each action is described only once. LabThings treats the Thing Description as your public API, and as a general rule anything not described in the Thing Description is not available over HTTP or to a `.DirectThingClient`\ .
+Thing Description documents are higher-level than OpenAPI_ and focus on the capabilities of the Thing. For example, they include a list of properties, where each action is described only once. LabThings treats the Thing Description as your public API, and as a general rule anything not described in the Thing Description is not available over HTTP.
Comparison of Thing Description and OpenAPI
-------------------------------------------
@@ -30,4 +30,3 @@ OpenAPI describes each HTTP endpoint individually. There are usually more HTTP e
.. _WoT: https://www.w3.org/WoT/
.. _Thing Description: https://www.w3.org/TR/wot-thing-description/
-.. _OpenAPI: https://www.openapis.org/
diff --git a/docs/source/index.rst b/docs/source/index.rst
index f43b163f..90100a63 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -6,10 +6,10 @@ Documentation for LabThings-FastAPI
:caption: Contents:
quickstart/quickstart.rst
- wot_core_concepts.rst
- structure.rst
tutorial/index.rst
+ structure.rst
examples.rst
+ documentation.rst
actions.rst
thing_slots.rst
dependencies/dependencies.rst
@@ -17,8 +17,10 @@ Documentation for LabThings-FastAPI
concurrency.rst
using_things.rst
see_also.rst
+ wot_core_concepts.rst
autoapi/index
+ developer_notes/index.rst
`labthings-fastapi` is a Python library to simplify the process of making laboratory instruments available via a HTTP. It aims to create an API that is usable from any modern programming language, with API documentation in both :ref:`openapi` and :ref:`gen_td` formats. It is the underlying framework for v3 of the `OpenFlexure Microscope software `_. Key features and design aims are:
@@ -28,13 +30,13 @@ Documentation for LabThings-FastAPI
* Actions are decorated methods of a `.Thing` class. There is no need for separate schemas or endpoint definitions.
* Properties are defined either as typed attributes (similar to `pydantic` or `dataclasses`) or with a `property`\ -like decorator.
* Lifecycle and concurrency are appropriate for hardware: `Thing` code is always run in a thread, and each `Thing` is instantiated, started up, and shut down only once.
-* Vocabulary and concepts are aligned with the `W3C Web of Things `_ standard (see :doc:`wot_core_concepts`)
+* Vocabulary and concepts are aligned with the `W3C Web of Things `_ standard (see :ref:`wot_cc`)
Previous version
----------------
This is a ground-up rewrite of python-labthings_, replacing Flask 1 and Marshmallow with FastAPI and Pydantic.
-Compared to `python-labthings`_, this framework updates dependencies, shrinks the codebase, and simplifies the API (see :doc:`lt_structure`).
+Compared to `python-labthings`_, this framework updates dependencies, shrinks the codebase, and simplifies the API (see :doc:`structure`).
* FastAPI more or less completely eliminates OpenAPI generation code from our codebase
* Marshmallow schemas and endpoint classes are replaced with Python type hints, eliminating double- or triple-definition of actions and their inputs/outputs.
* Thing Description generation is very much simplified by the new structure (multiple Things instead of one massive Thing with many extensions)
diff --git a/docs/source/see_also.rst b/docs/source/see_also.rst
index cbb6b696..13c8918f 100644
--- a/docs/source/see_also.rst
+++ b/docs/source/see_also.rst
@@ -1,48 +1,10 @@
+.. _see_also:
+
See Also
========
LabThings-FastAPI makes quite heavy use of a few key concepts from external libraries, including `fastapi`, `pydantic`, and of course Python's core library. This page attempts to summarise these, and also acts as a useful place for docstrings to link to, so we can avoid repetition.
-.. _descriptors:
-
-Descriptors
------------
-
-Descriptors are a way to intercept attribute access on an object. By default, attributes of an object are just variables - so an object called ``foo`` might have an attribute called ``bar``, and you may read its value with ``foo.bar``, write its value with ``foo.bar = "baz"``, and delete the attribute with ``del foo.bar``. If ``foo`` is a descriptor, Python will call the ``__get__`` method of that descriptor when it's read and the ``__set__`` method when it's written to. You have quite probably used a descriptor already, because the built-in `~builtins.property` creates a descriptor object: that's what runs your getter method when the property is accessed. The descriptor protocol is described with plenty of examples in the `Descriptor Guide`_ in the Python documentation.
-
-In LabThings-FastAPI, descriptors are used to implement :ref:`wot_actions` and :ref:`wot_properties` on `.Thing` subclasses. The intention is that these will function like standard Python methods and properties, but will also be available over HTTP, along with automatic documentation in the :ref:`wot_td` and OpenAPI documents.
-
-There are a few useful notes that relate to many of the descriptors in LabThings-FastAPI:
-
-* Descriptor objects **may have more than one owner**. As a rule, a descriptor object
- (e.g. an instance of `.DataProperty`) is assigned to an attribute of one `.Thing` subclass. There may, however, be multiple *instances* of that class, so it is not safe to assume that the descriptor object corresponds to only one `.Thing`. This is why the `.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `.Thing` and are not simply stored in the descriptor. Usually, this is done using `.WeakKeyDictionary` objects, which allow us to look up values based on the `.Thing`, without interfering with garbage collection.
-
- The example below shows how this can go wrong.
-
- .. code-block:: python
-
- class BadProperty:
- "An example of a descriptor that has unwanted behaviour."
- def __init__(self):
- self._value = None
-
- def __get__(self, obj):
- return self._value
-
- def __set__(self, obj, val):
- self._value = val
-
- class BrokenExample:
- myprop = BadProperty()
-
- a = BrokenExample()
- b = BrokenExample()
-
- assert a.myprop is None
- b.myprop = True
- assert a.myprop is None # FAILS because `myprop` shares values between a and b
-
-* Descriptor objects **may know their name**. Python calls ``__set_name__`` on a descriptor if it is available. This allows the descriptor to know the name of the attribute to which it is assigned. LabThings-FastAPI uses the name in the URL and in the Thing Description. When ``__set_name__`` is called, the descriptor **is also passed the class that owns it**. This allows us to check for type hints and docstrings that are part of the class, rather than part of the descriptor.
-* There is a convention that descriptors return their value when accessed as an instance attribute, but return themselves when accessed as a class attribute (as done by `builtins.property`). LabThings adheres to that convention.
-
-.. _`Descriptor Guide`: https://docs.python.org/3/howto/descriptor.html
\ No newline at end of file
+* LabThings makes much use of :ref:`descriptors` - see that page for implementation details and a link to the Python descriptor documentation.
+* LabThings-FastAPI uses `FastAPI `_ to implement the HTTP server and generate OpenAPI documentation. This documentation uses intersphinx to link to specific `fastapi` classes and functions where appropriate.
+* LabThings-FastAPI uses `pydantic `_ to define data models for action inputs and outputs, and for property values. This documentation uses intersphinx to link to specific `pydantic` classes and functions where appropriate.
\ No newline at end of file
diff --git a/docs/source/structure.rst b/docs/source/structure.rst
index ef813b1c..d786c989 100644
--- a/docs/source/structure.rst
+++ b/docs/source/structure.rst
@@ -1,5 +1,5 @@
.. _labthings_cc:
-.. _labthings_structure:
+.. _structure:
LabThings structure
===================
@@ -20,7 +20,9 @@ LabThings-FastAPI is built on top of `fastapi`\ , which is a fast, modern HTTP f
* Generating a :ref:`gen_td` in addition to the :ref:`openapi` documentation produced by `fastapi`\ .
* Making connections between `.Thing` instances as required.
-`.Thing`\ s
+.. _things:
+
+Things
-----------
Each unit of hardware (or software) that should be exposed by the server is implemented as a subclass of `.Thing`\ . A `.Thing` subclass represents a particular type of instrument (whether hardware or software), and its functionality is described using actions and properties, described below. `.Thing`\ s don't have to correspond to separate pieces of hardware: it's possible (and indeed recommended) to use `.Thing` subclasses for software components, plug-ins, swappable modules, or anything else that needs to add functionality to the server. `.Thing`\ s may access each other's attributes, so you can write a `.Thing` that implements a particular measurement protocol or task, using hardware that's accessed through other `.Thing` instances on the server. Each `.Thing` is documented by a :ref:`gen_td` which outlines its features in a higher-level way than :ref:`openapi`\ .
@@ -35,7 +37,7 @@ The attributes of a `.Thing` are made available over HTTP by decorating or marki
Client Code
-----------
-Client code can be written in any language that supports an HTTP request. However, LabThings FastAPI provides additional functionality that makes writing client code in Python easier.
+Client code can be written in any language that supports an HTTP request. However, LabThings FastAPI provides additional functionality that makes writing client code in Python easier. See :ref:`using_things` for more detail.
`.ThingClient` is a class that wraps up the required HTTP requests into a simpler interface. It can retrieve the :ref:`gen_td` over HTTP and use it to generate a new object with methods matching each `.thing_action` and properties matching each `.property`.
diff --git a/docs/source/thing_slots.rst b/docs/source/thing_slots.rst
index 244e9bdf..8e1aaa94 100644
--- a/docs/source/thing_slots.rst
+++ b/docs/source/thing_slots.rst
@@ -1,4 +1,4 @@
-.. thing_slots:
+.. _thing_slots:
Thing Slots
===========
diff --git a/docs/source/tutorial/properties.rst b/docs/source/tutorial/properties.rst
index 9e3b970e..ebdff0e7 100644
--- a/docs/source/tutorial/properties.rst
+++ b/docs/source/tutorial/properties.rst
@@ -1,4 +1,5 @@
.. _tutorial_properties:
+.. _properties:
Properties
=========================
@@ -134,3 +135,10 @@ Observable properties
Properties can be made observable, which means that clients can subscribe to changes in the property's value. This is useful for properties that change frequently, such as sensor readings or instrument settings. In order for a property to be observable, LabThings must know whenever it changes. Currently, this means only data properties can be observed, as functional properties do not have a simple value that can be tracked.
Properties are currently only observable via websockets: in the future, it may be possible to observe them from other `.Thing` instances or from other parts of the code.
+
+.. _settings:
+
+Settings
+------------
+
+Settings are properties with an additional feature: they are saved to disk. This means that settings will be automatically restored after the server is restarted. The function `.setting` can be used to declare a `.DataSetting` or decorate a function to make a `.FunctionalSetting` in the same way that `.property` can. It is usually imported as ``lt.setting``\ .
diff --git a/docs/source/tutorial/running_labthings.rst b/docs/source/tutorial/running_labthings.rst
index 4a1977de..a3f8baad 100644
--- a/docs/source/tutorial/running_labthings.rst
+++ b/docs/source/tutorial/running_labthings.rst
@@ -9,7 +9,7 @@ Once you have activated the virtual environment, you should be able to run an ex
.. code-block:: bash
- labthings-server --json '{"things":{"/mything":"labthings_fastapi.example_things:MyThing"}}'
+ labthings-server --json '{"things":{"mything":"labthings_fastapi.example_things:MyThing"}}'
This command will start a LabThings server, and will print the root URL for your server (by default, ``http://127.0.0.1:5000``). The ``127.0.0.1`` part means the server is only accessible from your computer, so you don't need to worry about other computers on your network accessing it.
@@ -28,7 +28,7 @@ It is worth unpicking the command you ran to start the server: it has one argume
{
"things": {
- "/mything": "labthings_fastapi.example_things:MyThing"
+ "mything": "labthings_fastapi.example_things:MyThing"
}
}
@@ -38,3 +38,9 @@ You can then start the server using the command:
labthings-server --config example_things.json
+.. _serving_from_python:
+
+Starting the server from Python
+-------------------------------
+
+It is also possible to start a LabThings server from within a Python script. This is the only way to serve `.Thing` classes that are not importable (e.g. if you're running example code that's not structured as a proper package). Many of the examples will start a server in this way - for example :ref:`tutorial_thing`\ .
diff --git a/docs/source/tutorial/writing_a_thing.rst b/docs/source/tutorial/writing_a_thing.rst
index f6aa16bf..776923e0 100644
--- a/docs/source/tutorial/writing_a_thing.rst
+++ b/docs/source/tutorial/writing_a_thing.rst
@@ -1,4 +1,4 @@
-.. tutorial_thing:
+.. _tutorial_thing:
Writing a Thing
=========================
diff --git a/docs/source/using_things.rst b/docs/source/using_things.rst
index ea4d24c7..e60010ac 100644
--- a/docs/source/using_things.rst
+++ b/docs/source/using_things.rst
@@ -1,3 +1,5 @@
+.. _using_things:
+
Using Things
============
@@ -5,8 +7,17 @@ The interface to a `Thing` is defined by its actions, properties and events [#ev
`.ThingClient` subclasses can be generated dynamically from a URL using :meth:`.ThingClient.from_url`. This creates an object with the right methods, properties and docstrings, though type hints are often missing. The client can be "introspected" to explore its methods and properties using tools that work at run-time (e.g. autocompletion in a Jupyter notebook), but "static" analysis tools will not yet work.
+Both the input and return types of the functions of a `.ThingClient` are intended to match those of the `.Thing` it's connected to, however there are currently some differences. In particular, `pydantic` models are usually converted to dictionaries. This is something that is on the long-term roadmap to improve, possibly with :ref:`code generation `\ .
+
.. [#events] Events are not yet implemented.
+.. _things_from_things:
+
+Using Things from other Things
+------------------------------
+
+Code within a Thing may access other Things on the same server using :ref:`thing_slots`. These are attributes of the `.Thing` that will be supplied by the server when it is set up. When you access a `.thing_slot` it will return the other `.Thing` instance, and it may be used like any other Python object. `.thing_slot`\ s may be optional, or may be configured to return multiple `.Thing` instances: see the :ref:`thing_slots` documentation for more details.
+
Using Things from other languages
----------------------------------
@@ -19,18 +30,7 @@ Dynamic class generation
The object returned by `.ThingClient.from_url` is an instance of a dynamically-created subclass of `.ThingClient`. Dynamically creating the class is needed because we don't know what the methods and properties should be until we have downloaded the Thing Description. However, this means most code autocompletion tools, type checkers, and linters will not work well with these classes. In the future, LabThings-FastAPI will generate custom client subclasses that can be shared in client modules, which should fix these problems (see below).
-.. _things_from_things:
-
-Using Things from other Things
-------------------------------
-
-One goal of LabThings-FastAPI is to make code portable between a client (e.g. a Jupyter notebook, or a Python script on another computer) and server-side code (i.e. code inside an action of a `.Thing`). This is done using a `.DirectThingClient` class, which is a subclass of `.ThingClient`.
-
-A `.DirectThingClient` class will call actions and properties of other `.Thing` subclasses using the same interface that would be used by a remote client, which means code for an action may be developed as an HTTP client, for example in a Jupyter notebook, and then moved to the server with minimal changes. Currently, there are a few differences in behaviour between working locally or remotely, most notably the return types (which are usually Pydantic models on the server, and currently dictionaries on the client). This should be improved in the future.
-
-It is also possible for a `.Thing` to access other `.Thing` instances directly. This gives access to functionality that is only available in Python, i.e. not available through a `.ThingClient` over HTTP. However, the `.Thing` must then be supplied manually with any :ref:`dependencies` required by its actions, and the public API as defined by the :ref:`wot_td` is no longer enforced.
-
-Actions that make use of other `.Thing` objects on the same server should access them using :ref:`dependencies`.
+.. _client_codegen:
Planned future development: static code generation
--------------------------------------------------
diff --git a/docs/source/wot_core_concepts.rst b/docs/source/wot_core_concepts.rst
index ead89eaf..09b70c87 100644
--- a/docs/source/wot_core_concepts.rst
+++ b/docs/source/wot_core_concepts.rst
@@ -61,4 +61,4 @@ Thing Description documents are higher-level than OpenAPI_ and focus on the capa
Interaction Affordances
-----------------------
-The Web of Things standard often talks about Affordances. This is the collective term for _wot_properties, _wot_actions, and _wot_events.
+The Web of Things standard often talks about Affordances. This is the collective term for :ref:`wot_properties`, :ref:`wot_actions`, and :ref:`wot_events`.
diff --git a/pyproject.toml b/pyproject.toml
index 47cbd943..6818da46 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -40,6 +40,7 @@ dev = [
"sphinx-rtd-theme",
"sphinx>=7.2",
"sphinx-autoapi",
+ "sphinx-toolbox",
"codespell",
]
diff --git a/src/labthings_fastapi/actions/__init__.py b/src/labthings_fastapi/actions/__init__.py
index cdca5895..c4fe80ec 100644
--- a/src/labthings_fastapi/actions/__init__.py
+++ b/src/labthings_fastapi/actions/__init__.py
@@ -1,6 +1,6 @@
"""Actions module.
-:ref:`wot_actions` are represented by methods, decorated with the `.thing_action`
+:ref:`actions` are represented by methods, decorated with the `.thing_action`
decorator.
See the :ref:`actions` documentation for a top-level overview of actions in
@@ -35,29 +35,22 @@
from ..exceptions import (
InvocationCancelledError,
InvocationError,
+ NoBlobManagerError,
)
from ..outputs.blob import BlobIOContextDep, blobdata_to_url_ctx
-from ..invocation_contexts import (
- CancelEvent,
- get_cancel_event,
- set_invocation_id,
-)
+from .. import invocation_contexts
if TYPE_CHECKING:
# We only need these imports for type hints, so this avoids circular imports.
from ..descriptors import ActionDescriptor
from ..thing import Thing
-ACTION_INVOCATIONS_PATH = "/action_invocations"
-"""The API route used to list `.Invocation` objects."""
+__all__ = ["ACTION_INVOCATIONS_PATH", "Invocation", "ActionManager"]
-class NoBlobManagerError(RuntimeError):
- """Raised if an API route accesses Invocation outputs without a BlobIOContextDep.
- Any access to an invocation output must have BlobIOContextDep as a dependency, as
- the output may be a blob, and the blob needs this context to resolve its URL.
- """
+ACTION_INVOCATIONS_PATH = "/action_invocations"
+"""The API route used to list `.Invocation` objects."""
class Invocation(Thread):
@@ -197,9 +190,9 @@ def thing(self) -> Thing:
return thing
@property
- def cancel_hook(self) -> CancelEvent:
+ def cancel_hook(self) -> invocation_contexts.CancelEvent:
"""The cancel event associated with this Invocation."""
- return get_cancel_event(self.id)
+ return invocation_contexts.get_cancel_event(self.id)
def cancel(self) -> None:
"""Cancel the task by requesting the code to stop.
@@ -278,7 +271,7 @@ def run(self) -> None:
logger = self.thing.logger
# The line below saves records matching our ID to ``self._log``
add_thing_log_destination(self.id, self._log)
- with set_invocation_id(self.id):
+ with invocation_contexts.set_invocation_id(self.id):
try:
action.emit_changed_event(self.thing, self._status.value)
diff --git a/src/labthings_fastapi/base_descriptor.py b/src/labthings_fastapi/base_descriptor.py
index d2f66b4f..3b9c9a2c 100644
--- a/src/labthings_fastapi/base_descriptor.py
+++ b/src/labthings_fastapi/base_descriptor.py
@@ -390,8 +390,8 @@ def __set_name__(self, owner: type[Thing], name: str) -> None:
.. code-block:: python
class MyThing(Thing):
- subscripted_property = DataProperty[int](0)
- annotated_property: int = DataProperty(0)
+ subscripted_property = DataProperty[int](default=0)
+ annotated_property: int = DataProperty(default=0)
The second form often works better with autocompletion, though it
is usually called via a function to avoid type checking errors.
diff --git a/src/labthings_fastapi/decorators/__init__.py b/src/labthings_fastapi/decorators/__init__.py
index f1c61460..a4b2861e 100644
--- a/src/labthings_fastapi/decorators/__init__.py
+++ b/src/labthings_fastapi/decorators/__init__.py
@@ -1,12 +1,10 @@
"""Mark the Interaction Affordances of a Thing.
-See :ref:`wot_cc` for definitions of Interaction Affordance and other terms.
-
LabThings generates a :ref:`wot_td` to allow actions, properties, and
events to be used by client code. The descriptions of each "interaction
-affordance" rely on docstrings and Python type hints to provide a full
-description of the parameters, so it's important that you use these
-effectively.
+affordance" (see :ref:`wot_affordances`) rely on docstrings and Python
+type hints to provide a full description of the parameters, so it's
+important that you use these effectively.
If you have a complex datatype, it's recommended to use a `pydantic` model
to describe it - this is often the case for complicated properties or events.
@@ -25,9 +23,9 @@
----------
As with Actions, Properties can be declared by decorating either a function, or
-an attribute, with :deco:`.thing_property`. You can use the decorator either on
+an attribute, with :deco:`.property`. You can use the decorator either on
a function (in which case that
-function acts as the "getter" just like with Python's :deco`property` decorator).
+function acts as the "getter" just like with Python's :deco:`property` decorator).
Events
------
diff --git a/src/labthings_fastapi/exceptions.py b/src/labthings_fastapi/exceptions.py
index fc99dba9..c00c0adc 100644
--- a/src/labthings_fastapi/exceptions.py
+++ b/src/labthings_fastapi/exceptions.py
@@ -137,3 +137,11 @@ class LogConfigurationError(RuntimeError):
certain handlers and filters to be set up. This exception is raised if they
cannot be added, or if they are not present when they are needed.
"""
+
+
+class NoBlobManagerError(RuntimeError):
+ """Raised if an API route accesses Invocation outputs without a BlobIOContextDep.
+
+ Any access to an invocation output must have BlobIOContextDep as a dependency, as
+ the output may be a blob, and the blob needs this context to resolve its URL.
+ """
diff --git a/src/labthings_fastapi/properties.py b/src/labthings_fastapi/properties.py
index 3f66fc0e..9ce12150 100644
--- a/src/labthings_fastapi/properties.py
+++ b/src/labthings_fastapi/properties.py
@@ -1,6 +1,6 @@
"""Define properties of `.Thing` objects.
-:ref:`wot_properties` are attributes of a `.Thing` that may be read or written to
+:ref:`properties` are attributes of a `.Thing` that may be read or written to
over HTTP, and they are described in :ref:`gen_docs`. They are implemented with
a function `.property` (usually referenced as ``lt.property``), which is
intentionally similar to Python's built in `property`.
@@ -11,6 +11,7 @@
import labthings_fastapi as lt
+
class Counter(lt.Thing):
"A counter that knows what's remaining."
@@ -29,19 +30,19 @@ def remaining(self) -> int:
def remaining(self, value: int) -> None:
self.target = self.count + value
- The first two properties are simple variables: they may be read and assigned
- to, and will behave just like a regular variable. Their syntax is similar to
- `dataclasses` or `pydantic` in that `.property` is used as a "field specifier"
- to set options like the default value, and the type annotation is on the
- class attribute. Documentation is in strings immediately following the
- properties, which is understood by most automatic documentation tools.
+The first two properties are simple variables: they may be read and assigned
+to, and will behave just like a regular variable. Their syntax is similar to
+`dataclasses` or `pydantic` in that `.property` is used as a "field specifier"
+to set options like the default value, and the type annotation is on the
+class attribute. Documentation is in strings immediately following the
+properties, which is understood by most automatic documentation tools.
- ``remaining`` is defined using a "getter" function, meaning this code will
- be run each time ``counter.remaining`` is accessed. Its type will be the
- return type of the function, and its docstring will come from the function
- too. Setters with only a getter are read-only.
+``remaining`` is defined using a "getter" function, meaning this code will
+be run each time ``counter.remaining`` is accessed. Its type will be the
+return type of the function, and its docstring will come from the function
+too. Setters with only a getter are read-only.
- Adding a "setter" to properties is optional, and makes them read-write.
+Adding a "setter" to properties is optional, and makes them read-write.
"""
from __future__ import annotations
@@ -215,9 +216,9 @@ def property(
) -> Value | FunctionalProperty[Value]:
r"""Define a Property on a `.Thing`\ .
- This function may be used to define :ref:`wot_properties` in
+ 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:`.thing_property` documentation.
+ 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
diff --git a/src/labthings_fastapi/server/__init__.py b/src/labthings_fastapi/server/__init__.py
index 89ed16e9..cd42e0f4 100644
--- a/src/labthings_fastapi/server/__init__.py
+++ b/src/labthings_fastapi/server/__init__.py
@@ -36,6 +36,8 @@
# `_thing_servers` is used as a global from `ThingServer.__init__`
from ..outputs.blob import BlobDataManager
+__all__ = ["ThingServer"]
+
ThingSubclass = TypeVar("ThingSubclass", bound=Thing)
diff --git a/src/labthings_fastapi/thing.py b/src/labthings_fastapi/thing.py
index 9935a3c3..8d0164bf 100644
--- a/src/labthings_fastapi/thing.py
+++ b/src/labthings_fastapi/thing.py
@@ -1,8 +1,8 @@
"""A class to represent hardware or software Things.
The `.Thing` class enables most of the functionality of this library,
-and is the way in to most of its features. See :ref:`wot_cc` and :ref:`labthings_cc`
-for more.
+and is the way in to most of its features. See :ref:`structure`
+for how it fits with the rest of the library.
"""
from __future__ import annotations
@@ -29,12 +29,12 @@
from .utilities.introspection import get_summary, get_docstring
from .websockets import websocket_endpoint
from .exceptions import PropertyNotObservableError
+from .thing_server_interface import ThingServerInterface
if TYPE_CHECKING:
from .server import ThingServer
from .actions import ActionManager
- from .thing_server_interface import ThingServerInterface
_LOGGER = logging.getLogger(__name__)
@@ -49,17 +49,17 @@ class Thing:
Subclassing Notes
-----------------
- * ``__init__``: You should accept any arguments you need to configure the Thing
+ * ``__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
+ * ``__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:`.thing_action`
+ * Properties and Actions are defined using decorators: the :deco:`.thing_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.
@@ -67,7 +67,7 @@ class Thing:
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,
+ * `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
@@ -79,6 +79,9 @@ class Thing:
title: str
"""A human-readable description of the Thing"""
+ _thing_server_interface: ThingServerInterface
+ """Provide access to features of the server that this `.Thing` is attached to."""
+
def __init__(self, thing_server_interface: ThingServerInterface) -> None:
"""Initialise a Thing.
diff --git a/src/labthings_fastapi/utilities/introspection.py b/src/labthings_fastapi/utilities/introspection.py
index 8f90dc3f..daf83d11 100644
--- a/src/labthings_fastapi/utilities/introspection.py
+++ b/src/labthings_fastapi/utilities/introspection.py
@@ -64,7 +64,7 @@ def input_model_from_signature(
LabThings-FastAPI does not currently support actions that take
positional arguments, because this does not convert nicely into
- JSONSchema or Thing Description documents (see :ref:`wot_td`).
+ JSONSchema or Thing Description documents (see :ref:`gen_docs`).
:param func: the function to analyse.
:param remove_first_positional_arg: Remove the first argument from the