|
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 | +""" |
2 | 7 |
|
3 | 8 | from __future__ import annotations |
4 | 9 | import uuid |
|
9 | 14 |
|
10 | 15 |
|
11 | 16 | 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. |
13 | 35 |
|
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. |
16 | 38 | """ |
17 | 39 | return uuid.uuid4() |
18 | 40 |
|
19 | 41 |
|
20 | 42 | 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 | +""" |
21 | 51 |
|
22 | 52 |
|
23 | 53 | 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. |
25 | 61 |
|
26 | | - This will have a level of at least INFO. |
| 62 | + :return: A `logging.Logger` object specific to this invocation. |
27 | 63 | """ |
28 | 64 | logger = logging.getLogger(f"labthings_fastapi.actions.{id}") |
29 | 65 | logger.setLevel(logging.INFO) |
30 | 66 | return logger |
31 | 67 |
|
32 | 68 |
|
33 | 69 | 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 | +""" |
34 | 75 |
|
35 | 76 |
|
36 | 77 | class InvocationCancelledError(BaseException): |
37 | 78 | """An invocation was cancelled by the user. |
38 | 79 |
|
39 | 80 | Note that this inherits from BaseException so won't be caught by |
40 | 81 | `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. |
41 | 91 | """ |
42 | 92 |
|
43 | 93 |
|
44 | 94 | 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 | + |
45 | 102 | 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 | + """ |
46 | 108 | threading.Event.__init__(self) |
47 | 109 | self.invocation_id = id |
48 | 110 |
|
49 | 111 | 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 | + """ |
51 | 116 | if self.is_set(): |
52 | 117 | raise InvocationCancelledError("The action was cancelled.") |
53 | 118 |
|
54 | 119 | 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 | + """ |
56 | 124 | if self.wait(timeout): |
57 | 125 | raise InvocationCancelledError("The action was cancelled.") |
58 | 126 |
|
|
0 commit comments