Skip to content

Commit 7098a3f

Browse files
committed
Merge tag 'v0.3.3' into v0.x.x
- A new module `frequenz.client.common.enum_proto` has been added, which provides a generic `enum_from_proto()` function to convert protobuf enums to Python enums. - Documentation for some `frequenz.client.common.microgrid.electrical_components.ElectricalComponentCategory` values was improved. - The metrics and components enums `.from_proto()` are deprecated, please use the new `enum_from_proto()` instead. - Classes to represent microgrid-related ID were added (`MicrogridId`, `EnterpriseId`, `ElectricalComponentId`, and `SensorId`). * tag 'v0.3.3': Update release notes Bump the minimum version of `frequenz-core` to 1.0.2 Deprecate uses of enum's `.from_proto()` Add a generic `enum_from_proto()` function Add the `HVAC` component category Add missing component categories Make enums unique Clear release notes Fix dependencies and prepare release notes for v0.3.2 release Clear release notes Add release notes Add microgrid-related ID types Fix warning filtering in pytest configuration
2 parents 082e389 + 2e59182 commit 7098a3f

File tree

8 files changed

+232
-12
lines changed

8 files changed

+232
-12
lines changed

pyproject.toml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ classifiers = [
2626
]
2727
requires-python = ">= 3.11, < 4"
2828
dependencies = [
29-
"typing-extensions >= 4.6.0, < 5",
29+
"typing-extensions >= 4.13.0, < 5",
3030
"frequenz-api-common @ git+https://github.com/frequenz-floss/frequenz-api-common.git@2e89add6a16d42b23612f0f791a499919f3738ed",
31+
"frequenz-core >= 1.0.2, < 2",
3132
]
3233
dynamic = ["version"]
3334

@@ -149,8 +150,11 @@ filterwarnings = [
149150
"error",
150151
"once::DeprecationWarning",
151152
"once::PendingDeprecationWarning",
152-
# We use a raw string (single quote) to avoid the need to escape special
153-
# chars as this is a regex
153+
# We ignore warnings about protobuf gencode version being one version older
154+
# than the current version, as this is supported by protobuf, and we expect to
155+
# have such cases. If we go too far, we will get a proper error anyways.
156+
# We use a raw string (single quotes) to avoid the need to escape special
157+
# characters as this is a regex.
154158
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
155159
]
156160
testpaths = ["tests", "src"]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Conversion of protobuf int enums to Python enums."""
5+
6+
import enum
7+
from typing import Literal, TypeVar, overload
8+
9+
EnumT = TypeVar("EnumT", bound=enum.Enum)
10+
"""A type variable that is bound to an enum."""
11+
12+
13+
@overload
14+
def enum_from_proto(
15+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[False]
16+
) -> EnumT: ...
17+
18+
19+
@overload
20+
def enum_from_proto(
21+
value: int, enum_type: type[EnumT], *, allow_invalid: Literal[True] = True
22+
) -> EnumT | int: ...
23+
24+
25+
def enum_from_proto(
26+
value: int, enum_type: type[EnumT], *, allow_invalid: bool = True
27+
) -> EnumT | int:
28+
"""Convert a protobuf int enum value to a python enum.
29+
30+
Example:
31+
```python
32+
import enum
33+
34+
from proto import proto_pb2 # Just an example. pylint: disable=import-error
35+
36+
@enum.unique
37+
class SomeEnum(enum.Enum):
38+
# These values should match the protobuf enum values.
39+
UNSPECIFIED = 0
40+
SOME_VALUE = 1
41+
42+
enum_value = enum_from_proto(proto_pb2.SomeEnum.SOME_ENUM_SOME_VALUE, SomeEnum)
43+
# -> SomeEnum.SOME_VALUE
44+
45+
enum_value = enum_from_proto(42, SomeEnum)
46+
# -> 42
47+
48+
enum_value = enum_from_proto(
49+
proto_pb2.SomeEnum.SOME_ENUM_UNKNOWN_VALUE, SomeEnum, allow_invalid=False
50+
)
51+
# -> ValueError
52+
```
53+
54+
Args:
55+
value: The protobuf int enum value.
56+
enum_type: The python enum type to convert to.
57+
allow_invalid: If `True`, return the value as an `int` if the value is not
58+
a valid member of the enum (this allows for forward-compatibility with new
59+
enum values defined in the protocol but not added to the Python enum yet).
60+
If `False`, raise a `ValueError` if the value is not a valid member of the
61+
enum.
62+
63+
Returns:
64+
The resulting python enum value if the protobuf value is known, otherwise
65+
the input value converted to a plain `int`.
66+
67+
Raises:
68+
ValueError: If `allow_invalid` is `False` and the value is not a valid member
69+
of the enum.
70+
"""
71+
try:
72+
return enum_type(value)
73+
except ValueError:
74+
if allow_invalid:
75+
return value
76+
raise

src/frequenz/client/common/metric/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33

44
"""Module to define the metrics used with the common client."""
55

6-
from enum import Enum
6+
import enum
77
from typing import Self
88

99
# pylint: disable=no-name-in-module
1010
from frequenz.api.common.v1.metrics.metric_sample_pb2 import Metric as PBMetric
11+
from typing_extensions import deprecated
1112

1213
# pylint: enable=no-name-in-module
1314

1415

15-
class Metric(Enum):
16+
@enum.unique
17+
class Metric(enum.Enum):
1618
"""List of supported metrics.
1719
1820
AC energy metrics information:
@@ -140,6 +142,7 @@ class Metric(Enum):
140142
SENSOR_IRRADIANCE = PBMetric.METRIC_SENSOR_IRRADIANCE
141143

142144
@classmethod
145+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
143146
def from_proto(cls, metric: PBMetric.ValueType) -> Self:
144147
"""Convert a protobuf Metric value to Metric enum.
145148

src/frequenz/client/common/microgrid/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,17 @@
22
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
33

44
"""Frequenz microgrid definition."""
5+
6+
from typing import final
7+
8+
from frequenz.core.id import BaseId
9+
10+
11+
@final
12+
class EnterpriseId(BaseId, str_prefix="EID"):
13+
"""A unique identifier for an enterprise account."""
14+
15+
16+
@final
17+
class MicrogridId(BaseId, str_prefix="MID"):
18+
"""A unique identifier for a microgrid."""

src/frequenz/client/common/microgrid/electrical_components/__init__.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
33

44
"""Defines the electrical components that can be used in a microgrid."""
5+
56
from __future__ import annotations
67

7-
from enum import Enum
8+
import enum
9+
from typing import final
810

911
# pylint: disable=no-name-in-module
1012
from frequenz.api.common.v1.microgrid.electrical_components.electrical_components_pb2 import (
@@ -16,11 +18,19 @@
1618
from frequenz.api.common.v1.microgrid.electrical_components.electrical_components_pb2 import (
1719
ElectricalComponentStateCode as PBElectricalComponentStateCode,
1820
)
21+
from frequenz.core.id import BaseId
22+
from typing_extensions import deprecated
1923

2024
# pylint: enable=no-name-in-module
2125

2226

23-
class ElectricalComponentCategory(Enum):
27+
@final
28+
class ElectricalComponentId(BaseId, str_prefix="CID"):
29+
"""A unique identifier for a microgrid electrical component."""
30+
31+
32+
@enum.unique
33+
class ElectricalComponentCategory(enum.Enum):
2434
"""Possible types of microgrid electrical component."""
2535

2636
UNSPECIFIED = (
@@ -67,20 +77,38 @@ class ElectricalComponentCategory(Enum):
6777
"""A relay, used for switching electrical circuits on and off."""
6878

6979
PRECHARGER = PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_PRECHARGER
70-
"""A precharger, used for preparing electrical circuits for switching on."""
80+
"""A precharge module.
81+
82+
Precharging involves gradually ramping up the DC voltage to prevent any
83+
potential damage to sensitive electrical components like capacitors.
84+
85+
While many inverters and batteries come equipped with in-built precharging
86+
mechanisms, some may lack this feature. In such cases, we need to use
87+
external precharging modules.
88+
"""
7189

7290
FUSE = PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_FUSE
7391
"""A fuse, used for protecting electrical circuits from overcurrent."""
7492

75-
TRANSFORMER = (
93+
VOLTAGE_TRANSFORMER = (
7694
PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_VOLTAGE_TRANSFORMER
7795
)
78-
"""A transformer, used for changing the voltage of electrical circuits."""
96+
"""A voltage transformer.
97+
98+
Voltage transformers are used to step up or step down the voltage, keeping
99+
the power somewhat constant by increasing or decreasing the current. If voltage is
100+
stepped up, current is stepped down, and vice versa.
101+
102+
Note:
103+
Voltage transformers have efficiency losses, so the output power is
104+
always less than the input power.
105+
"""
79106

80107
HVAC = PBElectricalComponentCategory.ELECTRICAL_COMPONENT_CATEGORY_HVAC
81108
"""A heating, ventilation, and air conditioning (HVAC) system."""
82109

83110
@classmethod
111+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
84112
def from_proto(
85113
cls, component_category: PBElectricalComponentCategory.ValueType
86114
) -> ElectricalComponentCategory:
@@ -105,7 +133,8 @@ def to_proto(self) -> PBElectricalComponentCategory.ValueType:
105133
return self.value
106134

107135

108-
class ElectricalComponentStateCode(Enum):
136+
@enum.unique
137+
class ElectricalComponentStateCode(enum.Enum):
109138
"""All possible states of a microgrid electrical component."""
110139

111140
UNSPECIFIED = (
@@ -207,6 +236,7 @@ class ElectricalComponentStateCode(Enum):
207236
"""The precharger circuit is closed, allowing full current to flow to the main circuit."""
208237

209238
@classmethod
239+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
210240
def from_proto(
211241
cls, component_state: PBElectricalComponentStateCode.ValueType
212242
) -> ElectricalComponentStateCode:
@@ -231,7 +261,8 @@ def to_proto(self) -> PBElectricalComponentStateCode.ValueType:
231261
return self.value
232262

233263

234-
class ElectricalComponentDiagnosticCode(Enum):
264+
@enum.unique
265+
class ElectricalComponentDiagnosticCode(enum.Enum):
235266
"""All diagnostics that can occur across electrical component categories."""
236267

237268
UNSPECIFIED = (
@@ -426,6 +457,7 @@ class ElectricalComponentDiagnosticCode(Enum):
426457
times."""
427458

428459
@classmethod
460+
@deprecated("Use `frequenz.client.common.enum_proto.enum_from_proto` instead.")
429461
def from_proto(
430462
cls, component_error_code: PBElectricalComponentDiagnosticCode.ValueType
431463
) -> ElectricalComponentDiagnosticCode:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Microgrid sensors."""
5+
6+
from typing import final
7+
8+
from frequenz.core.id import BaseId
9+
10+
11+
@final
12+
class SensorId(BaseId, str_prefix="SID"):
13+
"""A unique identifier for a microgrid sensor."""

tests/microgrid/test_ids.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for microgrid-related IDs."""
5+
6+
import pytest
7+
from frequenz.core.id import BaseId
8+
9+
from frequenz.client.common.microgrid import EnterpriseId, MicrogridId
10+
from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId
11+
from frequenz.client.common.microgrid.sensors import SensorId
12+
13+
14+
@pytest.mark.parametrize(
15+
"id_class, prefix",
16+
[
17+
(EnterpriseId, "EID"),
18+
(MicrogridId, "MID"),
19+
(ElectricalComponentId, "CID"),
20+
(SensorId, "SID"),
21+
],
22+
)
23+
def test_string_representation(id_class: type[BaseId], prefix: str) -> None:
24+
"""Test string representation of IDs."""
25+
_id = id_class(123)
26+
27+
assert str(_id) == f"{prefix}123"
28+
assert repr(_id) == f"{id_class.__name__}(123)"

tests/test_enum_proto.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# License: MIT
2+
# Copyright © 2025 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for enum_from_proto utility."""
5+
6+
import enum
7+
8+
import pytest
9+
10+
from frequenz.client.common.enum_proto import enum_from_proto
11+
12+
13+
class _TestEnum(enum.Enum):
14+
"""A test enum for enum_from_proto tests."""
15+
16+
ZERO = 0
17+
ONE = 1
18+
TWO = 2
19+
20+
21+
@pytest.mark.parametrize("enum_member", _TestEnum)
22+
def test_valid_allow_invalid(enum_member: _TestEnum) -> None:
23+
"""Test conversion of valid enum values."""
24+
assert enum_from_proto(enum_member.value, _TestEnum) == enum_member
25+
assert (
26+
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=True) == enum_member
27+
)
28+
29+
30+
@pytest.mark.parametrize("value", [42, -1])
31+
def test_invalid_allow_invalid(value: int) -> None:
32+
"""Test unknown values with allow_invalid=True (default)."""
33+
assert enum_from_proto(value, _TestEnum) == value
34+
assert enum_from_proto(value, _TestEnum, allow_invalid=True) == value
35+
36+
37+
@pytest.mark.parametrize("enum_member", _TestEnum)
38+
def test_valid_disallow_invalid(enum_member: _TestEnum) -> None:
39+
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
40+
assert (
41+
enum_from_proto(enum_member.value, _TestEnum, allow_invalid=False)
42+
== enum_member
43+
)
44+
45+
46+
@pytest.mark.parametrize("value", [42, -1])
47+
def test_invalid_disallow(value: int) -> None:
48+
"""Test unknown values with allow_invalid=False (should raise ValueError)."""
49+
with pytest.raises(ValueError, match=rf"^{value} is not a valid _TestEnum$"):
50+
enum_from_proto(value, _TestEnum, allow_invalid=False)

0 commit comments

Comments
 (0)