Add way to get thing from test environment by type

This commit is contained in:
Julian Stirling 2025-12-17 18:57:44 +00:00
parent 6be260cb59
commit 0d09dc58da

View file

@ -3,13 +3,15 @@
import tempfile import tempfile
import time import time
from types import TracebackType from types import TracebackType
from typing import Any, Optional, Self from typing import Any, Mapping, Optional, Self, TypeVar
import requests import requests
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
import labthings_fastapi as lt import labthings_fastapi as lt
ThingSubclass = TypeVar("ThingSubclass", bound=lt.Thing)
ACTION_RUNNING_KEYWORDS = ["idle", "pending", "running"] ACTION_RUNNING_KEYWORDS = ["idle", "pending", "running"]
@ -29,7 +31,9 @@ class LabThingsTestEnv:
""" """
def __init__( def __init__(
self, things: dict[str, lt.Thing | str], settings_folder: Optional[str] = None self,
things: Mapping[str, lt.Thing | str],
settings_folder: Optional[str] = None,
) -> None: ) -> None:
"""Initialise the test environment. """Initialise the test environment.
@ -87,11 +91,49 @@ class LabThingsTestEnv:
if thing_name not in self.server.things: if thing_name not in self.server.things:
raise ValueError(f"No Thing named {thing_name}") raise ValueError(f"No Thing named {thing_name}")
def get_thing(self, thing_name: str) -> lt.Thing: def get_thing_by_name(self, thing_name: str) -> lt.Thing:
"""Get a Thing from the server by name.""" """Get a Thing from the server by name."""
self.check_thing_exists(thing_name) self.check_thing_exists(thing_name)
return self.server.things[thing_name] return self.server.things[thing_name]
def get_thing_by_type(self, thing_class: type[ThingSubclass]) -> ThingSubclass:
"""Get a thing by type.
:param thing_class: The subclass of thing to match.
:return: The Thing that matches the subclass.
:raises RuntimeError: If there are multiple things of the same type, or no
matching thing. If there are multiple things of this type use
``get_thing_by_name`` or ``get_all_things_by_type``.
"""
matching = self.get_all_things_by_type(thing_class)
n_things = len(matching)
if n_things == 0:
raise RuntimeError(f"No Thing of type {thing_class} on this server.")
if n_things > 1:
raise RuntimeError(
f"Cannot get Thing by type as there are {n_things} of type "
f"{thing_class} on this server."
)
return next(iter(matching.values()))
def get_all_things_by_type(
self, thing_class: type[ThingSubclass]
) -> Mapping[str, ThingSubclass]:
"""Get a dictionary of all things by matching a type.
:param thing_class: The subclass of thing to match.
:return: A dictionary of Things that match the subclass. If none match this
will be and empty dictionary.
"""
matching = {}
for thing_name, thing in self.server.things.items():
if isinstance(thing, thing_class):
matching[thing_name] = thing
return matching
def get_thing_client(self, thing_name: str) -> lt.ThingClient: def get_thing_client(self, thing_name: str) -> lt.ThingClient:
"""Get a ThingClient for a Thing by name.""" """Get a ThingClient for a Thing by name."""
self.check_thing_exists(thing_name) self.check_thing_exists(thing_name)
@ -102,7 +144,7 @@ class LabThingsTestEnv:
self, self,
thing_name: str, thing_name: str,
action_name: str, action_name: str,
action_kwargs: Optional[dict[str, Any]] = None, action_kwargs: Optional[Mapping[str, Any]] = None,
) -> requests.Response: ) -> requests.Response:
"""Start an action and return the server response. """Start an action and return the server response.
@ -117,7 +159,7 @@ class LabThingsTestEnv:
def poll_action( def poll_action(
self, response: requests.Response, interval: float = 0.01 self, response: requests.Response, interval: float = 0.01
) -> dict[str, Any]: ) -> Mapping[str, Any]:
"""Poll an action until it completes and return the final response data.""" """Poll an action until it completes and return the final response data."""
invocation_data = response.json() invocation_data = response.json()
@ -141,11 +183,11 @@ class LabThingsTestEnv:
response.raise_for_status() response.raise_for_status()
def _get_link(obj: dict[str, Any], rel: str) -> dict[str, Any]: def _get_link(obj: Mapping[str, Any], rel: str) -> Mapping[str, Any]:
"""Retrieve a link from an object's `links` list, by its `rel` attribute.""" """Retrieve a link from an object's `links` list, by its `rel` attribute."""
return next(link for link in obj["links"] if link["rel"] == rel) return next(link for link in obj["links"] if link["rel"] == rel)
def _invocation_href(invocation_data: dict[str, Any]) -> str: def _invocation_href(invocation_data: Mapping[str, Any]) -> str:
"""Get the invocation href from the invocation response data.""" """Get the invocation href from the invocation response data."""
return _get_link(invocation_data, "self")["href"] return _get_link(invocation_data, "self")["href"]