Skip to content

Commit 32a9cd1

Browse files
authored
Merge pull request #261 from labthings/app-config
Add application configuration that propagates to Things
2 parents 2e0eb20 + 5f58fda commit 32a9cd1

File tree

5 files changed

+67
-2
lines changed

5 files changed

+67
-2
lines changed

src/labthings_fastapi/server/__init__.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"""
88

99
from __future__ import annotations
10-
from typing import AsyncGenerator, Optional, TypeVar
10+
from typing import Any, AsyncGenerator, Optional, TypeVar
1111
from typing_extensions import Self
1212
import os
1313

@@ -65,6 +65,7 @@ def __init__(
6565
self,
6666
things: ThingsConfig,
6767
settings_folder: Optional[str] = None,
68+
application_config: Optional[Mapping[str, Any]] = None,
6869
) -> None:
6970
r"""Initialise a LabThings server.
7071
@@ -81,10 +82,18 @@ def __init__(
8182
arguments, and any connections to other `.Thing`\ s.
8283
:param settings_folder: the location on disk where `.Thing`
8384
settings will be saved.
85+
:param application_config: A mapping containing custom configuration for the
86+
application. This is not processed by LabThings. Each `.Thing` can access
87+
application. This is not processed by LabThings. Each `.Thing` can access
88+
this via the Thing-Server interface.
8489
"""
8590
self.startup_failure: dict | None = None
8691
configure_thing_logger() # Note: this is safe to call multiple times.
87-
self._config = ThingServerConfig(things=things, settings_folder=settings_folder)
92+
self._config = ThingServerConfig(
93+
things=things,
94+
settings_folder=settings_folder,
95+
application_config=application_config,
96+
)
8897
self.app = FastAPI(lifespan=self.lifespan)
8998
self._set_cors_middleware()
9099
self._set_url_for_middleware()
@@ -148,6 +157,15 @@ def things(self) -> Mapping[str, Thing]:
148157
"""
149158
return MappingProxyType(self._things)
150159

160+
@property
161+
def application_config(self) -> Mapping[str, Any] | None:
162+
"""Return the application configuration from the config file.
163+
164+
:return: The custom configuration as specified in the configuration
165+
file.
166+
"""
167+
return self._config.application_config
168+
151169
ThingInstance = TypeVar("ThingInstance", bound=Thing)
152170

153171
def things_by_class(self, cls: type[ThingInstance]) -> Sequence[ThingInstance]:

src/labthings_fastapi/server/config_model.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,18 @@ def thing_configs(self) -> Mapping[ThingName, ThingConfig]:
180180
description="The location of the settings folder.",
181181
)
182182

183+
application_config: dict[str, Any] | None = Field(
184+
default=None,
185+
description=(
186+
"""Any custom settings required by the application.
187+
188+
These settings will be available to any Things within the application via
189+
their ``application_config`` attribute. Any validation of the dictionary is
190+
the responsibility of application code.
191+
"""
192+
),
193+
)
194+
183195

184196
def normalise_things_config(things: ThingsConfig) -> Mapping[ThingName, ThingConfig]:
185197
r"""Ensure every Thing is defined by a `.ThingConfig` object.

src/labthings_fastapi/testing.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,14 @@ def _action_manager(self) -> ActionManager:
121121
"""
122122
raise NotImplementedError("MockThingServerInterface has no ActionManager.")
123123

124+
@property
125+
def application_config(self) -> None:
126+
"""Return an empty application configuration when mocking.
127+
128+
:return: None
129+
"""
130+
return None
131+
124132

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

src/labthings_fastapi/thing_server_interface.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44
from concurrent.futures import Future
5+
from copy import deepcopy
56
import os
67
from typing import (
78
TYPE_CHECKING,
@@ -156,6 +157,11 @@ def path(self) -> str:
156157
"""
157158
return self._get_server().path_for_thing(self.name)
158159

160+
@property
161+
def application_config(self) -> Mapping[str, Any] | None:
162+
"""The custom application configuration options from configuration."""
163+
return deepcopy(self._get_server().application_config)
164+
159165
def get_thing_states(self) -> Mapping[str, Any]:
160166
"""Retrieve metadata from all Things on the server.
161167

tests/test_thing.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,24 @@ def test_add_thing():
1313
"""Check that thing can be added to the server"""
1414
server = ThingServer({"thing": MyThing})
1515
assert isinstance(server.things["thing"], MyThing)
16+
17+
18+
def test_thing_can_access_application_config():
19+
"""Check that a thing can access its application config."""
20+
conf = {
21+
"things": {"thing1": MyThing, "thing2": MyThing},
22+
"application_config": {"foo": "bar", "mock": True},
23+
}
24+
25+
server = ThingServer.from_config(conf)
26+
thing1 = server.things["thing1"]
27+
thing2 = server.things["thing2"]
28+
29+
# Check both Things can access the application config
30+
thing1_config = thing1._thing_server_interface.application_config
31+
thing2_config = thing2._thing_server_interface.application_config
32+
assert thing1_config == {"foo": "bar", "mock": True}
33+
assert thing1_config == thing2_config
34+
# But that they are not the same dictionary, preventing mutations affecting
35+
# behaviour of another thing.
36+
assert thing1_config is not thing2_config

0 commit comments

Comments
 (0)