Skip to content

Commit 407e562

Browse files
committed
Started work on invocation dependencies.
1 parent cd73933 commit 407e562

File tree

2 files changed

+77
-8
lines changed

2 files changed

+77
-8
lines changed

NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ switched to using `pydoclint` directly, and configured it in `pyproject.toml`. I
3939
* `client/in_server.py` needs a fairly thorough rewrite. It is probably efficient to do this after client code generation is merged.
4040
* `outputs/mjpeg_stream.py`: review the locks and stream termination
4141
* `tests/` still uses `poll_task` from `temp_client.py`. We should use `poll_invocation` from `client` instead (it's identical). We should also review how `TestClient` is used and perhaps make more use of the client module. This might want to wait until after code generation is implemented, as that will substantially change the client module.
42+
* `blocking_portal` should probably just be a property of `.Thing`.

src/labthings_fastapi/dependencies/invocation.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
"""FastAPI dependency for an invocation ID"""
1+
"""FastAPI dependencies for invocation-specific resources.
2+
3+
There are a number of LabThings-FastAPI features that are specific to each
4+
invocation of an action. These may be accessed using the dependencies_ in
5+
this module.
6+
"""
27

38
from __future__ import annotations
49
import uuid
@@ -9,50 +14,113 @@
914

1015

1116
def invocation_id() -> uuid.UUID:
12-
"""Return a UUID for an action invocation
17+
"""Generate a UUID for an action invocation.
18+
19+
This is for use as a FastAPI dependency (see dependencies_).
20+
21+
Because `fastapi` only evaluates each dependency once per HTTP
22+
request, the `.UUID` we generate here is available to all of
23+
the dependencies declared by the ``POST`` endpoint that starts
24+
an action.
25+
26+
Any dependency that has a parameter with the type hint
27+
`.InvocationID` will be supplied with the ID we generate
28+
here, it will be consistent within one HTTP request, and will
29+
be unique for each request (i.e. for each invocation of the
30+
action).
31+
32+
This dependency is used by the `.InvocationLogger`, `.CancelHook`
33+
and other resources to ensure they all have the same ID, even
34+
before the `.Invocation` object has been created.
1335
14-
This is for use as a FastAPI dependency, to allow other dependencies to
15-
access the invocation ID. Useful for e.g. file management.
36+
:return: A unique ID for the current HTTP request, i.e. for this
37+
invocation of an action.
1638
"""
1739
return uuid.uuid4()
1840

1941

2042
InvocationID = Annotated[uuid.UUID, Depends(invocation_id)]
43+
"""A FastAPI dependency that supplies the invocation ID.
44+
45+
This calls `.invocation_id` to generate a new `.UUID`. It is used
46+
to supply the invocation ID when an action is invoked.
47+
48+
Any dependency of an action may access the invocation ID by
49+
using this dependency.
50+
"""
2151

2252

2353
def invocation_logger(id: InvocationID) -> logging.Logger:
24-
"""Retrieve a logger object for an action invocation
54+
"""Make a logger object for an action invocation.
55+
56+
This function should be used as a dependency for an action, and
57+
will supply a logger that's specific to each invocation of that
58+
action. This is how `.Invocation.log` is generated.
59+
60+
:param id: The Invocation ID, supplied as a FastAPI dependency.
2561
26-
This will have a level of at least INFO.
62+
:return: A `logging.Logger` object specific to this invocation.
2763
"""
2864
logger = logging.getLogger(f"labthings_fastapi.actions.{id}")
2965
logger.setLevel(logging.INFO)
3066
return logger
3167

3268

3369
InvocationLogger = Annotated[logging.Logger, Depends(invocation_logger)]
70+
"""A FastAPI dependency supplying a logger for the action invocation.
71+
72+
This calls `.invocation_logger` to generate a logger for the current
73+
invocation. For details of how to use dependencies, see dependencies_.
74+
"""
3475

3576

3677
class InvocationCancelledError(BaseException):
3778
"""An invocation was cancelled by the user.
3879
3980
Note that this inherits from BaseException so won't be caught by
4081
`except Exception`, it must be handled specifically.
82+
83+
Action code may want to handle cancellation gracefully. This
84+
exception should be propagated if the action's status should be
85+
reported as ``cancelled``, or it may be handled so that the
86+
action finishes, returns a value, and is marked as ``completed``.
87+
88+
If this exception is handled, the `.CancelEvent` should be reset
89+
to allow another `.InvocationCancelledError` to be raised if the
90+
invocation receives a second cancellation signal.
4191
"""
4292

4393

4494
class CancelEvent(threading.Event):
95+
"""An Event subclass that enables cancellation of actions.
96+
97+
This `threading.Event` subclass adds methods to raise
98+
`.InvocationCancelledError` exceptions if the invocation is cancelled,
99+
usually by a ``DELETE`` request to the invocation's URL.
100+
"""
101+
45102
def __init__(self, id: InvocationID):
103+
"""Initialise the cancellation event.
104+
105+
:param id: The invocation ID, annotated as a dependency so it is
106+
supplied automatically by FastAPI.
107+
"""
46108
threading.Event.__init__(self)
47109
self.invocation_id = id
48110

49111
def raise_if_set(self):
50-
"""Raise a CancelledError if the event is set"""
112+
"""Raise a CancelledError if the event is set.
113+
114+
:raises InvocationCancelledError: if the event has been cancelled.
115+
"""
51116
if self.is_set():
52117
raise InvocationCancelledError("The action was cancelled.")
53118

54119
def sleep(self, timeout: float):
55-
"""Sleep for a given time in seconds, but raise an exception if cancelled"""
120+
"""Sleep for a given time in seconds, but raise an exception if cancelled.
121+
122+
:raises InvocationCancelledError: if the event has been cancelled.
123+
"""
56124
if self.wait(timeout):
57125
raise InvocationCancelledError("The action was cancelled.")
58126

0 commit comments

Comments
 (0)