Skip to content

Commit 079dae4

Browse files
committed
Create a top-level summary for the API docs, referenced as lt.* in the rest of the documentation.
1 parent 7baafe1 commit 079dae4

41 files changed

Lines changed: 881 additions & 417 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/source/actions.rst

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
Actions
44
=======
55

6-
Actions are the way `.Thing` objects are instructed to do things. In Python
7-
terms, any method of a `.Thing` that we want to be able to call over HTTP
8-
should be decorated as an Action, using `.action`.
6+
Actions are the way `~lt.Thing` objects are instructed to do things. In Python
7+
terms, any method of a `~lt.Thing` that we want to be able to call over HTTP
8+
should be decorated as an Action, using `lt.action`.
99

1010
This page gives an overview of how actions are implemented in LabThings-FastAPI.
1111
Our implementation should align with :ref:`wot_actions` as defined by the Web of Things standard.
@@ -19,7 +19,7 @@ invokes an action will return almost immediately with a ``201`` code, and a
1919
JSON payload that describes the invocation as an `.InvocationModel`. This includes
2020
a link ``href`` that can be polled to check the status of the invocation.
2121

22-
The HTTP implementation of `.ThingClient` first makes a ``POST`` request to
22+
The HTTP implementation of `~lt.ThingClient` first makes a ``POST`` request to
2323
invoke the action, then polls the invocation using the ``href`` supplied.
2424
Once the action has finished (i.e. its status is ``completed``, ``error``, or
2525
``cancelled``), its output (the return value) is retrieved and used as the
@@ -38,14 +38,14 @@ where Invocation-related HTTP endpoints are handled, including listing all the
3838
Running actions from other actions
3939
----------------------------------
4040

41-
If code running in a `.Thing` runs methods belonging either to that `.Thing`
42-
or to another `.Thing` on the same server, no new thread is created: the
41+
If code running in a `~lt.Thing` runs methods belonging either to that `~lt.Thing`
42+
or to another `~lt.Thing` on the same server, no new thread is created: the
4343
called action runs in the same thread as the calling action, just like any
4444
other Python code.
4545

4646
Action inputs and outputs
4747
-------------------------
48-
The code that implements an action is a method of a `.Thing`, meaning it is
48+
The code that implements an action is a method of a `~lt.Thing`, meaning it is
4949
a function. The input parameters are the function's arguments, and the output
5050
parameter is the function's return value. Type hints on both arguments and
5151
return value are used to document the action in the OpenAPI description and
@@ -58,7 +58,7 @@ Python construct giving access to the object on which the action is defined.
5858

5959
Logging from actions
6060
--------------------
61-
Action code should use `.Thing.logger` to log messages. This will be configured
61+
Action code should use `~lt.Thing.logger` to log messages. This will be configured
6262
to handle messages on a per-invocation basis and make them available when the action
6363
is queried over HTTP.
6464

@@ -75,7 +75,7 @@ If an action could run for a long time, it is useful to be able to cancel it
7575
cleanly. LabThings makes provision for this by allowing actions to be cancelled
7676
using a ``DELETE`` HTTP request. In order to allow an action to be cancelled,
7777
you must give LabThings opportunities to interrupt it. This is most often done
78-
by replacing a `time.sleep()` statement with `.cancellable_sleep()` which
78+
by replacing a `time.sleep()` statement with `lt.cancellable_sleep()` which
7979
is equivalent, but will raise an exception if the action is cancelled.
8080

8181
For more advanced options, see `.invocation_contexts` for detail.
@@ -90,15 +90,15 @@ such that the action code can use module-level symbols rather than needing
9090
to explicitly pass the logger and cancel hook as arguments to the action
9191
method.
9292

93-
Usually, you don't need to consider this mechanism: simply use `.Thing.logger`
94-
or `.cancellable_sleep` as explained above. However, if you want to run actions
93+
Usually, you don't need to consider this mechanism: simply use `~lt.Thing.logger`
94+
or `~lt.cancellable_sleep` as explained above. However, if you want to run actions
9595
outside of the server (for example, for testing purposes) or if you want to
9696
call one action from another action, but not share the cancellation signal
9797
or log, functions are provided in `.invocation_contexts` to manage this.
9898

9999
If you start a new thread from an action, code running in that thread will
100100
not have an invocation ID set in a context variable. A subclass of
101-
`threading.Thread` is provided to do this, `.ThreadWithInvocationID`\ .
101+
`threading.Thread` is provided to do this, `~lt.ThreadWithInvocationID`\ .
102102
This may be useful for test code, or if you wish to run actions in the
103103
background, with the option of cancelling them.
104104

docs/source/concurrency.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@ Concurrency in LabThings-FastAPI
55

66
One of the major challenges when controlling hardware, particularly from web frameworks, is concurrency. Most web frameworks assume resources (database connections, object storage, etc.) may be instantiated multiple times, and often initialise or destroy objects as required. In contrast, hardware can usually only be controlled from one process, and usually is initialised and shut down only once.
77

8-
LabThings-FastAPI instantiates each :class:`.Thing` only once, and runs all code in a thread. More specifically, each time an action is invoked via HTTP, a new thread is created to run the action. Similarly, each time a property is read or written, a new thread is created to run the property method. This means that :class:`.Thing` code should protect important variables or resources using locks from the `threading` module, and need not worry about writing asynchronous code.
8+
LabThings-FastAPI instantiates each :class:`~lt.Thing` only once, and runs all code in a thread. More specifically, each time an action is invoked via HTTP, a new thread is created to run the action. Similarly, each time a property is read or written, a new thread is created to run the property method. This means that :class:`~lt.Thing` code should protect important variables or resources using locks from the `threading` module, and need not worry about writing asynchronous code.
99

10-
In the case of properties, the HTTP response is only returned once the `.Thing` code is complete. Actions currently return a response immediately, and must be polled to determine when they have completed. This behaviour may change in the future, most likely with the introduction of a timeout to allow the client to choose between waiting for a response or polling.
10+
In the case of properties, the HTTP response is only returned once the `~lt.Thing` code is complete. Actions currently return a response immediately, and must be polled to determine when they have completed. This behaviour may change in the future, most likely with the introduction of a timeout to allow the client to choose between waiting for a response or polling.
1111

1212
Many of the functions that handle HTTP requests are asynchronous, running in an :mod:`anyio` event loop. This enables many HTTP connections to be handled at once with good efficiency. The `anyio documentation`_ describes the functions that link between async and threaded code. When the LabThings server is started, we create an :class:`anyio.from_thread.BlockingPortal`, which allows threaded code to run code asynchronously in the event loop.
1313

14-
An action can run async code using its server interface. See `.ThingServerInterface.start_async_task_soon` for details.
14+
An action can run async code using its server interface. See `~lt.ThingServerInterface.start_async_task_soon` for details.
1515

16-
There are relatively few occasions when `.Thing` code will need to consider this explicitly: more usually the blocking portal will be obtained by a LabThings function, for example the `.MJPEGStream` class.
16+
There are relatively few occasions when `~lt.Thing` code will need to consider this explicitly: more usually the blocking portal will be obtained by a LabThings function, for example the `.MJPEGStream` class.
1717

1818
.. _`anyio documentation`: https://anyio.readthedocs.io/en/stable/threads.html
1919

2020
Calling Things from other Things
2121
--------------------------------
2222

23-
When one `Thing` calls the actions or properties of another `.Thing`, either directly or via a `.DirectThingClient`, no new threads are spawned: the action or property is run in the same thread as the caller. This mirrors the behaviour of the `.ThingClient`, which blocks until the action or property is complete. See :doc:`using_things` for more details on how to call actions and properties of other Things.
23+
When one `Thing` calls the actions or properties of another `~lt.Thing`, either directly or via a `.DirectThingClient`, no new threads are spawned: the action or property is run in the same thread as the caller. This mirrors the behaviour of the `~lt.ThingClient`, which blocks until the action or property is complete. See :doc:`using_things` for more details on how to call actions and properties of other Things.
2424

2525
Invocations and concurrency
2626
---------------------------
2727

2828
Each time an action is run ("invoked" in :ref:`wot_cc`), we create a new thread to run it. This thread has a context variable set, such that ``lt.cancellable_sleep`` and ``lt.get_invocation_logger`` are aware of which invocation is currently running. If an action spawns a new thread (e.g. using `threading.Thread`\ ), this new thread will not have an invocation ID, and consequently the two invocation-specific functions mentioned will not work.
2929

30-
Usually, the best solution to this problem is to generate a new invocation ID for the thread. This means only the original action thread will receive cancellation events, and only the original action thread will log to the invocation logger. If the action is cancelled, you must cancel the background thread. This is the behaviour of `.ThreadWithInvocationID`\ .
30+
Usually, the best solution to this problem is to generate a new invocation ID for the thread. This means only the original action thread will receive cancellation events, and only the original action thread will log to the invocation logger. If the action is cancelled, you must cancel the background thread. This is the behaviour of `~lt.ThreadWithInvocationID`\ .
3131

3232
It is also possible to copy the current invocation ID to a new thread. This is often a bad idea, as it's ill-defined whether the exception will arise in the original thread or the new one if the invocation is cancelled. Logs from the two threads will also be interleaved. If it's desirable to log from the background thread, the invocation logger may safely be passed as an argument, rather than accessed via ``lt.get_invocation_logger``\ .

docs/source/conf.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import inspect
12
import logging
2-
import labthings_fastapi
3+
import labthings_fastapi as lt
34
import importlib.metadata
45

56
# Configuration file for the Sphinx documentation builder.
@@ -20,6 +21,7 @@
2021

2122
extensions = [
2223
"sphinx.ext.intersphinx",
24+
"sphinx.ext.autodoc",
2325
# "sphinx.ext.napoleon",
2426
# "autodoc2",
2527
"autoapi.extension",
@@ -66,10 +68,7 @@
6668
skipper_log.addHandler(logging.FileHandler("./skipper.log", mode="w"))
6769
skipper_log.setLevel(logging.DEBUG)
6870

69-
convenience_modules = {
70-
"labthings_fastapi": labthings_fastapi.__all__,
71-
}
72-
canonical_fq_names = [
71+
canonical_fq_names = {
7372
"labthings_fastapi.descriptors.action.ActionDescriptor",
7473
"labthings_fastapi.outputs.blob.BlobDataManager",
7574
"labthings_fastapi.invocations.InvocationModel",
@@ -79,7 +78,16 @@
7978
"labthings_fastapi.actions.ActionManager",
8079
"labthings_fastapi.descriptors.endpoint.EndpointDescriptor",
8180
"labthings_fastapi.utilities.introspection.EmptyObject",
82-
]
81+
}
82+
83+
# Everything in `labthings_fastapi` is documented elsewhere, so we
84+
# add all of those fq names to the list.
85+
top_level_objects = [getattr(lt, name) for name in lt.__all__]
86+
canonical_fq_names.update(
87+
f"{obj.__module__}.{obj.__qualname__}"
88+
for obj in top_level_objects
89+
if not inspect.ismodule(obj)
90+
)
8391

8492

8593
def unqual(name):
@@ -90,8 +98,6 @@ def unqual(name):
9098

9199
canonical_names = {unqual(n): n for n in canonical_fq_names}
92100

93-
skipper_log.info("Convenience modules: %s.", convenience_modules)
94-
95101

96102
def skip_public_api(app, what, name: str, obj, skip, options):
97103
"""Skip documenting members that are re-exported from the public API."""
@@ -100,10 +106,6 @@ def skip_public_api(app, what, name: str, obj, skip, options):
100106
if unqual in canonical_names and name != canonical_names[unqual]:
101107
skip = True
102108
return skip
103-
for conv, all in convenience_modules.items():
104-
if unqual in all and name != f"{conv}.{unqual}":
105-
skipper_log.warning(f"skipping {name}")
106-
skip = True
107109
return skip
108110

109111

docs/source/developer_notes/descriptors.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
Descriptors
44
===========
55

6-
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.
6+
Descriptors are a way to intercept attribute access on an object, and they are used extensively by LabThings to add functionality to `~lt.Thing` instances, while continuing to look like normal Python objects.
77

88
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.
99

10-
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`.
10+
In LabThings-FastAPI, descriptors are used to implement :ref:`actions` and :ref:`properties` on `~lt.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`.
1111

1212
.. _field_typing:
1313

@@ -22,7 +22,7 @@ Field typing
2222
my_property: int = lt.property(default=0)
2323
"""An integer property."""
2424
25-
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.
25+
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 `~lt.DataProperty` descriptor, we need to use the descriptor protocol to figure it out.
2626

2727
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.
2828

@@ -39,7 +39,7 @@ Descriptor implementation
3939
There are a few useful notes that relate to many of the descriptors in LabThings-FastAPI:
4040

4141
* Descriptor objects **may have more than one owner**. As a rule, a descriptor object
42-
(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.
42+
(e.g. an instance of `~lt.DataProperty`) is assigned to an attribute of one `~lt.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 `~lt.Thing`. This is why the `~lt.Thing` is passed to the ``__get__`` method: we should ensure that any values being remembered are keyed to the owning `~lt.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 `~lt.Thing`, without interfering with garbage collection.
4343

4444
The example below shows how this can go wrong.
4545

docs/source/examples.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Examples
22
========
33

4-
For a simple example `.Thing` and instructions on how to serve it, see the :ref:`tutorial`\ .
4+
For a simple example `~lt.Thing` and instructions on how to serve it, see the :ref:`tutorial`\ .
55

6-
For more complex examples, there is a useful collection of `.Thing` subclasses implemented as part of the `OpenFlexure Microscope`_ in the `things`_ submodule. This includes control of a camera and a translation stage, as well as various software `.Thing`\ s that integrate the two.
6+
For more complex examples, there is a useful collection of `~lt.Thing` subclasses implemented as part of the `OpenFlexure Microscope`_ in the `things`_ submodule. This includes control of a camera and a translation stage, as well as various software `~lt.Thing`\ s that integrate the two.
77

88
.. _`OpenFlexure Microscope`: https://gitlab.com/openflexure/openflexure-microscope-server/
99
.. _`things`: https://gitlab.com/openflexure/openflexure-microscope-server/-/tree/v3/src/openflexure_microscope_server/things/

docs/source/index.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ Documentation for LabThings-FastAPI
2020
wot_core_concepts.rst
2121
removed_features.rst
2222

23+
quick_reference.rst
2324
autoapi/index
2425
developer_notes/index.rst
2526

2627
`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 <https://gitlab.com/openflexure/openflexure-microscope-server/>`_. Key features and design aims are:
2728

28-
* The functionality of a unit of hardware or software is described using `.Thing` subclasses.
29-
* Methods and properties of `.Thing` subclasses may be added to the HTTP API and associated documentation using decorators.
29+
* The functionality of a unit of hardware or software is described using `~lt.Thing` subclasses.
30+
* Methods and properties of `~lt.Thing` subclasses may be added to the HTTP API and associated documentation using decorators.
3031
* Datatypes of action input/outputs and properties are defined with Python type hints.
31-
* Actions are decorated methods of a `.Thing` class. There is no need for separate schemas or endpoint definitions.
32+
* Actions are decorated methods of a `~lt.Thing` class. There is no need for separate schemas or endpoint definitions.
3233
* Properties are defined either as typed attributes (similar to `pydantic` or `dataclasses`) or with a `property`\ -like decorator.
3334
* 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.
3435
* Vocabulary and concepts are aligned with the `W3C Web of Things <https://www.w3.org/WoT/>`_ standard (see :ref:`wot_cc`)

0 commit comments

Comments
 (0)