1919_logger = logging .getLogger (__name__ )
2020
2121
22+ class InvalidValueForKeyError (ValueError ):
23+ """An error indicating that the value under the specified key is invalid."""
24+
25+ def __init__ (self , msg : str , * , key : str , value : Any ) -> None :
26+ """Initialize this error.
27+
28+ Args:
29+ msg: The error message.
30+ key: The key that has an invalid value.
31+ value: The actual value that was found that is not a mapping.
32+ """
33+ super ().__init__ (msg )
34+
35+ self .key = key
36+ """The key that has an invalid value."""
37+
38+ self .value = value
39+ """The actual value that was found that is not a mapping."""
40+
41+
2242class ConfigManager (BackgroundService ):
2343 """A manager for configuration files.
2444
@@ -106,17 +126,28 @@ def __repr__(self) -> str:
106126
107127 def new_receiver (
108128 self ,
129+ key : str ,
130+ / ,
109131 * ,
110132 skip_unchanged : bool = True ,
111- ) -> Receiver [Mapping [str , Any ] | None ]:
112- """Create a new receiver for the configuration.
133+ ) -> Receiver [Mapping [str , Any ] | InvalidValueForKeyError | None ]:
134+ """Create a new receiver for receiving the configuration for a particular key .
113135
114136 This method has a lot of features and functionalities to make it easier to
115137 receive configurations, but it also imposes some restrictions on how the
116138 configurations are received. If you need more control over the configuration
117139 receiver, you can create a receiver directly using
118140 [`config_channel.new_receiver()`][frequenz.sdk.config.ConfigManager.config_channel].
119141
142+ ### Filtering
143+
144+ Only the configuration under the `key` will be received by the receiver. If the
145+ `key` is not found in the configuration, the receiver will receive `None`.
146+
147+ The value under `key` must be another mapping, otherwise an error
148+ will be logged and a [`frequenz.sdk.config.InvalidValueForKeyError`][] instance
149+ will be sent to the receiver.
150+
120151 ### Skipping superfluous updates
121152
122153 If there is a burst of configuration updates, the receiver will only receive the
@@ -133,26 +164,72 @@ def new_receiver(
133164 ```
134165
135166 Args:
167+ key: The configuration key to be read by the receiver.
136168 skip_unchanged: Whether to skip sending the configuration if it hasn't
137169 changed compared to the last one received.
138170
139171 Returns:
140172 The receiver for the configuration.
141173 """
142- receiver = self .config_channel .new_receiver (name = str (self ), limit = 1 )
174+ receiver = self .config_channel .new_receiver (name = f"{ self } :{ key } " , limit = 1 )
175+
176+ def _get_key_or_error (
177+ config : Mapping [str , Any ]
178+ ) -> Mapping [str , Any ] | InvalidValueForKeyError | None :
179+ try :
180+ return _get_key (config , key )
181+ except InvalidValueForKeyError as error :
182+ return error
183+
184+ key_receiver = receiver .map (_get_key_or_error )
143185
144186 if skip_unchanged :
145- receiver = receiver .filter (WithPrevious (not_equal_with_logging ))
187+ return key_receiver .filter (WithPrevious (_not_equal_with_logging ))
146188
147- return receiver
189+ return key_receiver
148190
149191
150- def not_equal_with_logging (
151- old_config : Mapping [str , Any ], new_config : Mapping [str , Any ]
192+ def _not_equal_with_logging (
193+ old_value : Mapping [str , Any ] | InvalidValueForKeyError | None ,
194+ new_value : Mapping [str , Any ] | InvalidValueForKeyError | None ,
152195) -> bool :
153196 """Return whether the two mappings are not equal, logging if they are the same."""
154- if old_config == new_config :
197+ if old_value == new_value :
155198 _logger .info ("Configuration has not changed, skipping update" )
156- _logger .debug ("Old configuration being kept: %r" , old_config )
157199 return False
200+
201+ if isinstance (new_value , InvalidValueForKeyError ) and not isinstance (
202+ old_value , InvalidValueForKeyError
203+ ):
204+ _logger .error (
205+ "Configuration for key %r has an invalid value: %r" ,
206+ new_value .key ,
207+ new_value .value ,
208+ )
158209 return True
210+
211+
212+ def _get_key (config : Mapping [str , Any ], key : str ) -> Mapping [str , Any ] | None :
213+ """Get the value from the configuration under the specified key.
214+
215+ Args:
216+ config: The configuration to get the value from.
217+ key: The key to get the value for.
218+
219+ Returns:
220+ The value under the key, or `None` if the key is not found.
221+
222+ Raises:
223+ InvalidValueForKeyError: If the value under the key is not a mapping.
224+ """
225+ match config .get (key ):
226+ case None :
227+ return None
228+ case Mapping () as value :
229+ return value
230+ case invalid_value :
231+ raise InvalidValueForKeyError (
232+ f"Value for key { key !r} is not a mapping: { invalid_value !r} " ,
233+ key = key ,
234+ value = invalid_value ,
235+ )
0 commit comments