Skip to content

Commit 5727457

Browse files
authored
Merge pull request #194 from labthings/drop-dependencies-feedback
Drop dependencies This PR replaces dependencies with several new features: `lt.thing_slot`, a `ThingServerInterface`, and `lt.cancellable_sleep`. These are all described in the documentation in more detail. Dependencies and DirectThingClient are still in the codebase: these should be deprecated for the next release and removed in the following one.
2 parents 5f510d8 + 62e2730 commit 5727457

Some content is hidden

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

70 files changed

+4522
-1308
lines changed

docs/source/actions.rst

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Actions
55

66
Actions are the way `.Thing` objects are instructed to do things. In Python
77
terms, any method of a `.Thing` that we want to be able to call over HTTP
8-
should be decorated as an Action, using :deco:`.thing_action`.
8+
should be decorated as an Action, using `.thing_action`.
99

1010
This page gives an overview of how actions are implemented in LabThings-FastAPI.
1111
:ref:`wot_cc` includes a section on :ref:`wot_actions` that introduces the general concept.
@@ -58,6 +58,51 @@ The first is ``self`` (the first positional argument), which is always the
5858
supply resources needed by the action. Most often, this is a way of accessing
5959
other `.Things` on the same server.
6060

61+
.. action_logging:
62+
Logging from actions
63+
--------------------
64+
Action code should use `.Thing.logger` to log messages. This will be configured
65+
to handle messages on a per-invocation basis and make them available when the action
66+
is queried over HTTP.
67+
68+
This may be used to display status updates to the user when an action takes
69+
a long time to run, or it may simply be a helpful debugging aid.
70+
71+
See :mod:`.logs` for details of how this is implemented.
72+
73+
.. action_cancellation:
74+
Cancelling actions
75+
------------------
76+
If an action could run for a long time, it is useful to be able to cancel it
77+
cleanly. LabThings makes provision for this by allowing actions to be cancelled
78+
using a ``DELETE`` HTTP request. In order to allow an action to be cancelled,
79+
you must give LabThings opportunities to interrupt it. This is most often done
80+
by replacing a `time.sleep()` statement with `.cancellable_sleep()` which
81+
is equivalent, but will raise an exception if the action is cancelled.
82+
83+
For more advanced options, see `.invocation_contexts` for detail.
84+
85+
.. invocation_context:
86+
Invocation contexts
87+
-------------------
88+
Cancelling actions and capturing their logs requires action code to use a
89+
specific logger and check for cancel events. This is done using `contextvars`
90+
such that the action code can use module-level symbols rather than needing
91+
to explicitly pass the logger and cancel hook as arguments to the action
92+
method.
93+
94+
Usually, you don't need to consider this mechanism: simply use `.Thing.logger`
95+
or `.cancellable_sleep` as explained above. However, if you want to run actions
96+
outside of the server (for example, for testing purposes) or if you want to
97+
call one action from another action, but not share the cancellation signal
98+
or log, functions are provided in `.invocation_contexts` to manage this.
99+
100+
If you start a new thread from an action, code running in that thread will
101+
not have an invocation ID set in a context variable. A subclass of
102+
`threading.Thread` is provided to do this, `.ThreadWithInvocationID`\ .
103+
This may be useful for test code, or if you wish to run actions in the
104+
background, with the option of cancelling them.
105+
61106
Raising exceptions
62107
------------------
63108
If an action raises an unhandled exception, the action will terminate with an Error

docs/source/concurrency.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ In the case of properties, the HTTP response is only returned once the `.Thing`
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 obtain the blocking portal using the `~labthings_fastapi.dependencies.blocking_portal.BlockingPortal` dependency, i.e. by declaring an argument of that type. This avoids referring to the blocking portal through a global variable, which could lead to confusion if there are multiple event loops, e.g. during testing.
14+
An action can run async code using its server interface. See `.ThingServerInterface.start_async_task_soon` for details.
1515

1616
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.
1717

@@ -22,3 +22,11 @@ Calling Things from other Things
2222

2323
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.
2424

25+
Invocations and concurrency
26+
---------------------------
27+
28+
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.
29+
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`\ .
31+
32+
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/dependencies/dependencies.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,19 @@
33
Dependencies
44
============
55

6+
.. warning::
7+
8+
The use of dependencies is now deprecated. See :ref:`thing_slots` and `.ThingServerInterface` for a more intuitive way to access that functionality.
9+
610
LabThings makes use of the powerful "dependency injection" mechanism in FastAPI. You can see the `FastAPI documentation`_ for more information. In brief, FastAPI dependencies are annotated types that instruct FastAPI to supply certain function arguments automatically. This removes the need to set up resources at the start of a function, and ensures everything the function needs is declared and typed clearly. The most common use for dependencies in LabThings is where an action needs to make use of another `.Thing` on the same `.ThingServer`.
711

812
Inter-Thing dependencies
913
------------------------
1014

15+
.. warning::
16+
17+
These dependencies are deprecated - see :ref:`thing_slots` instead.
18+
1119
Simple actions depend only on their input parameters and the `.Thing` on which they are defined. However, it's quite common to need something else, for example accessing another `.Thing` instance on the same LabThings server. There are two important principles to bear in mind here:
1220

1321
* Other `.Thing` instances should be accessed using a `.DirectThingClient` subclass if possible. This creates a wrapper object that should work like a `.ThingClient`, meaning your code should work either on the server or in a client script. This makes the code much easier to debug.

docs/source/dependencies/example.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import labthings_fastapi as lt
66
from labthings_fastapi.example_things import MyThing
77

8-
MyThingClient = lt.deps.direct_thing_client_class(MyThing, "/mything/")
8+
MyThingClient = lt.deps.direct_thing_client_class(MyThing, "mything")
99
MyThingDep = Annotated[MyThingClient, Depends()]
1010

1111

@@ -18,9 +18,12 @@ def increment_counter(self, my_thing: MyThingDep) -> None:
1818
my_thing.increment_counter()
1919

2020

21-
server = lt.ThingServer()
22-
server.add_thing(MyThing(), "/mything/")
23-
server.add_thing(TestThing(), "/testthing/")
21+
server = lt.ThingServer(
22+
{
23+
"mything": MyThing,
24+
"testthing": TestThing,
25+
}
26+
)
2427

2528
if __name__ == "__main__":
2629
import uvicorn

docs/source/index.rst

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ Documentation for LabThings-FastAPI
77

88
quickstart/quickstart.rst
99
wot_core_concepts.rst
10-
lt_core_concepts.rst
10+
structure.rst
1111
tutorial/index.rst
1212
examples.rst
1313
actions.rst
14+
thing_slots.rst
1415
dependencies/dependencies.rst
1516
blobs.rst
1617
concurrency.rst
@@ -19,26 +20,21 @@ Documentation for LabThings-FastAPI
1920

2021
autoapi/index
2122

22-
`labthings-fastapi` implements a Web of Things interface for laboratory hardware using Python. This is a ground-up rewrite of python-labthings_, replacing Flask 1 and Marshmallow with FastAPI and Pydantic. It is the underlying framework for v3 of the `OpenFlexure Microscope software <https://gitlab.com/openflexure/openflexure-microscope-server/>`_.
23+
`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:
2324

24-
`labthings-fastapi` aims to simplify the process of making laboratory instruments available via an HTTP API. Key features and design aims are below:
25-
26-
* Functionality together in `Thing` subclasses, which represent units of hardware or software (see :doc:`wot_core_concepts`)
27-
* Methods and properties of `Thing` subclasses may be added to the HTTP API and Thing Description using decorators
25+
* The functionality of a unit of hardware or software is described using `.Thing` subclasses.
26+
* Methods and properties of `.Thing` subclasses may be added to the HTTP API and associated documentation using decorators.
27+
* Datatypes of action input/outputs and properties are defined with Python type hints.
28+
* Actions are decorated methods of a `.Thing` class. There is no need for separate schemas or endpoint definitions.
29+
* Properties are defined either as typed attributes (similar to `pydantic` or `dataclasses`) or with a `property`\ -like decorator.
30+
* 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.
2831
* Vocabulary and concepts are aligned with the `W3C Web of Things <https://www.w3.org/WoT/>`_ standard (see :doc:`wot_core_concepts`)
29-
- Things are classes, with properties and actions defined exactly once
30-
- Thing Descriptions are automatically generated, and validated with `pydantic`
31-
- OpenAPI documentation is automatically generated by FastAPI
32-
* We follow FastAPI_'s lead and try to use standard Python features to minimise unnecessary code
33-
- Datatypes of action input/outputs and properties are defined with Python type hints
34-
- Actions are defined exactly once, as a method of a `Thing` class
35-
- Properties and actions are declared using decorators (or descriptors if that's preferred)
36-
- FastAPI_ "Dependency injection" is used to manage relationships between Things and dependency on the server
37-
* Lifecycle and concurrency are appropriate for hardware: `Thing` code is always run in a thread, and each `Thing` is instantiated and shut down only once.
38-
- Starlette (used by FastAPI) can handle requests asynchronously - this improves performance and enables websockets and other long-lived connections.
39-
- `Thing` code is still, for now, threaded. In the future it may become possible to us other concurrency models in `Thing` code.
40-
41-
Compared to `python-labthings`_, this framework updates dependencies, shrinks the codebase, and simplifies the API (see :doc:`lt_core_concepts`).
32+
33+
Previous version
34+
----------------
35+
36+
This is a ground-up rewrite of python-labthings_, replacing Flask 1 and Marshmallow with FastAPI and Pydantic.
37+
Compared to `python-labthings`_, this framework updates dependencies, shrinks the codebase, and simplifies the API (see :doc:`lt_structure`).
4238
* FastAPI more or less completely eliminates OpenAPI generation code from our codebase
4339
* Marshmallow schemas and endpoint classes are replaced with Python type hints, eliminating double- or triple-definition of actions and their inputs/outputs.
4440
* Thing Description generation is very much simplified by the new structure (multiple Things instead of one massive Thing with many extensions)

docs/source/lt_core_concepts.rst

Lines changed: 0 additions & 61 deletions
This file was deleted.

docs/source/quickstart/counter.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,7 @@ def slowly_increase_counter(self) -> None:
3131
if __name__ == "__main__":
3232
import uvicorn
3333

34-
server = lt.ThingServer()
35-
36-
# The line below creates a TestThing instance and adds it to the server
37-
server.add_thing(TestThing(), "/counter/")
34+
server = lt.ThingServer({"counter": TestThing})
3835

3936
# We run the server using `uvicorn`:
4037
uvicorn.run(server.app, port=5000)

0 commit comments

Comments
 (0)