labthings-fastapi 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/PKG-INFO +3 -2
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/pyproject.toml +15 -2
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/__init__.py +7 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/actions.py +136 -55
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/base_descriptor.py +61 -157
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/client/__init__.py +9 -9
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/endpoints.py +18 -18
- labthings_fastapi-0.2.0/src/labthings_fastapi/exceptions.py +383 -0
- labthings_fastapi-0.2.0/src/labthings_fastapi/global_lock.py +79 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/invocation_contexts.py +2 -2
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/logs.py +2 -2
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/outputs/blob.py +1 -1
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/outputs/mjpeg_stream.py +10 -10
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/properties.py +498 -95
- labthings_fastapi-0.2.0/src/labthings_fastapi/server/__init__.py +530 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/server/cli.py +13 -28
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/server/config_model.py +74 -19
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/testing.py +44 -9
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing.py +51 -26
- labthings_fastapi-0.2.0/src/labthings_fastapi/thing_class_settings.py +107 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_description/__init__.py +1 -1
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_server_interface.py +70 -8
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_slots.py +48 -48
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/utilities/__init__.py +6 -6
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/utilities/introspection.py +2 -2
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/websockets.py +4 -4
- labthings_fastapi-0.1.0/src/labthings_fastapi/exceptions.py +0 -204
- labthings_fastapi-0.1.0/src/labthings_fastapi/server/__init__.py +0 -366
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/.gitignore +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/LICENSE +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/README.md +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/example_things/__init__.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/invocations.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/middleware/__init__.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/middleware/url_for.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/notifications.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/outputs/__init__.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/py.typed +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/server/fallback.html.jinja +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/server/fallback.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_description/_model.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_description/td-json-schema-validation.json +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/thing_description/validation.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/types/__init__.py +0 -0
- {labthings_fastapi-0.1.0 → labthings_fastapi-0.2.0}/src/labthings_fastapi/types/numpy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labthings-fastapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: An implementation of LabThings using FastAPI
|
|
5
5
|
Project-URL: Homepage, https://github.com/labthings/labthings-fastapi
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/labthings/labthings-fastapi/issues
|
|
@@ -15,7 +15,7 @@ Requires-Dist: fastapi[all]~=0.135.0
|
|
|
15
15
|
Requires-Dist: httpx
|
|
16
16
|
Requires-Dist: jsonschema
|
|
17
17
|
Requires-Dist: numpy>=1.20
|
|
18
|
-
Requires-Dist: pydantic~=2.
|
|
18
|
+
Requires-Dist: pydantic~=2.13
|
|
19
19
|
Requires-Dist: typing-extensions
|
|
20
20
|
Requires-Dist: zeroconf>=0.28.0
|
|
21
21
|
Provides-Extra: dev
|
|
@@ -35,6 +35,7 @@ Requires-Dist: sphinx-autoapi; extra == 'dev'
|
|
|
35
35
|
Requires-Dist: sphinx-rtd-theme; extra == 'dev'
|
|
36
36
|
Requires-Dist: sphinx-toolbox; extra == 'dev'
|
|
37
37
|
Requires-Dist: sphinx>=7.2; extra == 'dev'
|
|
38
|
+
Requires-Dist: sphobjinv; extra == 'dev'
|
|
38
39
|
Requires-Dist: tomli; (python_version < '3.11') and extra == 'dev'
|
|
39
40
|
Requires-Dist: types-jsonschema; extra == 'dev'
|
|
40
41
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "labthings-fastapi"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Richard Bowman", email="richard.bowman@cantab.net" },
|
|
6
6
|
]
|
|
@@ -13,7 +13,7 @@ classifiers = [
|
|
|
13
13
|
"Operating System :: OS Independent",
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"pydantic ~= 2.
|
|
16
|
+
"pydantic ~= 2.13",
|
|
17
17
|
"numpy>=1.20",
|
|
18
18
|
"jsonschema",
|
|
19
19
|
"typing_extensions",
|
|
@@ -41,6 +41,7 @@ dev = [
|
|
|
41
41
|
"sphinx>=7.2",
|
|
42
42
|
"sphinx-autoapi",
|
|
43
43
|
"sphinx-toolbox",
|
|
44
|
+
"sphobjinv",
|
|
44
45
|
"tomli; python_version < '3.11'",
|
|
45
46
|
"codespell",
|
|
46
47
|
]
|
|
@@ -73,6 +74,18 @@ addopts = [
|
|
|
73
74
|
markers = [
|
|
74
75
|
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
75
76
|
]
|
|
77
|
+
filterwarnings = [
|
|
78
|
+
# This deliberately doesn't filter by class, because doing so confuses
|
|
79
|
+
# coverage - it results in labthings_fastapi being imported before
|
|
80
|
+
# the coverage plugin.
|
|
81
|
+
# Instead, we simply filter by message.
|
|
82
|
+
#
|
|
83
|
+
# There are many, many small Thing definitions in the test suite.
|
|
84
|
+
# These don't all opt in to new features for reasons of brevity: the
|
|
85
|
+
# DeprecationWarnings this would generate are unhelpful, so we suppress them
|
|
86
|
+
# with the line below.
|
|
87
|
+
"ignore:.get_validate_properties_on_set. will become .True. by default:DeprecationWarning",
|
|
88
|
+
]
|
|
76
89
|
|
|
77
90
|
[tool.ruff]
|
|
78
91
|
target-version = "py310"
|
|
@@ -13,15 +13,21 @@ imports and is intended to be imported using:
|
|
|
13
13
|
|
|
14
14
|
import labthings_fastapi as lt
|
|
15
15
|
|
|
16
|
+
|
|
17
|
+
The most important symbols are described in `lt` with links to the full API
|
|
18
|
+
documentation as appropriate.
|
|
19
|
+
|
|
16
20
|
The example code elsewhere in the documentation generally follows this
|
|
17
21
|
convention. Symbols in the top-level module mostly exist elsewhere in
|
|
18
22
|
the package, but should be imported from here as a preference, to ensure
|
|
19
23
|
code does not break if modules are rearranged.
|
|
24
|
+
|
|
20
25
|
"""
|
|
21
26
|
|
|
22
27
|
from .thing import Thing
|
|
23
28
|
from .thing_slots import thing_slot
|
|
24
29
|
from .thing_server_interface import ThingServerInterface
|
|
30
|
+
from .thing_class_settings import ThingClassSettings
|
|
25
31
|
from .properties import property, setting, DataProperty, DataSetting
|
|
26
32
|
from .actions import action
|
|
27
33
|
from .endpoints import endpoint
|
|
@@ -45,6 +51,7 @@ from .invocation_contexts import (
|
|
|
45
51
|
__all__ = [
|
|
46
52
|
"Thing",
|
|
47
53
|
"ThingServerInterface",
|
|
54
|
+
"ThingClassSettings",
|
|
48
55
|
"property",
|
|
49
56
|
"setting",
|
|
50
57
|
"DataProperty",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Actions module.
|
|
2
2
|
|
|
3
|
-
:ref:`actions` are represented by methods, decorated with the
|
|
3
|
+
:ref:`actions` are represented by methods, decorated with the `lt.action`
|
|
4
4
|
decorator.
|
|
5
5
|
|
|
6
6
|
See the :ref:`actions` documentation for a top-level overview of actions in
|
|
@@ -9,16 +9,18 @@ LabThings-FastAPI.
|
|
|
9
9
|
Developer notes
|
|
10
10
|
---------------
|
|
11
11
|
|
|
12
|
-
Currently much of the code related to Actions is in
|
|
12
|
+
Currently much of the code related to Actions is in `lt.action` and the
|
|
13
13
|
underlying `.ActionDescriptor`. This is likely to be refactored in the near
|
|
14
14
|
future.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from contextlib import contextmanager
|
|
18
20
|
import datetime
|
|
19
21
|
import logging
|
|
20
22
|
from collections import deque
|
|
21
|
-
from functools import partial
|
|
23
|
+
from functools import partial, wraps
|
|
22
24
|
import inspect
|
|
23
25
|
from threading import Thread, Lock
|
|
24
26
|
import uuid
|
|
@@ -29,6 +31,7 @@ from typing import (
|
|
|
29
31
|
Callable,
|
|
30
32
|
Concatenate,
|
|
31
33
|
Generic,
|
|
34
|
+
Literal,
|
|
32
35
|
Optional,
|
|
33
36
|
ParamSpec,
|
|
34
37
|
TypeVar,
|
|
@@ -36,9 +39,10 @@ from typing import (
|
|
|
36
39
|
)
|
|
37
40
|
from weakref import WeakSet
|
|
38
41
|
import weakref
|
|
39
|
-
from fastapi import FastAPI, HTTPException, Request, Body, BackgroundTasks
|
|
42
|
+
from fastapi import APIRouter, FastAPI, HTTPException, Request, Body, BackgroundTasks
|
|
40
43
|
from pydantic import BaseModel, create_model
|
|
41
44
|
|
|
45
|
+
|
|
42
46
|
from .middleware.url_for import URLFor
|
|
43
47
|
from .base_descriptor import (
|
|
44
48
|
BaseDescriptor,
|
|
@@ -49,6 +53,7 @@ from .logs import add_thing_log_destination
|
|
|
49
53
|
from .utilities import model_to_dict, wrap_plain_types_in_rootmodel
|
|
50
54
|
from .invocations import InvocationModel, InvocationStatus
|
|
51
55
|
from .exceptions import (
|
|
56
|
+
GlobalLockBusyError,
|
|
52
57
|
InvocationCancelledError,
|
|
53
58
|
InvocationError,
|
|
54
59
|
NotConnectedToServerError,
|
|
@@ -71,7 +76,7 @@ if TYPE_CHECKING:
|
|
|
71
76
|
from .thing import Thing
|
|
72
77
|
|
|
73
78
|
|
|
74
|
-
__all__ = ["
|
|
79
|
+
__all__ = ["Invocation", "ActionManager"]
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
ACTION_INVOCATIONS_PATH = "/action_invocations"
|
|
@@ -84,7 +89,7 @@ class Invocation(Thread):
|
|
|
84
89
|
`.Invocation` threads add several bits of functionality compared to the base
|
|
85
90
|
`threading.Thread`.
|
|
86
91
|
|
|
87
|
-
* They are instantiated with an `.ActionDescriptor` and a
|
|
92
|
+
* They are instantiated with an `.ActionDescriptor` and a `~lt.Thing`
|
|
88
93
|
rather than a target function (see ``__init__``).
|
|
89
94
|
* Each invocation is assigned a unique ``ID`` to allow it to be polled
|
|
90
95
|
over HTTP.
|
|
@@ -105,7 +110,7 @@ class Invocation(Thread):
|
|
|
105
110
|
|
|
106
111
|
:param action: provides the function that we run, as well as metadata
|
|
107
112
|
and type information. The descriptor is not bound to an object, so we
|
|
108
|
-
supply the
|
|
113
|
+
supply the `~lt.Thing` it's bound to when the function is run.
|
|
109
114
|
:param thing: is the object on which we are running the ``action``, i.e.
|
|
110
115
|
it is supplied to the function wrapped by ``action`` as the ``self``
|
|
111
116
|
argument.
|
|
@@ -187,7 +192,7 @@ class Invocation(Thread):
|
|
|
187
192
|
|
|
188
193
|
@property
|
|
189
194
|
def thing(self) -> Thing:
|
|
190
|
-
"""The
|
|
195
|
+
"""The `~lt.Thing` to which the action is bound, i.e. this is ``self``.
|
|
191
196
|
|
|
192
197
|
:raises RuntimeError: if the Thing no longer exists.
|
|
193
198
|
"""
|
|
@@ -250,7 +255,7 @@ class Invocation(Thread):
|
|
|
250
255
|
|
|
251
256
|
The code to be run is the function wrapped in the `.ActionDescriptor`
|
|
252
257
|
that is passed in as ``action``. Its arguments are the associated
|
|
253
|
-
|
|
258
|
+
`~lt.Thing` (the first argument, i.e. ``self``), the ``input`` model
|
|
254
259
|
(split into keyword arguments for each field), and any ``dependencies``
|
|
255
260
|
(also as keyword arguments).
|
|
256
261
|
|
|
@@ -287,19 +292,22 @@ class Invocation(Thread):
|
|
|
287
292
|
# occur.
|
|
288
293
|
raise RuntimeError("Cannot start an invocation without a Thing.")
|
|
289
294
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
295
|
+
# The action's `context_for_func` context manager will acquire the
|
|
296
|
+
# global lock if needed.
|
|
297
|
+
with action.context_for_func(thing):
|
|
298
|
+
with self._status_lock:
|
|
299
|
+
self._status = InvocationStatus.RUNNING
|
|
300
|
+
self._start_time = datetime.datetime.now()
|
|
301
|
+
action.emit_changed_event(self.thing, self._status.value)
|
|
294
302
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
# Actually run the action
|
|
304
|
+
ret = action.func(thing, **kwargs, **self.dependencies)
|
|
305
|
+
|
|
306
|
+
with self._status_lock:
|
|
307
|
+
self._return_value = ret
|
|
308
|
+
self._status = InvocationStatus.COMPLETED
|
|
309
|
+
action.emit_changed_event(self.thing, self._status.value)
|
|
298
310
|
|
|
299
|
-
with self._status_lock:
|
|
300
|
-
self._return_value = ret
|
|
301
|
-
self._status = InvocationStatus.COMPLETED
|
|
302
|
-
action.emit_changed_event(self.thing, self._status.value)
|
|
303
311
|
except InvocationCancelledError:
|
|
304
312
|
logger.info(f"Invocation {self.id} was cancelled.")
|
|
305
313
|
with self._status_lock:
|
|
@@ -308,9 +316,17 @@ class Invocation(Thread):
|
|
|
308
316
|
except Exception as e: # skipcq: PYL-W0703
|
|
309
317
|
# First log
|
|
310
318
|
if isinstance(e, InvocationError):
|
|
311
|
-
# Log without traceback
|
|
319
|
+
# Log without traceback for anticipated errors
|
|
312
320
|
logger.error(e)
|
|
321
|
+
elif (
|
|
322
|
+
isinstance(e, GlobalLockBusyError)
|
|
323
|
+
and self._status == InvocationStatus.PENDING
|
|
324
|
+
):
|
|
325
|
+
# The global lock timed out before the function started.
|
|
326
|
+
# In this case, don't print a traceback.
|
|
327
|
+
logger.warning(f"Global lock was busy: didn't run {action.name}.")
|
|
313
328
|
else:
|
|
329
|
+
# Other exceptions show up in the log with a traceback
|
|
314
330
|
logger.exception(e)
|
|
315
331
|
# Then set status
|
|
316
332
|
with self._status_lock:
|
|
@@ -360,7 +376,7 @@ class ActionManager:
|
|
|
360
376
|
|
|
361
377
|
:param action: provides the function that we run, as well as metadata
|
|
362
378
|
and type information. The descriptor is not bound to an object, so we
|
|
363
|
-
supply the
|
|
379
|
+
supply the `~lt.Thing` it's bound to when the function is run.
|
|
364
380
|
:param thing: is the object on which we are running the ``action``, i.e.
|
|
365
381
|
it is supplied to the function wrapped by ``action`` as the ``self``
|
|
366
382
|
argument.
|
|
@@ -407,8 +423,8 @@ class ActionManager:
|
|
|
407
423
|
:param action: filters out only the invocations of a particular
|
|
408
424
|
`.ActionDescriptor`. Note that if there are two Things
|
|
409
425
|
of the same subclass, filtering by action will return invocations
|
|
410
|
-
on either
|
|
411
|
-
:param thing: returns only invocations of actions on a particular
|
|
426
|
+
on either `~lt.Thing`.
|
|
427
|
+
:param thing: returns only invocations of actions on a particular `~lt.Thing`.
|
|
412
428
|
This will often be combined with filtering by ``action`` to give the
|
|
413
429
|
list of invocations returned by a GET request on an action endpoint.
|
|
414
430
|
:param request: is used to pass a `fastapi.Request` object to the
|
|
@@ -438,17 +454,18 @@ class ActionManager:
|
|
|
438
454
|
for k in to_delete:
|
|
439
455
|
del self._invocations[k]
|
|
440
456
|
|
|
441
|
-
def
|
|
442
|
-
"""
|
|
457
|
+
def router(self) -> APIRouter:
|
|
458
|
+
"""Create a FastAPI Router with action-related endpoints.
|
|
443
459
|
|
|
444
|
-
:
|
|
460
|
+
:return: a Router with all action-related endpoints.
|
|
445
461
|
"""
|
|
462
|
+
router = APIRouter()
|
|
446
463
|
|
|
447
|
-
@
|
|
464
|
+
@router.get(ACTION_INVOCATIONS_PATH, response_model=list[InvocationModel])
|
|
448
465
|
def list_all_invocations(request: Request) -> list[InvocationModel]:
|
|
449
466
|
return self.list_invocations(request=request)
|
|
450
467
|
|
|
451
|
-
@
|
|
468
|
+
@router.get(
|
|
452
469
|
ACTION_INVOCATIONS_PATH + "/{id}",
|
|
453
470
|
responses={404: {"description": "Invocation ID not found"}},
|
|
454
471
|
)
|
|
@@ -473,7 +490,7 @@ class ActionManager:
|
|
|
473
490
|
detail="No action invocation found with ID {id}",
|
|
474
491
|
) from e
|
|
475
492
|
|
|
476
|
-
@
|
|
493
|
+
@router.get(
|
|
477
494
|
ACTION_INVOCATIONS_PATH + "/{id}/output",
|
|
478
495
|
response_model=Any,
|
|
479
496
|
responses={
|
|
@@ -521,7 +538,7 @@ class ActionManager:
|
|
|
521
538
|
return invocation.output.response()
|
|
522
539
|
return invocation.output
|
|
523
540
|
|
|
524
|
-
@
|
|
541
|
+
@router.delete(
|
|
525
542
|
ACTION_INVOCATIONS_PATH + "/{id}",
|
|
526
543
|
response_model=None,
|
|
527
544
|
responses={
|
|
@@ -561,6 +578,8 @@ class ActionManager:
|
|
|
561
578
|
)
|
|
562
579
|
invocation.cancel()
|
|
563
580
|
|
|
581
|
+
return router
|
|
582
|
+
|
|
564
583
|
|
|
565
584
|
ACTION_POST_NOTICE = """
|
|
566
585
|
## Important note
|
|
@@ -651,9 +670,9 @@ class ActionDescriptor(
|
|
|
651
670
|
.. note::
|
|
652
671
|
Descriptors are instantiated once per class. This means that we cannot
|
|
653
672
|
assume there is only one action corresponding to this descriptor: there
|
|
654
|
-
may be multiple
|
|
655
|
-
why the host
|
|
656
|
-
and why observers, for example, must be keyed by the
|
|
673
|
+
may be multiple `~lt.Thing` instances with the same descriptor. That is
|
|
674
|
+
why the host `~lt.Thing` must be passed to many functions as an argument,
|
|
675
|
+
and why observers, for example, must be keyed by the `~lt.Thing` rather
|
|
657
676
|
than kept as a property of ``self``.
|
|
658
677
|
"""
|
|
659
678
|
|
|
@@ -662,10 +681,11 @@ class ActionDescriptor(
|
|
|
662
681
|
func: Callable[Concatenate[OwnerT, ActionParams], ActionReturn],
|
|
663
682
|
response_timeout: float = 1,
|
|
664
683
|
retention_time: float = 300,
|
|
684
|
+
use_global_lock: Literal[False] | None = None,
|
|
665
685
|
) -> None:
|
|
666
|
-
"""Create a new action descriptor.
|
|
686
|
+
r"""Create a new action descriptor.
|
|
667
687
|
|
|
668
|
-
The action descriptor wraps a method of a
|
|
688
|
+
The action descriptor wraps a method of a `~lt.Thing`. It may still be
|
|
669
689
|
called from Python in the same way, but it will also be added to the
|
|
670
690
|
HTTP API and automatic documentation.
|
|
671
691
|
|
|
@@ -680,6 +700,16 @@ class ActionDescriptor(
|
|
|
680
700
|
of the action.
|
|
681
701
|
:param retention_time: how long, in seconds, the action should be kept
|
|
682
702
|
for after it has completed.
|
|
703
|
+
:param use_global_lock: If the global lock is enabled,
|
|
704
|
+
this parameter may be used to opt out. See :ref:`global_locking`
|
|
705
|
+
for details of how the global lock is implemented.
|
|
706
|
+
|
|
707
|
+
If this parameter is `False` then the lock will not be acquired, even
|
|
708
|
+
if global locking is enabled. That is appropriate if the action does
|
|
709
|
+
not have side effects that would cause problems for other actions, or
|
|
710
|
+
if more nuanced locking behaviour is required meaning the lock is
|
|
711
|
+
acquired directly in the action code, for example using
|
|
712
|
+
`~lt.ThingServerInterface.hold_global_lock`\ .
|
|
683
713
|
"""
|
|
684
714
|
super().__init__()
|
|
685
715
|
self.func = func
|
|
@@ -689,6 +719,7 @@ class ActionDescriptor(
|
|
|
689
719
|
name = func.__name__ # this is checked in __set_name__
|
|
690
720
|
self.response_timeout = response_timeout
|
|
691
721
|
self.retention_time = retention_time
|
|
722
|
+
self.use_global_lock = use_global_lock
|
|
692
723
|
self.dependency_params = fastapi_dependency_params(func)
|
|
693
724
|
self.input_model = input_model_from_signature(
|
|
694
725
|
func,
|
|
@@ -722,28 +753,78 @@ class ActionDescriptor(
|
|
|
722
753
|
f"'{self.func.__name__}'",
|
|
723
754
|
)
|
|
724
755
|
|
|
756
|
+
@contextmanager
|
|
757
|
+
def context_for_func(self, obj: OwnerT) -> Iterator[None]:
|
|
758
|
+
"""Create the context in which ``func`` runs.
|
|
759
|
+
|
|
760
|
+
Currently, if global locking is enabled and this action hasn't opted out,
|
|
761
|
+
this context manager will hold the global lock for the duration of the
|
|
762
|
+
action.
|
|
763
|
+
|
|
764
|
+
This method is intended to create a hook for pre-run set-up and post-run
|
|
765
|
+
clean-up code that may be customised by `Thing` implementations in the future,
|
|
766
|
+
such as acquiring locks or other resources.
|
|
767
|
+
|
|
768
|
+
When an action is run from Python code as ``thing.action()`` this context
|
|
769
|
+
manager is entered before executing `func` bound to the `Thing` instance.
|
|
770
|
+
|
|
771
|
+
When an action is run from HTTP, this context manager is entered while the
|
|
772
|
+
action's status is ``pending`` and the status changes to ``running`` just
|
|
773
|
+
before `func` (the function decorated by `~lt.action`) runs. This allows
|
|
774
|
+
some slightly nicer error handling, for example not cluttering the log with
|
|
775
|
+
stack traces if an action can't start because the global lock is in use.
|
|
776
|
+
|
|
777
|
+
:param obj: The object on which the method is being called.
|
|
778
|
+
:return: the function, wrapped if necessary.
|
|
779
|
+
"""
|
|
780
|
+
with obj._thing_server_interface._optionally_hold_global_lock(
|
|
781
|
+
self.use_global_lock
|
|
782
|
+
):
|
|
783
|
+
yield
|
|
784
|
+
|
|
725
785
|
def instance_get(self, obj: OwnerT) -> Callable[ActionParams, ActionReturn]:
|
|
726
|
-
"""Return the function, bound to an object
|
|
786
|
+
"""Return the function, bound to an object and wrapped in a context manager.
|
|
787
|
+
|
|
788
|
+
Accessing a regular Python method returns the method bound to the instance,
|
|
789
|
+
i.e. the `self` argument is supplied.
|
|
727
790
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
791
|
+
LabThings Actions work the same way, but they also wrap the function in a
|
|
792
|
+
context manager. Currently, this context manager will handle acquiring the
|
|
793
|
+
global lock if required.
|
|
731
794
|
|
|
732
|
-
|
|
795
|
+
If locking is disabled, the context manager does nothing.
|
|
796
|
+
If locking is enabled, we return a wrapped function that holds the
|
|
797
|
+
global lock while the action runs.
|
|
798
|
+
|
|
799
|
+
.. note::
|
|
800
|
+
|
|
801
|
+
The returned function will hold a reference to both `obj` and `self`
|
|
802
|
+
(this descriptor). Given that accessing ``instance.method`` returns
|
|
803
|
+
a function that's already bound to the instance, this shouldn't cause
|
|
804
|
+
any problems.
|
|
805
|
+
|
|
806
|
+
:param obj: the `~lt.Thing` to which we are attached. This will be
|
|
733
807
|
the first argument supplied to the function wrapped by this
|
|
734
808
|
descriptor.
|
|
735
809
|
:return: the action function, bound to ``obj``.
|
|
736
810
|
"""
|
|
737
|
-
|
|
811
|
+
|
|
812
|
+
@wraps(self.func)
|
|
813
|
+
def wrapped(*args: Any, **kwargs: Any) -> Any: # noqa: DOC
|
|
814
|
+
"""Acquire the lock then run `func` with supplied arguments."""
|
|
815
|
+
with self.context_for_func(obj):
|
|
816
|
+
return self.func(*args, **kwargs)
|
|
817
|
+
|
|
818
|
+
return partial(wrapped, obj)
|
|
738
819
|
|
|
739
820
|
def _observers_set(self, obj: Thing) -> WeakSet:
|
|
740
821
|
"""Return a set used to notify changes.
|
|
741
822
|
|
|
742
|
-
Note that we need to supply the
|
|
823
|
+
Note that we need to supply the `~lt.Thing` we are looking at, as in
|
|
743
824
|
general there may be more than one object of the same type, and
|
|
744
825
|
descriptor instances are shared between all instances of their class.
|
|
745
826
|
|
|
746
|
-
:param obj: The
|
|
827
|
+
:param obj: The `~lt.Thing` on which the action is being observed.
|
|
747
828
|
|
|
748
829
|
:return: a weak set of callables to notify on changes to the action.
|
|
749
830
|
This is used by websocket endpoints.
|
|
@@ -762,7 +843,7 @@ class ActionDescriptor(
|
|
|
762
843
|
portal. Async code must not use the blocking portal as it can deadlock
|
|
763
844
|
the event loop.
|
|
764
845
|
|
|
765
|
-
:param obj: The
|
|
846
|
+
:param obj: The `~lt.Thing` on which the action is being observed.
|
|
766
847
|
:param status: The status of the action, to be sent to observers.
|
|
767
848
|
"""
|
|
768
849
|
obj._thing_server_interface.start_async_task_soon(
|
|
@@ -778,10 +859,10 @@ class ActionDescriptor(
|
|
|
778
859
|
It will send messages to each observer to notify them that something
|
|
779
860
|
has changed.
|
|
780
861
|
|
|
781
|
-
:param obj: The
|
|
862
|
+
:param obj: The `~lt.Thing` on which the action is defined.
|
|
782
863
|
`.ActionDescriptor` objects are unique to the class, but there may
|
|
783
|
-
be more than one
|
|
784
|
-
We use ``obj`` to look up the observers of the current
|
|
864
|
+
be more than one `~lt.Thing` attached to a server with the same class.
|
|
865
|
+
We use ``obj`` to look up the observers of the current `~lt.Thing`.
|
|
785
866
|
:param value: The action status to communicate to the observers.
|
|
786
867
|
"""
|
|
787
868
|
action_name = self.name
|
|
@@ -801,12 +882,12 @@ class ActionDescriptor(
|
|
|
801
882
|
application.
|
|
802
883
|
|
|
803
884
|
:param app: The `fastapi.FastAPI` app to add the endpoint to.
|
|
804
|
-
:param thing: The
|
|
805
|
-
mind that the descriptor may be used by more than one
|
|
885
|
+
:param thing: The `~lt.Thing` to which the action is attached. Bear in
|
|
886
|
+
mind that the descriptor may be used by more than one `~lt.Thing`,
|
|
806
887
|
so this can't be a property of the descriptor.
|
|
807
888
|
|
|
808
889
|
:raises NotConnectedToServerError: if the function is run before the
|
|
809
|
-
``thing`` has a ``path`` property. This is assigned when the
|
|
890
|
+
``thing`` has a ``path`` property. This is assigned when the `~lt.Thing`
|
|
810
891
|
is added to a server.
|
|
811
892
|
"""
|
|
812
893
|
|
|
@@ -901,15 +982,15 @@ class ActionDescriptor(
|
|
|
901
982
|
|
|
902
983
|
This function describes the Action in :ref:`wot_td` format.
|
|
903
984
|
|
|
904
|
-
:param thing: The
|
|
985
|
+
:param thing: The `~lt.Thing` to which the action is attached.
|
|
905
986
|
:param path: The prefix applied to all endpoints associated with the
|
|
906
|
-
|
|
987
|
+
`~lt.Thing`. This is the URL for the Thing Description. If it is
|
|
907
988
|
omitted, we use the ``path`` property of the ``thing``.
|
|
908
989
|
|
|
909
990
|
:return: An `.ActionAffordance` describing this action.
|
|
910
991
|
|
|
911
992
|
:raises NotConnectedToServerError: if the function is run before the
|
|
912
|
-
``thing`` has a ``path`` property. This is assigned when the
|
|
993
|
+
``thing`` has a ``path`` property. This is assigned when the `~lt.Thing`
|
|
913
994
|
is added to a server.
|
|
914
995
|
"""
|
|
915
996
|
path = path or thing.path
|
|
@@ -968,7 +1049,7 @@ def action(
|
|
|
968
1049
|
ActionDescriptor[ActionParams, ActionReturn, OwnerT],
|
|
969
1050
|
]
|
|
970
1051
|
):
|
|
971
|
-
r"""Mark a method of a
|
|
1052
|
+
r"""Mark a method of a `~lt.Thing` as a LabThings Action.
|
|
972
1053
|
|
|
973
1054
|
Methods decorated with :deco:`action` will be available to call
|
|
974
1055
|
over HTTP as actions. See :ref:`actions` for an introduction to the concept
|