Skip to content

Commit 168ffdf

Browse files
authored
Deprecate UNSPECIFIED for simple enums (#221)
Deprecate `EnergyMarketCodeType.UNSPECIFIED`, `Metric.UNSPECIFIED` and `MetricConnectionCategory.UNSPECIFIED` and add new safe accessor methods to retrieve the valid enum values or raise an exception: - `DeliveryArea.get_code_type()` - `MetricConnection.get_category()` - `MetricSample.get_metric()` The `UNSPECIFIED` value is now stored as a low-level `0` integer, so users can still access it directly if needed, but regular use will raise an exception if trying to use an unspecified or unrecognized value. This should lead to much simpler happy path, while still allowing for the possibility of handling unspecified values in a controlled manner and making rare and error conditions more explicit. The `ElectricalComponentCategory` and `XxxType` enums are not affected by this change, as they do not have an `UNSPECIFIED` value, but will be completely removed in the future. Part of #223.
2 parents b19ab7f + 1d56ac7 commit 168ffdf

21 files changed

Lines changed: 547 additions & 61 deletions

RELEASE_NOTES.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,38 @@
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
* The `UNSPECIFIED` members in the following enums are now deprecated:
10+
11+
* `frequenz.client.common.grid.EnergyMarketCodeType`
12+
* `frequenz.client.common.metrics.Metric`
13+
* `frequenz.client.common.metrics.MetricConnectionCategory`
14+
15+
When loading these types from protobuf using dataclass-level converters (e.g., `delivery_area_from_proto`, `metric_sample_from_proto`), the low-level fields (`code_type`, `category`, `metric`) now store the raw integer `0` for unspecified values instead of the deprecated member. Unspecified values should be rare errors, so it is better to expose them only via the low-level interface.
16+
17+
Lower-level enum-level converters still return the deprecated member.
18+
19+
Users are encouraged to switch from direct field access to the new `get_*()` methods (see New Features), which provide a safer way to handle unspecified or unrecognized values.
1020

1121
## New Features
1222

23+
* Added new exceptions:
24+
25+
* `frequenz.client.common.ClientCommonError` as a base exception for the package.
26+
* `frequenz.client.common.UnspecifiedValueError` for unspecified values (raw `0` or the deprecated member).
27+
* `frequenz.client.common.UnrecognizedValueError` for enum members not yet recognized by the library. Carries the raw integer value in its `value` attribute.
28+
29+
* Added safe convenience getters that raise the new exceptions for unspecified or unrecognized values:
30+
31+
* `frequenz.client.common.grid.DeliveryArea.get_code_type()`
32+
* `frequenz.client.common.metrics.MetricConnection.get_category()`
33+
* `frequenz.client.common.metrics.MetricSample.get_metric()`
34+
1335
* Added a new `frequenz.client.common.types.Lifetime` type together with the `frequenz.client.common.types.proto.v1alpha8.lifetime_from_proto` conversion function.
36+
1437
* Added a new `frequenz.client.common.types.Location` type together with the `frequenz.client.common.types.proto.v1alpha8.location_from_proto` conversion function.
38+
1539
* Added a new `frequenz.client.common.microgrid.Microgrid` type, together with the `frequenz.client.common.microgrid.proto.v1alpha8.microgrid_from_proto` conversion function.
16-
* Added a new `frequenz.client.common.ClientCommonError` base exception and `UnspecifiedValueError` at the package root.
40+
1741
* Added a new `frequenz.client.common.microgrid.electrical_components` package, featuring a `ElectricalComponent` class hierarchy and its families (battery, inverter, EV charger, etc.), and `ElectricalComponentConnection`, including `v1alpha8` proto conversion functions.
1842
* Added a new `frequenz.client.common.microgrid.Microgrid` type with a raising `is_active()` method, together with the `frequenz.client.common.microgrid.proto.v1alpha8.microgrid_from_proto` conversion function.
1943

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ requires-python = ">= 3.11, < 4"
2828
dependencies = [
2929
"typing-extensions >= 4.13.0, < 5",
3030
"frequenz-api-common >= 0.8.4, < 1",
31-
"frequenz-core >= 1.0.2, < 2",
31+
"frequenz-core >= 1.3.0, < 2",
3232
"protobuf >= 6.33.6, < 8",
3333
]
3434
dynamic = ["version"]

src/frequenz/client/common/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33

44
"""Common code and utilities for Frequenz API clients."""
55

6-
from ._exception import ClientCommonError, UnspecifiedValueError
6+
from ._exception import (
7+
ClientCommonError,
8+
UnrecognizedValueError,
9+
UnspecifiedValueError,
10+
)
711

812
__all__ = [
913
"ClientCommonError",
14+
"UnrecognizedValueError",
1015
"UnspecifiedValueError",
1116
]

src/frequenz/client/common/_exception.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,36 @@ class ClientCommonError(Exception):
88
"""Base class for all errors raised by frequenz-client-common."""
99

1010

11+
class UnrecognizedValueError(ClientCommonError, ValueError):
12+
"""Raised when a semantic accessor sees an unrecognized protobuf value.
13+
14+
This happens when the server sets an enum value that this version of the
15+
client does not recognize, as opposed to an unspecified value (see
16+
[`UnspecifiedValueError`][..UnspecifiedValueError]). The raw
17+
unrecognized value is available as `value`.
18+
19+
This is also a ``ValueError`` for convenience.
20+
"""
21+
22+
def __init__(self, value: int, message: str | None = None) -> None:
23+
"""Initialize this error.
24+
25+
Args:
26+
value: The raw protobuf value that was not recognized.
27+
message: A custom error message. If `None`, a default message
28+
mentioning the unrecognized value is used.
29+
"""
30+
self.value: int = value
31+
super().__init__(
32+
message if message is not None else f"unrecognized enum value: {value!r}"
33+
)
34+
35+
1136
class UnspecifiedValueError(ClientCommonError, ValueError):
12-
"""Raised when a semantic accessor sees an unspecified or unknown protobuf value.
37+
"""Raised when a semantic accessor sees an unspecified protobuf value.
38+
39+
For a value that is set but not recognized by this client, see
40+
[`UnrecognizedValueError`][..UnrecognizedValueError].
1341
1442
This is also a [`ValueError`][] for convenience.
1543
"""

src/frequenz/client/common/grid/_delivery_area.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33

44
"""Delivery area information for the energy market."""
55

6-
import enum
6+
import warnings
77
from dataclasses import dataclass
8+
from typing import assert_never
89

10+
from frequenz.core.enum import Enum, deprecated_member, unique
911

10-
@enum.unique
11-
class EnergyMarketCodeType(enum.Enum):
12+
from .._exception import UnrecognizedValueError, UnspecifiedValueError
13+
14+
15+
@unique
16+
class EnergyMarketCodeType(Enum):
1217
"""The identification code types used in the energy market.
1318
1419
CodeType specifies the type of identification code used for uniquely
@@ -35,7 +40,11 @@ class EnergyMarketCodeType(enum.Enum):
3540
processing errors.
3641
"""
3742

38-
UNSPECIFIED = 0
43+
UNSPECIFIED = deprecated_member(
44+
0,
45+
"EnergyMarketCodeType.UNSPECIFIED is deprecated; use the `int` value `0` "
46+
"instead if you really need to check for this low-level value.",
47+
)
3948
"""Unspecified type. This value is a placeholder and should not be used."""
4049

4150
EUROPE_EIC = 1
@@ -74,6 +83,9 @@ class DeliveryArea:
7483
7584
This code could be extended in the future, in case an unknown code type is
7685
encountered, a plain integer value is used to represent it.
86+
87+
This is the lower-level, forward-compatible accessor; prefer
88+
`DeliveryArea.get_code_type()` to obtain a known member or a clear error.
7789
"""
7890

7991
def __str__(self) -> str:
@@ -85,3 +97,36 @@ def __str__(self) -> str:
8597
else self.code_type.name
8698
)
8799
return f"{code}[{code_type}]"
100+
101+
def get_code_type(self) -> EnergyMarketCodeType:
102+
"""Return the code type as a known enum member.
103+
104+
This is the higher-level accessor for the `code_type` attribute: it
105+
resolves the value to a known `EnergyMarketCodeType` member or raises a
106+
clear, catchable error.
107+
108+
Returns:
109+
The code type, when it is a known `EnergyMarketCodeType` member.
110+
111+
Raises:
112+
UnspecifiedValueError: If the code type is unspecified.
113+
UnrecognizedValueError: If the code type is a value not recognized by
114+
this version of the client. The raw value is available on the
115+
exception's `value` attribute.
116+
"""
117+
# Suppressing the deprecation warning can be removed when UNSPECIFIED is removed
118+
with warnings.catch_warnings():
119+
warnings.filterwarnings("ignore", category=DeprecationWarning)
120+
match self.code_type:
121+
case 0 | EnergyMarketCodeType.UNSPECIFIED:
122+
raise UnspecifiedValueError(f"code type of {self} is unspecified")
123+
case EnergyMarketCodeType() as code_type:
124+
return code_type
125+
case int() as code_type:
126+
raise UnrecognizedValueError(
127+
code_type,
128+
f"code type {code_type!r} of {self} is not a recognized "
129+
"EnergyMarketCodeType",
130+
)
131+
case unknown:
132+
assert_never(unknown)

src/frequenz/client/common/grid/proto/v1alpha8/_delivery_area.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,13 @@ def delivery_area_from_proto(message: delivery_area_pb2.DeliveryArea) -> Deliver
5757
if code is None:
5858
issues.append("code is empty")
5959

60-
code_type = energy_market_code_type_from_proto(message.code_type)
61-
if code_type is EnergyMarketCodeType.UNSPECIFIED:
60+
raw_code_type = message.code_type
61+
code_type: EnergyMarketCodeType | int = (
62+
raw_code_type
63+
if raw_code_type == 0
64+
else energy_market_code_type_from_proto(raw_code_type)
65+
)
66+
if raw_code_type == 0:
6267
issues.append("code_type is unspecified")
6368
elif isinstance(code_type, int):
6469
issues.append("code_type is unrecognized")

src/frequenz/client/common/metrics/_metric.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
"""Supported metrics for microgrid components."""
55

6-
import enum
6+
from frequenz.core import enum as core_enum
77

88

9-
@enum.unique
10-
class Metric(enum.Enum):
9+
@core_enum.unique
10+
class Metric(core_enum.Enum):
1111
"""List of supported metrics.
1212
1313
Metric units are as follows:
@@ -39,7 +39,11 @@ class Metric(enum.Enum):
3939
period, and therefore can be inconsistent.
4040
"""
4141

42-
UNSPECIFIED = 0
42+
UNSPECIFIED = core_enum.deprecated_member(
43+
0,
44+
"Metric.UNSPECIFIED is deprecated; use the `int` value `0` "
45+
"instead if you really need to check for this low-level value.",
46+
)
4347
"""The metric is unspecified (this should not be used)."""
4448

4549
DC_VOLTAGE = 1

src/frequenz/client/common/metrics/_sample.py

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
"""Definition to work with metric sample values."""
55

66
import enum
7+
import warnings
78
from collections.abc import Sequence
89
from dataclasses import dataclass
910
from datetime import datetime
1011
from typing import assert_never
1112

13+
from frequenz.core import enum as core_enum
14+
15+
from .._exception import UnrecognizedValueError, UnspecifiedValueError
1216
from ._bounds import Bounds
1317
from ._metric import Metric
1418

@@ -66,11 +70,15 @@ def __str__(self) -> str:
6670
return f"avg:{self.avg}{extra_str}"
6771

6872

69-
@enum.unique
70-
class MetricConnectionCategory(enum.Enum):
73+
@core_enum.unique
74+
class MetricConnectionCategory(core_enum.Enum):
7175
"""The categories of connections from which metrics can be obtained."""
7276

73-
UNSPECIFIED = 0
77+
UNSPECIFIED = core_enum.deprecated_member(
78+
0,
79+
"MetricConnectionCategory.UNSPECIFIED is deprecated; use the `int` value `0` "
80+
"instead if you really need to check for this low-level value.",
81+
)
7482
"""The connection category was not specified (do not use)."""
7583

7684
OTHER = 1
@@ -100,7 +108,13 @@ class MetricConnection:
100108
"""A connection from which a metric was obtained."""
101109

102110
category: MetricConnectionCategory | int
103-
"""The category of the connection from which the metric was obtained."""
111+
"""The category of the connection from which the metric was obtained.
112+
113+
This is the lower-level, forward-compatible accessor: it may hold a known
114+
`MetricConnectionCategory` member, the raw `int` `0` when the category is
115+
unspecified, or any other raw `int` not yet known to this client. Prefer
116+
`MetricConnection.get_category()` to obtain a known member or a clear error.
117+
"""
104118

105119
name: str | None = None
106120
"""The name of the specific connection from which the metric was obtained.
@@ -122,6 +136,40 @@ def __str__(self) -> str:
122136
return f"{category_name}({self.name})"
123137
return category_name
124138

139+
def get_category(self) -> MetricConnectionCategory:
140+
"""Return the connection category as a known enum member.
141+
142+
This is the higher-level accessor for the lower-level
143+
[`category`][frequenz.client.common.metrics.MetricConnection.category]
144+
field: it returns a known member or raises instead of exposing the raw
145+
sentinel `0` or an unknown `int`.
146+
147+
Returns:
148+
The category when it is a known `MetricConnectionCategory` member.
149+
150+
Raises:
151+
UnspecifiedValueError: If the category is unspecified (the raw value
152+
`0` or a member whose value is `0`).
153+
UnrecognizedValueError: If the category is an `int` this client does
154+
not recognize. The raw value is available on the error's `value`
155+
attribute.
156+
"""
157+
with warnings.catch_warnings():
158+
warnings.filterwarnings("ignore", category=DeprecationWarning)
159+
match self.category:
160+
case 0 | MetricConnectionCategory.UNSPECIFIED:
161+
raise UnspecifiedValueError("connection category is unspecified")
162+
case MetricConnectionCategory():
163+
return self.category
164+
case int():
165+
raise UnrecognizedValueError(
166+
self.category,
167+
f"connection category {self.category!r} is not a recognized "
168+
"MetricConnectionCategory",
169+
)
170+
case unexpected:
171+
assert_never(unexpected)
172+
125173

126174
@dataclass(frozen=True, kw_only=True)
127175
class MetricSample:
@@ -141,7 +189,13 @@ class MetricSample:
141189
"""The moment when the metric was sampled."""
142190

143191
metric: Metric | int
144-
"""The metric that was sampled."""
192+
"""The metric that was sampled.
193+
194+
This is the lower-level, forward-compatible accessor: it may hold a known
195+
`Metric` member, the raw `int` `0` when the metric is unspecified, or any
196+
other raw `int` not yet known to this client. Prefer
197+
`MetricSample.get_metric()` to obtain a known member or a clear error.
198+
"""
145199

146200
value: float | AggregatedMetricValue | None
147201
"""The value of the sampled metric."""
@@ -227,3 +281,36 @@ def as_single_value(
227281
return None
228282
case unexpected:
229283
assert_never(unexpected)
284+
285+
def get_metric(self) -> Metric:
286+
"""Return the sampled metric as a known enum member.
287+
288+
This is the higher-level accessor for the lower-level
289+
[`metric`][frequenz.client.common.metrics.MetricSample.metric] field: it
290+
returns a known member or raises instead of exposing the raw sentinel
291+
`0` or an unknown `int`.
292+
293+
Returns:
294+
The metric when it is a known `Metric` member.
295+
296+
Raises:
297+
UnspecifiedValueError: If the metric is unspecified (the raw value
298+
`0` or a member whose value is `0`).
299+
UnrecognizedValueError: If the metric is an `int` this client does
300+
not recognize. The raw value is available on the error's `value`
301+
attribute.
302+
"""
303+
with warnings.catch_warnings():
304+
warnings.filterwarnings("ignore", category=DeprecationWarning)
305+
match self.metric:
306+
case 0 | Metric.UNSPECIFIED:
307+
raise UnspecifiedValueError("sampled metric is unspecified")
308+
case Metric():
309+
return self.metric
310+
case int():
311+
raise UnrecognizedValueError(
312+
self.metric,
313+
f"sampled metric {self.metric!r} is not a recognized Metric",
314+
)
315+
case unexpected:
316+
assert_never(unexpected)

0 commit comments

Comments
 (0)