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