Skip to content

Commit 73674eb

Browse files
committed
Add metric from/to protobuf v1alpha8 conversion
To make sure we can decouple the clients from the underlying protocol, we need to make sure we always use explicit conversion between Python enums and protobuf enums. This is especially important to be able to support multiple versions of a protobuf message, for example an upcoming v1alpha9 Metric enum, so downstream project can migrate to new versions in a backwards-compatible way. Signed-off-by: Leandro Lucarella <luca-frequenz@llucax.com>
1 parent e3c0feb commit 73674eb

4 files changed

Lines changed: 135 additions & 3 deletions

File tree

RELEASE_NOTES.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
<!-- Here goes a general summary of what this release is about -->
66

7-
## Upgrading
7+
## Deprecation
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
- Converting `Metric` enums from/to protobuf directly is deprecated and will be dropped in the next breaking release.
10+
11+
You should switch to use the new conversion functions in `frequenz.client.common.metrics.proto.v1alpha8` to convert from/to protobuf.
12+
13+
Since we can't emit deprecation messages for this (as they will trigger every time a metric value is used), please consider using the new conversion functions as soon as possible so the migration to the next breaking release is smooth.
1014

1115
## New Features
1216

13-
<!-- Here goes the main new features and examples or instructions on how to use them -->
17+
- A new module `frequenz.client.common.metrics.proto.v1alpha8` has been added to provide conversion functions for `Metric`s from/to protobuf version `v1alpha8`.
1418

1519
## Bug Fixes
1620

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# License: MIT
2+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Conversion of Metric from/to protobuf v1alpha8."""
5+
6+
from ._metric import metric_from_proto, metric_to_proto
7+
8+
__all__ = [
9+
"metric_from_proto",
10+
"metric_to_proto",
11+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# License: MIT
2+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Coversion of Metric to/from protobuf v1alpha8."""
5+
6+
7+
from frequenz.api.common.v1alpha8.metrics import metrics_pb2
8+
9+
from ....proto import enum_from_proto
10+
from ..._metric import Metric
11+
12+
13+
def metric_from_proto(message: metrics_pb2.Metric.ValueType) -> Metric | int:
14+
"""Convert a protobuf Metric message to a Metric enum member.
15+
16+
Args:
17+
message: A protobuf Metric message.
18+
19+
Returns:
20+
The corresponding Metric enum member.
21+
"""
22+
return enum_from_proto(message, Metric)
23+
24+
25+
def metric_to_proto(metric: Metric) -> metrics_pb2.Metric.ValueType:
26+
"""Convert a Metric enum member to a protobuf Metric message.
27+
28+
Args:
29+
metric: A Metric enum member.
30+
31+
Returns:
32+
The corresponding protobuf Metric message.
33+
"""
34+
return metrics_pb2.Metric.ValueType(metric.value)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# License: MIT
2+
# Copyright © 2026 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for Metric to/from protobuf v1alpha8 conversion.
5+
6+
These tests ensure that, for this version, all enum members are correctly matched by
7+
name and value between the Python `Metric` enum and the protobuf `Metric` enum.
8+
"""
9+
10+
import pytest
11+
from frequenz.api.common.v1alpha8.metrics import metrics_pb2
12+
13+
from frequenz.client.common.metrics import Metric
14+
from frequenz.client.common.metrics.proto.v1alpha8 import (
15+
metric_from_proto,
16+
metric_to_proto,
17+
)
18+
19+
PB_NAMES: list[str] = [m.name for m in metrics_pb2.Metric.DESCRIPTOR.values]
20+
21+
UNKNOWN_PB_VALUE = metrics_pb2.Metric.ValueType(max(m.value for m in Metric) + 1)
22+
23+
24+
@pytest.mark.parametrize("pb_name", PB_NAMES)
25+
def test_proto_enum_matches_enum_name(pb_name: str) -> None:
26+
"""Test that all known protobuf enum names have a matching Metric enum member."""
27+
pb_value = metrics_pb2.Metric.Value(pb_name)
28+
try:
29+
metric = Metric[pb_name.removeprefix("METRIC_")]
30+
assert metric.value == pb_value
31+
except KeyError:
32+
pass # It is OK to have new protobuf enum values not yet in Metric.
33+
34+
35+
@pytest.mark.parametrize("pb_name", PB_NAMES)
36+
def test_proto_enum_matches_enum_value(pb_name: str) -> None:
37+
"""Test that all known protobuf enum values have a matching Metric enum member."""
38+
pb_value = metrics_pb2.Metric.Value(pb_name)
39+
try:
40+
metric = Metric(pb_value)
41+
assert metric.value == pb_value
42+
except ValueError:
43+
pass # It is OK to have new protobuf enum values not yet in Metric.
44+
45+
46+
@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name)
47+
def test_enum_matches_proto_enum_name(metric: Metric) -> None:
48+
"""Test that all Metric enum members have a matching protobuf enum name."""
49+
pb_value = metrics_pb2.Metric.ValueType(metric.value)
50+
pb_name = metrics_pb2.Metric.Name(pb_value)
51+
assert pb_name == f"METRIC_{metric.name}"
52+
53+
54+
@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name)
55+
def test_enum_matches_proto_enum_value(metric: Metric) -> None:
56+
"""Test that all Metric enum members have a matching protobuf enum value."""
57+
pb_value = metrics_pb2.Metric.Value(f"METRIC_{metric.name}")
58+
assert metric.value == pb_value
59+
60+
61+
@pytest.mark.parametrize("pb_name", PB_NAMES)
62+
def test_from_proto(pb_name: str) -> None:
63+
"""Test conversion from protobuf returns a matching member or the int for unknown values."""
64+
pb_value = metrics_pb2.Metric.Value(pb_name)
65+
metric = metric_from_proto(pb_value)
66+
if pb_value in [m.value for m in Metric]:
67+
assert metric is Metric(pb_value)
68+
else:
69+
assert metric == pb_value
70+
71+
72+
def test_from_proto_unknown() -> None:
73+
"""Test conversion from protobuf for yet unknown values return the int."""
74+
metric = metric_from_proto(UNKNOWN_PB_VALUE)
75+
assert isinstance(metric, int)
76+
assert metric == UNKNOWN_PB_VALUE
77+
78+
79+
@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name)
80+
def test_to_proto(metric: Metric) -> None:
81+
"""Test conversion to protobuf return a matching protobuf value."""
82+
pb_value = metric_to_proto(metric)
83+
assert pb_value == metric.value

0 commit comments

Comments
 (0)