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
22 changes: 20 additions & 2 deletions src/labthings_fastapi/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""

from __future__ import annotations
from typing import AsyncGenerator, Optional, TypeVar
from typing import Any, AsyncGenerator, Optional, TypeVar
from typing_extensions import Self
import os

Expand Down Expand Up @@ -65,6 +65,7 @@ def __init__(
self,
things: ThingsConfig,
settings_folder: Optional[str] = None,
application_config: Optional[Mapping[str, Any]] = None,
) -> None:
r"""Initialise a LabThings server.

Expand All @@ -81,10 +82,18 @@ def __init__(
arguments, and any connections to other `.Thing`\ s.
:param settings_folder: the location on disk where `.Thing`
settings will be saved.
:param application_config: A mapping containing custom configuration for the
application. This is not processed by LabThings. Each `.Thing` can access
application. This is not processed by LabThings. Each `.Thing` can access
this via the Thing-Server interface.
"""
self.startup_failure: dict | None = None
configure_thing_logger() # Note: this is safe to call multiple times.
self._config = ThingServerConfig(things=things, settings_folder=settings_folder)
self._config = ThingServerConfig(
things=things,
settings_folder=settings_folder,
application_config=application_config,
)
self.app = FastAPI(lifespan=self.lifespan)
self._set_cors_middleware()
self._set_url_for_middleware()
Expand Down Expand Up @@ -148,6 +157,15 @@ def things(self) -> Mapping[str, Thing]:
"""
return MappingProxyType(self._things)

@property
def application_config(self) -> Mapping[str, Any] | None:
"""Return the application configuration from the config file.

:return: The custom configuration as specified in the configuration
file.
"""
return self._config.application_config

ThingInstance = TypeVar("ThingInstance", bound=Thing)

def things_by_class(self, cls: type[ThingInstance]) -> Sequence[ThingInstance]:
Expand Down
12 changes: 12 additions & 0 deletions src/labthings_fastapi/server/config_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,18 @@ def thing_configs(self) -> Mapping[ThingName, ThingConfig]:
description="The location of the settings folder.",
)

application_config: dict[str, Any] | None = Field(
default=None,
description=(
"""Any custom settings required by the application.

These settings will be available to any Things within the application via
their ``application_config`` attribute. Any validation of the dictionary is
the responsibility of application code.
"""
),
)


def normalise_things_config(things: ThingsConfig) -> Mapping[ThingName, ThingConfig]:
r"""Ensure every Thing is defined by a `.ThingConfig` object.
Expand Down
8 changes: 8 additions & 0 deletions src/labthings_fastapi/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ def _action_manager(self) -> ActionManager:
"""
raise NotImplementedError("MockThingServerInterface has no ActionManager.")

@property
def application_config(self) -> None:
"""Return an empty application configuration when mocking.

:return: None
"""
return None


ThingSubclass = TypeVar("ThingSubclass", bound="Thing")

Expand Down
6 changes: 6 additions & 0 deletions src/labthings_fastapi/thing_server_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations
from concurrent.futures import Future
from copy import deepcopy
import os
from typing import (
TYPE_CHECKING,
Expand Down Expand Up @@ -156,6 +157,11 @@ def path(self) -> str:
"""
return self._get_server().path_for_thing(self.name)

@property
def application_config(self) -> Mapping[str, Any] | None:
"""The custom application configuration options from configuration."""
return deepcopy(self._get_server().application_config)

def get_thing_states(self) -> Mapping[str, Any]:
"""Retrieve metadata from all Things on the server.

Expand Down
21 changes: 21 additions & 0 deletions tests/test_thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,24 @@ def test_add_thing():
"""Check that thing can be added to the server"""
server = ThingServer({"thing": MyThing})
assert isinstance(server.things["thing"], MyThing)


def test_thing_can_access_application_config():
"""Check that a thing can access its application config."""
conf = {
"things": {"thing1": MyThing, "thing2": MyThing},
"application_config": {"foo": "bar", "mock": True},
}

server = ThingServer.from_config(conf)
thing1 = server.things["thing1"]
thing2 = server.things["thing2"]

# Check both Things can access the application config
thing1_config = thing1._thing_server_interface.application_config
thing2_config = thing2._thing_server_interface.application_config
assert thing1_config == {"foo": "bar", "mock": True}
assert thing1_config == thing2_config
# But that they are not the same dictionary, preventing mutations affecting
# behaviour of another thing.
assert thing1_config is not thing2_config