Skip to content

Commit d336a20

Browse files
Add application configuration that propagates to Things
1 parent 51d2349 commit d336a20

File tree

6 files changed

+66
-2
lines changed

6 files changed

+66
-2
lines changed

src/labthings_fastapi/server/__init__.py

Lines changed: 16 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
@@ -84,7 +85,11 @@ def __init__(
8485
"""
8586
self.startup_failure: dict | None = None
8687
configure_thing_logger() # Note: this is safe to call multiple times.
87-
self._config = ThingServerConfig(things=things, settings_folder=settings_folder)
88+
self._config = ThingServerConfig(
89+
things=things,
90+
settings_folder=settings_folder,
91+
application_config=application_config,
92+
)
8893
self.app = FastAPI(lifespan=self.lifespan)
8994
self._set_cors_middleware()
9095
self._set_url_for_middleware()
@@ -148,6 +153,15 @@ def things(self) -> Mapping[str, Thing]:
148153
"""
149154
return MappingProxyType(self._things)
150155

156+
@property
157+
def application_config(self) -> Mapping[str, Any] | None:
158+
"""Return the application configuration from the config file.
159+
160+
:return: The custom configuration as specified in the configuration
161+
file.
162+
"""
163+
return self._config.application_config
164+
151165
ThingInstance = TypeVar("ThingInstance", bound=Thing)
152166

153167
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`` property. 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.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from typing import TYPE_CHECKING, Any, Optional
1010
from typing_extensions import Self
1111
from collections.abc import Mapping
12+
from copy import deepcopy
1213
import logging
1314
import os
1415
import json
@@ -82,6 +83,8 @@ class Thing:
8283
_thing_server_interface: ThingServerInterface
8384
"""Provide access to features of the server that this `.Thing` is attached to."""
8485

86+
application_config: Mapping[str, Any] | None
87+
8588
def __init__(self, thing_server_interface: ThingServerInterface) -> None:
8689
"""Initialise a Thing.
8790
@@ -98,6 +101,9 @@ def __init__(self, thing_server_interface: ThingServerInterface) -> None:
98101
`.create_thing_without_server` which generates a mock interface.
99102
"""
100103
self._thing_server_interface = thing_server_interface
104+
# Create a deepcopy of the configuration so if one Thing mutates the config
105+
# it cannot propagate.
106+
self.application_config = deepcopy(thing_server_interface.application_config)
101107
self._disable_saving_settings: bool = False
102108

103109
@property

src/labthings_fastapi/thing_server_interface.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ def path(self) -> str:
156156
"""
157157
return self._get_server().path_for_thing(self.name)
158158

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

tests/test_thing.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,22 @@ 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+
assert thing1.application_config == {"foo": "bar", "mock": True}
31+
assert thing1.application_config == thing2.application_config
32+
# But that they are not the same dictionary, preventing mutations affecting
33+
# behaviour of another thing.
34+
assert thing1.application_config is not thing2.application_config

0 commit comments

Comments
 (0)