Skip to content

Commit c109ec7

Browse files
Merge pull request #144 from labthings/add-blob-manager-to-action-descriptor-list-invocations
Add BlobIOContextDep to ActionDescriptor.list_invocations so context vars are available
2 parents d2d5fe6 + cdccab0 commit c109ec7

File tree

3 files changed

+53
-3
lines changed

3 files changed

+53
-3
lines changed

src/labthings_fastapi/actions/__init__.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
InvocationCancelledError,
3636
invocation_logger,
3737
)
38-
from ..outputs.blob import BlobIOContextDep
38+
from ..outputs.blob import BlobIOContextDep, blobdata_to_url_ctx
3939

4040
if TYPE_CHECKING:
4141
# We only need these imports for type hints, so this avoids circular imports.
@@ -46,6 +46,14 @@
4646
"""The API route used to list `.Invocation` objects."""
4747

4848

49+
class NoBlobManagerError(RuntimeError):
50+
"""Raised if an API route accesses Invocation outputs without a BlobIOContextDep.
51+
52+
Any access to an invocation output must have BlobIOContextDep as a dependency, as
53+
the output may be a blob, and the blob needs this context to resolve its URL.
54+
"""
55+
56+
4957
class Invocation(Thread):
5058
"""A Thread subclass that retains output values and tracks progress.
5159
@@ -123,7 +131,23 @@ def id(self) -> uuid.UUID:
123131

124132
@property
125133
def output(self) -> Any:
126-
"""Return value of the Action. If the Action is still running, returns None."""
134+
"""Return value of the Action. If the Action is still running, returns None.
135+
136+
:raise NoBlobManagerError: If this is called in a context where the blob
137+
manager context variables are not available. This stops errors being raised
138+
later once the blob is returned and tries to serialise. If the errors
139+
happen during serialisation the stack-trace will not clearly identify
140+
the route with the missing dependency.
141+
"""
142+
try:
143+
blobdata_to_url_ctx.get()
144+
except LookupError as e:
145+
raise NoBlobManagerError(
146+
"An invocation output has been requested from a api route that "
147+
"doesn't have a BlobIOContextDep dependency. This dependency is needed "
148+
" for blobs to identify their url."
149+
) from e
150+
127151
with self._status_lock:
128152
return self._return_value
129153

src/labthings_fastapi/descriptors/action.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,9 @@ def start_action(
339339
),
340340
summary=f"All invocations of {self.name}.",
341341
)
342-
def list_invocations(action_manager: ActionManagerContextDep):
342+
def list_invocations(
343+
action_manager: ActionManagerContextDep, _blob_manager: BlobIOContextDep
344+
):
343345
return action_manager.list_invocations(self, thing)
344346

345347
def action_affordance(

tests/test_actions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@ def run(payload=None):
1818
return run
1919

2020

21+
def test_get_action_invocations():
22+
"""Test that running "get" on an action returns a list of invocations."""
23+
with TestClient(server.app) as client:
24+
# When we start the action has no invocations
25+
invocations_before = client.get("/thing/increment_counter").json()
26+
assert invocations_before == []
27+
# Start the action
28+
r = client.post("/thing/increment_counter")
29+
assert r.status_code in (200, 201)
30+
# Now it is started, there is a list of 1 dictionary containing the
31+
# invocation information.
32+
invocations_after = client.get("/thing/increment_counter").json()
33+
assert len(invocations_after) == 1
34+
assert isinstance(invocations_after, list)
35+
assert isinstance(invocations_after[0], dict)
36+
assert "status" in invocations_after[0]
37+
assert "id" in invocations_after[0]
38+
assert "action" in invocations_after[0]
39+
assert "href" in invocations_after[0]
40+
assert "timeStarted" in invocations_after[0]
41+
# Let the task finish before ending the test
42+
poll_task(client, r.json())
43+
44+
2145
def test_counter():
2246
with TestClient(server.app) as client:
2347
before_value = client.get("/thing/counter").json()

0 commit comments

Comments
 (0)