Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/labthings_fastapi/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

from .base_descriptor import BaseDescriptor
from .logs import add_thing_log_destination
from .utilities import model_to_dict
from .utilities import model_to_dict, wrap_plain_types_in_rootmodel
from .invocations import InvocationModel, InvocationStatus, LogRecordModel
from .dependencies.invocation import NonWarningInvocationID
from .exceptions import (
Expand Down Expand Up @@ -159,8 +159,8 @@
"""
try:
blobdata_to_url_ctx.get()
except LookupError as e:
raise NoBlobManagerError(

Check warning on line 163 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

162-163 lines are not covered with tests
"An invocation output has been requested from a api route that "
"doesn't have a BlobIOContextDep dependency. This dependency is needed "
" for blobs to identify their url."
Expand Down Expand Up @@ -411,8 +411,8 @@
:param id: the unique ID of the action to retrieve.
:return: the `.Invocation` object.
"""
with self._invocations_lock:
return self._invocations[id]

Check warning on line 415 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

414-415 lines are not covered with tests

def list_invocations(
self,
Expand Down Expand Up @@ -477,7 +477,6 @@

@app.get(
ACTION_INVOCATIONS_PATH + "/{id}",
response_model=InvocationModel,
responses={404: {"description": "Invocation ID not found"}},
)
def action_invocation(
Expand Down Expand Up @@ -540,8 +539,8 @@
with self._invocations_lock:
try:
invocation: Any = self._invocations[id]
except KeyError as e:
raise HTTPException(

Check warning on line 543 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

542-543 lines are not covered with tests
status_code=404,
detail="No action invocation found with ID {id}",
) from e
Expand All @@ -554,7 +553,7 @@
invocation.output.response
):
# TODO: honour "accept" header
return invocation.output.response()

Check warning on line 556 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

556 line is not covered with tests
return invocation.output

@app.delete(
Expand All @@ -579,8 +578,8 @@
with self._invocations_lock:
try:
invocation: Any = self._invocations[id]
except KeyError as e:
raise HTTPException(

Check warning on line 582 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

581-582 lines are not covered with tests
status_code=404,
detail="No action invocation found with ID {id}",
) from e
Expand Down Expand Up @@ -683,7 +682,7 @@
remove_first_positional_arg=True,
ignore=[p.name for p in self.dependency_params],
)
self.output_model = return_type(func)
self.output_model = wrap_plain_types_in_rootmodel(return_type(func))
self.invocation_model = create_model(
f"{name}_invocation",
__base__=InvocationModel,
Expand All @@ -705,7 +704,7 @@
"""
super().__set_name__(owner, name)
if self.name != self.func.__name__:
raise ValueError(

Check warning on line 707 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

707 line is not covered with tests
f"Action name '{self.name}' does not match function name "
f"'{self.func.__name__}'",
)
Expand Down Expand Up @@ -854,14 +853,14 @@
try:
responses[200]["model"] = self.output_model
pass
except AttributeError:
print(f"Failed to generate response model for action {self.name}")

Check warning on line 857 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

856-857 lines are not covered with tests
# Add an additional media type if we may return a file
if hasattr(self.output_model, "media_type"):
responses[200]["content"][self.output_model.media_type] = {}

Check warning on line 860 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

860 line is not covered with tests
# Now we can add the endpoint to the app.
if thing.path is None:
raise NotConnectedToServerError(

Check warning on line 863 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

863 line is not covered with tests
"Can't add the endpoint without thing.path!"
)
app.post(
Expand Down Expand Up @@ -909,7 +908,7 @@
"""
path = path or thing.path
if path is None:
raise NotConnectedToServerError("Can't generate forms without a path!")

Check warning on line 911 in src/labthings_fastapi/actions.py

View workflow job for this annotation

GitHub Actions / coverage

911 line is not covered with tests
forms = [
Form[ActionOp](href=path + self.name, op=[ActionOp.invokeaction]),
]
Expand Down
9 changes: 4 additions & 5 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def action_wrapper(*args, **kwargs):
return action_wrapper


def assert_input_models_equivalent(model_a, model_b):
def assert_models_equivalent(model_a, model_b):
"""Check two basemodels are equivalent."""
keys = list(model_a.model_fields.keys())
assert list(model_b.model_fields.keys()) == keys
Expand Down Expand Up @@ -198,11 +198,10 @@ def decorated(
"""An example decorated action with type annotations."""
return 0.5

assert_input_models_equivalent(
Example.action.input_model, Example.decorated.input_model
assert_models_equivalent(Example.action.input_model, Example.decorated.input_model)
assert_models_equivalent(
Example.action.output_model, Example.decorated.output_model
)
assert Example.action.output_model == Example.decorated.output_model

# Check we can make the thing and it has a valid TD
example = create_thing_without_server(Example)
example.validate_thing_description()
Expand Down
22 changes: 22 additions & 0 deletions tests/test_numpy_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pydantic import BaseModel, RootModel
import numpy as np
from fastapi.testclient import TestClient

from labthings_fastapi.testing import create_thing_without_server
from labthings_fastapi.types.numpy import NDArray, DenumpifyingDict
Expand Down Expand Up @@ -70,6 +71,14 @@ class MyNumpyThing(lt.Thing):
def action_with_arrays(self, a: NDArray) -> NDArray:
return a * 2

@lt.action
def read_array(self) -> NDArray:
return np.array([1, 2])

@lt.property
def array_property(self) -> NDArray:
return np.array([3, 4, 5])


def test_thing_description():
"""Make sure the TD validates when numpy types are used."""
Expand Down Expand Up @@ -102,3 +111,16 @@ def test_rootmodel():
m = ArrayModel(root=input)
assert isinstance(m.root, np.ndarray)
assert (m.model_dump() == [0, 1, 2]).all()


def test_numpy_over_http():
"""Read numpy array over http."""
server = lt.ThingServer({"np_thing": MyNumpyThing})
with TestClient(server.app) as client:
np_thing_client = lt.ThingClient.from_url("/np_thing/", client=client)

arrayprop = np_thing_client.array_property
assert np.array_equal(np.asarray(arrayprop), np.array([3, 4, 5]))

array = np_thing_client.read_array()
assert np.array_equal(np.asarray(array), np.array([1, 2]))