From 73674ebae44406343f731835f49708c2426010d9 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Fri, 23 Jan 2026 13:24:46 +0100 Subject: [PATCH] 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 --- RELEASE_NOTES.md | 10 ++- .../common/metrics/proto/v1alpha8/__init__.py | 11 +++ .../common/metrics/proto/v1alpha8/_metric.py | 34 ++++++++ tests/metrics/proto/v1alpha8/test_metric.py | 83 +++++++++++++++++++ 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 src/frequenz/client/common/metrics/proto/v1alpha8/__init__.py create mode 100644 src/frequenz/client/common/metrics/proto/v1alpha8/_metric.py create mode 100644 tests/metrics/proto/v1alpha8/test_metric.py diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 92616f8..1c9106f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,13 +4,17 @@ -## Upgrading +## Deprecation - +- Converting `Metric` enums from/to protobuf directly is deprecated and will be dropped in the next breaking release. + + You should switch to use the new conversion functions in `frequenz.client.common.metrics.proto.v1alpha8` to convert from/to protobuf. + + 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. ## New Features - +- A new module `frequenz.client.common.metrics.proto.v1alpha8` has been added to provide conversion functions for `Metric`s from/to protobuf version `v1alpha8`. ## Bug Fixes diff --git a/src/frequenz/client/common/metrics/proto/v1alpha8/__init__.py b/src/frequenz/client/common/metrics/proto/v1alpha8/__init__.py new file mode 100644 index 0000000..53b0a69 --- /dev/null +++ b/src/frequenz/client/common/metrics/proto/v1alpha8/__init__.py @@ -0,0 +1,11 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Conversion of Metric from/to protobuf v1alpha8.""" + +from ._metric import metric_from_proto, metric_to_proto + +__all__ = [ + "metric_from_proto", + "metric_to_proto", +] diff --git a/src/frequenz/client/common/metrics/proto/v1alpha8/_metric.py b/src/frequenz/client/common/metrics/proto/v1alpha8/_metric.py new file mode 100644 index 0000000..aafe59d --- /dev/null +++ b/src/frequenz/client/common/metrics/proto/v1alpha8/_metric.py @@ -0,0 +1,34 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Coversion of Metric to/from protobuf v1alpha8.""" + + +from frequenz.api.common.v1alpha8.metrics import metrics_pb2 + +from ....proto import enum_from_proto +from ..._metric import Metric + + +def metric_from_proto(message: metrics_pb2.Metric.ValueType) -> Metric | int: + """Convert a protobuf Metric message to a Metric enum member. + + Args: + message: A protobuf Metric message. + + Returns: + The corresponding Metric enum member. + """ + return enum_from_proto(message, Metric) + + +def metric_to_proto(metric: Metric) -> metrics_pb2.Metric.ValueType: + """Convert a Metric enum member to a protobuf Metric message. + + Args: + metric: A Metric enum member. + + Returns: + The corresponding protobuf Metric message. + """ + return metrics_pb2.Metric.ValueType(metric.value) diff --git a/tests/metrics/proto/v1alpha8/test_metric.py b/tests/metrics/proto/v1alpha8/test_metric.py new file mode 100644 index 0000000..0de0650 --- /dev/null +++ b/tests/metrics/proto/v1alpha8/test_metric.py @@ -0,0 +1,83 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for Metric to/from protobuf v1alpha8 conversion. + +These tests ensure that, for this version, all enum members are correctly matched by +name and value between the Python `Metric` enum and the protobuf `Metric` enum. +""" + +import pytest +from frequenz.api.common.v1alpha8.metrics import metrics_pb2 + +from frequenz.client.common.metrics import Metric +from frequenz.client.common.metrics.proto.v1alpha8 import ( + metric_from_proto, + metric_to_proto, +) + +PB_NAMES: list[str] = [m.name for m in metrics_pb2.Metric.DESCRIPTOR.values] + +UNKNOWN_PB_VALUE = metrics_pb2.Metric.ValueType(max(m.value for m in Metric) + 1) + + +@pytest.mark.parametrize("pb_name", PB_NAMES) +def test_proto_enum_matches_enum_name(pb_name: str) -> None: + """Test that all known protobuf enum names have a matching Metric enum member.""" + pb_value = metrics_pb2.Metric.Value(pb_name) + try: + metric = Metric[pb_name.removeprefix("METRIC_")] + assert metric.value == pb_value + except KeyError: + pass # It is OK to have new protobuf enum values not yet in Metric. + + +@pytest.mark.parametrize("pb_name", PB_NAMES) +def test_proto_enum_matches_enum_value(pb_name: str) -> None: + """Test that all known protobuf enum values have a matching Metric enum member.""" + pb_value = metrics_pb2.Metric.Value(pb_name) + try: + metric = Metric(pb_value) + assert metric.value == pb_value + except ValueError: + pass # It is OK to have new protobuf enum values not yet in Metric. + + +@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name) +def test_enum_matches_proto_enum_name(metric: Metric) -> None: + """Test that all Metric enum members have a matching protobuf enum name.""" + pb_value = metrics_pb2.Metric.ValueType(metric.value) + pb_name = metrics_pb2.Metric.Name(pb_value) + assert pb_name == f"METRIC_{metric.name}" + + +@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name) +def test_enum_matches_proto_enum_value(metric: Metric) -> None: + """Test that all Metric enum members have a matching protobuf enum value.""" + pb_value = metrics_pb2.Metric.Value(f"METRIC_{metric.name}") + assert metric.value == pb_value + + +@pytest.mark.parametrize("pb_name", PB_NAMES) +def test_from_proto(pb_name: str) -> None: + """Test conversion from protobuf returns a matching member or the int for unknown values.""" + pb_value = metrics_pb2.Metric.Value(pb_name) + metric = metric_from_proto(pb_value) + if pb_value in [m.value for m in Metric]: + assert metric is Metric(pb_value) + else: + assert metric == pb_value + + +def test_from_proto_unknown() -> None: + """Test conversion from protobuf for yet unknown values return the int.""" + metric = metric_from_proto(UNKNOWN_PB_VALUE) + assert isinstance(metric, int) + assert metric == UNKNOWN_PB_VALUE + + +@pytest.mark.parametrize("metric", list(Metric), ids=lambda m: m.name) +def test_to_proto(metric: Metric) -> None: + """Test conversion to protobuf return a matching protobuf value.""" + pb_value = metric_to_proto(metric) + assert pb_value == metric.value