Skip to content

Commit 27cfff8

Browse files
committed
Add a Reconfigurable *mixin*
This class is mainly provided as a guideline on how to implement actors that can be reconfigured, so actor authors don't forget to do the basic steps to allow reconfiguration, and to have a common interface and pattern when creating reconfigurable actors. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 2baa6b8 commit 27cfff8

File tree

1 file changed

+140
-0
lines changed

1 file changed

+140
-0
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Mixin for reconfigurable classes."""
5+
6+
from __future__ import annotations
7+
8+
from typing import (
9+
TYPE_CHECKING,
10+
Any,
11+
Final,
12+
Generic,
13+
Literal,
14+
Sequence,
15+
assert_type,
16+
overload,
17+
)
18+
19+
import marshmallow
20+
from frequenz.channels import Receiver
21+
from marshmallow import Schema
22+
23+
from . import _global
24+
from ._base_schema import BaseConfigSchema
25+
from ._manager import ConfigManager
26+
from ._util import DataclassT
27+
28+
29+
class Reconfigurable(Generic[DataclassT]):
30+
"""A mixin for reconfigurable classes.
31+
32+
This mixin provides a method to initialize the configuration of a class. It is
33+
meant mostly as a guide on how to implement reconfigurable classes.
34+
35+
TODO: Example in module.
36+
"""
37+
38+
def __init__(
39+
self,
40+
*,
41+
config_key: str | Sequence[str],
42+
config_schema: type[DataclassT],
43+
config_manager: ConfigManager | None = None,
44+
**kwargs: Any,
45+
) -> None:
46+
"""Initialize this reconfigurable mixin.
47+
48+
Args:
49+
config_key: The key to use to retrieve the configuration from the
50+
configuration manager.
51+
config_schema: The schema to use to load the configuration.
52+
config_manager: The configuration manager to use. If `None`, the [global
53+
configuration manager][frequenz.sdk.config.get_config_manager] will be
54+
used.
55+
**kwargs: Additional keyword arguments to be passed to the parent class
56+
constructor. This is only provided to allow this class to be used as
57+
a mixin alonside other classes that require additional keyword
58+
arguments.
59+
"""
60+
self.config_schema: Final[type[DataclassT]] = config_schema
61+
if not isinstance(config_key, (str, tuple)):
62+
config_key = tuple(config_key)
63+
self.config_key: Final[str | tuple[str, ...]] = config_key
64+
if config_manager is None:
65+
config_manager = _global.get_config_manager()
66+
self.config_manager: Final[ConfigManager] = config_manager
67+
super().__init__(**kwargs)
68+
69+
@overload
70+
async def initialize_config( # noqa: DOC502
71+
self,
72+
*,
73+
skip_unchanged: bool = True,
74+
skip_none: Literal[True] = True,
75+
base_schema: type[Schema] | None = BaseConfigSchema,
76+
**marshmallow_load_kwargs: Any,
77+
) -> Receiver[DataclassT]: ...
78+
79+
@overload
80+
async def initialize_config( # noqa: DOC502
81+
self,
82+
*,
83+
skip_unchanged: bool = True,
84+
skip_none: Literal[False] = False,
85+
base_schema: type[Schema] | None = BaseConfigSchema,
86+
**marshmallow_load_kwargs: Any,
87+
) -> Receiver[DataclassT | None]: ...
88+
89+
# The noqa DOC502 is needed because we raise TimeoutError indirectly.
90+
async def initialize_config( # noqa: DOC502
91+
self,
92+
*,
93+
skip_unchanged: bool = True,
94+
skip_none: bool = True,
95+
base_schema: type[Schema] | None = BaseConfigSchema,
96+
**marshmallow_load_kwargs: Any,
97+
) -> Receiver[DataclassT] | Receiver[DataclassT | None]:
98+
"""Initialize the configuration.
99+
100+
Args:
101+
skip_unchanged: Whether to skip unchanged configurations.
102+
skip_none: Whether to skip sending the configuration if it is `None`. Only
103+
valid when `key` is not `None`.
104+
base_schema: The base schema to use for the configuration schema.
105+
**marshmallow_load_kwargs: Additional arguments to pass to
106+
`marshmallow.Schema.load`.
107+
108+
Returns:
109+
A receiver to get configuration updates,
110+
[ready][frequenz.channels.Receiver.ready] to receive the first
111+
configuration.
112+
113+
Raises:
114+
asyncio.TimeoutError: If the first configuration can't be received in time.
115+
"""
116+
if "unknown" not in marshmallow_load_kwargs:
117+
marshmallow_load_kwargs["unknown"] = marshmallow.EXCLUDE
118+
if skip_none:
119+
recv_not_none = await self.config_manager.new_receiver(
120+
wait_for_first=True,
121+
skip_unchanged=skip_unchanged,
122+
skip_none=True,
123+
key=self.config_key,
124+
schema=self.config_schema,
125+
base_schema=base_schema,
126+
**marshmallow_load_kwargs,
127+
)
128+
assert_type(recv_not_none, Receiver[DataclassT])
129+
return recv_not_none
130+
recv_none = await self.config_manager.new_receiver(
131+
wait_for_first=True,
132+
skip_unchanged=skip_unchanged,
133+
skip_none=False,
134+
key=self.config_key,
135+
schema=self.config_schema,
136+
base_schema=base_schema,
137+
**marshmallow_load_kwargs,
138+
)
139+
assert_type(recv_none, Receiver[DataclassT | None])
140+
return recv_none

0 commit comments

Comments
 (0)