From 22fc3517d5d9d8df84419bce6bf74d9397e127ae Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Tue, 7 Jul 2026 10:03:10 +0200 Subject: [PATCH 1/4] Add `InvalidAttributeError` exception This exception will be the base for any exception raised when an attribute that holds invalid data is accessed through a semantic accessor. Signed-off-by: Leandro Lucarella --- src/frequenz/client/common/__init__.py | 2 ++ src/frequenz/client/common/_exception.py | 30 +++++++++++++++++ .../exception/test_invalid_attribute_error.py | 33 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tests/exception/test_invalid_attribute_error.py diff --git a/src/frequenz/client/common/__init__.py b/src/frequenz/client/common/__init__.py index 302376ce..8752297e 100644 --- a/src/frequenz/client/common/__init__.py +++ b/src/frequenz/client/common/__init__.py @@ -5,12 +5,14 @@ from ._exception import ( ClientCommonError, + InvalidAttributeError, UnrecognizedEnumValueError, UnspecifiedEnumValueError, ) __all__ = [ "ClientCommonError", + "InvalidAttributeError", "UnrecognizedEnumValueError", "UnspecifiedEnumValueError", ] diff --git a/src/frequenz/client/common/_exception.py b/src/frequenz/client/common/_exception.py index d7fb21b0..67f8b8a9 100644 --- a/src/frequenz/client/common/_exception.py +++ b/src/frequenz/client/common/_exception.py @@ -8,6 +8,36 @@ class ClientCommonError(Exception): """Base class for all errors raised by frequenz-client-common.""" +class InvalidAttributeError(ClientCommonError, ValueError): + """Raised when a semantic accessor sees an invalid value for a field. + + This is also a [`ValueError`][] for convenience. + """ + + def __init__( + self, instance: object, attr_name: str, message: str | None = None + ) -> None: + """Initialize this error. + + Args: + instance: The object instance that had an invalid value. + attr_name: The name of the attribute that had an invalid value. + message: A custom error message. If `None`, a default message + mentioning the instance and attribute is used. + """ + self.instance: object = instance + """The object instance that had an invalid value.""" + + self.attr_name: str = attr_name + """The name of the attribute that had an invalid value.""" + + super().__init__( + message + if message is not None + else f"invalid value for attribute {attr_name!r} in {instance}" + ) + + class UnrecognizedEnumValueError(ClientCommonError, ValueError): """Raised when a semantic accessor sees an unrecognized protobuf enum value. diff --git a/tests/exception/test_invalid_attribute_error.py b/tests/exception/test_invalid_attribute_error.py new file mode 100644 index 00000000..0a6b56ba --- /dev/null +++ b/tests/exception/test_invalid_attribute_error.py @@ -0,0 +1,33 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for `InvalidAttributeError`.""" + +from frequenz.client.common import ClientCommonError, InvalidAttributeError + + +def test_is_client_common_and_value_error() -> None: + """`InvalidAttributeError` is both a `ClientCommonError` and a `ValueError`.""" + assert issubclass(InvalidAttributeError, ClientCommonError) + assert issubclass(InvalidAttributeError, ValueError) + + +def test_stores_instance_and_attr_name() -> None: + """`InvalidAttributeError` stores its `instance` and `attr_name` args as attributes.""" + instance = object() + error = InvalidAttributeError(instance, "attr_x") + assert error.instance is instance + assert error.attr_name == "attr_x" + + +def test_default_message() -> None: + """The default message follows the `invalid value for attribute ...` template.""" + assert ( + str(InvalidAttributeError("some-instance", "attr_x")) + == "invalid value for attribute 'attr_x' in some-instance" + ) + + +def test_custom_message_replaces_the_default() -> None: + """A custom message replaces the default entirely.""" + assert str(InvalidAttributeError("i", "a", "explicit msg")) == "explicit msg" From f5e85c44beff7cb24fedb81cb01592970fff8f46 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Tue, 7 Jul 2026 10:42:38 +0200 Subject: [PATCH 2/4] Make value errors inherit from `InvalidAttributeError` Make `UnrecognizedEnumValueError` and `UnspecifiedEnumValueError` inherit from `InvalidAttributeError`. Also split exception tests as the file grew too much when adding the new tests. Signed-off-by: Leandro Lucarella --- src/frequenz/client/common/_exception.py | 41 +++++++++++++++-- .../client/common/grid/_delivery_area.py | 10 +---- src/frequenz/client/common/metrics/_sample.py | 17 ++----- .../client/common/microgrid/_microgrid.py | 7 ++- .../_electrical_component.py | 12 ++++- tests/exception/__init__.py | 4 ++ tests/exception/test_client_common_error.py | 24 ++++++++++ .../test_unrecognized_enum_value_error.py | 41 +++++++++++++++++ .../test_unspecified_enum_value_error.py | 38 ++++++++++++++++ tests/test_exception.py | 44 ------------------- 10 files changed, 165 insertions(+), 73 deletions(-) create mode 100644 tests/exception/__init__.py create mode 100644 tests/exception/test_client_common_error.py create mode 100644 tests/exception/test_unrecognized_enum_value_error.py create mode 100644 tests/exception/test_unspecified_enum_value_error.py delete mode 100644 tests/test_exception.py diff --git a/src/frequenz/client/common/_exception.py b/src/frequenz/client/common/_exception.py index 67f8b8a9..6c971eb7 100644 --- a/src/frequenz/client/common/_exception.py +++ b/src/frequenz/client/common/_exception.py @@ -38,7 +38,7 @@ def __init__( ) -class UnrecognizedEnumValueError(ClientCommonError, ValueError): +class UnrecognizedEnumValueError(InvalidAttributeError): """Raised when a semantic accessor sees an unrecognized protobuf enum value. This happens when the server sets an enum value that this version of the @@ -49,21 +49,33 @@ class UnrecognizedEnumValueError(ClientCommonError, ValueError): This is also a ``ValueError`` for convenience. """ - def __init__(self, value: int, message: str | None = None) -> None: + def __init__( + self, instance: object, attr_name: str, value: int, message: str | None = None + ) -> None: """Initialize this error. Args: + instance: The object instance that had the unrecognized value. + attr_name: The name of the attribute that had the unrecognized value. value: The raw protobuf value that was not recognized. message: A custom error message. If `None`, a default message mentioning the unrecognized value is used. """ self.value: int = value + """The raw protobuf value that was not recognized.""" + super().__init__( - message if message is not None else f"unrecognized enum value: {value!r}" + instance, + attr_name, + ( + message + if message is not None + else f"unrecognized enum value {value!r} for attribute {attr_name!r} in {instance}" + ), ) -class UnspecifiedEnumValueError(ClientCommonError, ValueError): +class UnspecifiedEnumValueError(InvalidAttributeError): """Raised when a semantic accessor sees an unspecified protobuf enum value. For a value that is set but not recognized by this client, see @@ -71,3 +83,24 @@ class UnspecifiedEnumValueError(ClientCommonError, ValueError): This is also a [`ValueError`][] for convenience. """ + + def __init__( + self, instance: object, attr_name: str, message: str | None = None + ) -> None: + """Initialize this error. + + Args: + instance: The object instance that had the unspecified value. + attr_name: The name of the attribute that had the unspecified value. + message: A custom error message. If `None`, a default message + mentioning the unspecified value is used. + """ + super().__init__( + instance, + attr_name, + ( + message + if message is not None + else f"unspecified enum value for attribute {attr_name!r} in {instance}" + ), + ) diff --git a/src/frequenz/client/common/grid/_delivery_area.py b/src/frequenz/client/common/grid/_delivery_area.py index 4dd4e990..5cabaa4d 100644 --- a/src/frequenz/client/common/grid/_delivery_area.py +++ b/src/frequenz/client/common/grid/_delivery_area.py @@ -119,16 +119,10 @@ def get_code_type(self) -> EnergyMarketCodeType: warnings.filterwarnings("ignore", category=DeprecationWarning) match self.code_type: case 0 | EnergyMarketCodeType.UNSPECIFIED: - raise UnspecifiedEnumValueError( - f"code type of {self} is unspecified" - ) + raise UnspecifiedEnumValueError(self, "code_type") case EnergyMarketCodeType() as code_type: return code_type case int() as code_type: - raise UnrecognizedEnumValueError( - code_type, - f"code type {code_type!r} of {self} is not a recognized " - "EnergyMarketCodeType", - ) + raise UnrecognizedEnumValueError(self, "code_type", code_type) case unknown: assert_never(unknown) diff --git a/src/frequenz/client/common/metrics/_sample.py b/src/frequenz/client/common/metrics/_sample.py index 99eddfda..71dfb515 100644 --- a/src/frequenz/client/common/metrics/_sample.py +++ b/src/frequenz/client/common/metrics/_sample.py @@ -157,17 +157,11 @@ def get_category(self) -> MetricConnectionCategory: warnings.filterwarnings("ignore", category=DeprecationWarning) match self.category: case 0 | MetricConnectionCategory.UNSPECIFIED: - raise UnspecifiedEnumValueError( - "connection category is unspecified" - ) + raise UnspecifiedEnumValueError(self, "category") case MetricConnectionCategory(): return self.category case int(): - raise UnrecognizedEnumValueError( - self.category, - f"connection category {self.category!r} is not a recognized " - "MetricConnectionCategory", - ) + raise UnrecognizedEnumValueError(self, "category", self.category) case unexpected: assert_never(unexpected) @@ -305,13 +299,10 @@ def get_metric(self) -> Metric: warnings.filterwarnings("ignore", category=DeprecationWarning) match self.metric: case 0 | Metric.UNSPECIFIED: - raise UnspecifiedEnumValueError("sampled metric is unspecified") + raise UnspecifiedEnumValueError(self, "metric") case Metric(): return self.metric case int(): - raise UnrecognizedEnumValueError( - self.metric, - f"sampled metric {self.metric!r} is not a recognized Metric", - ) + raise UnrecognizedEnumValueError(self, "metric", self.metric) case unexpected: assert_never(unexpected) diff --git a/src/frequenz/client/common/microgrid/_microgrid.py b/src/frequenz/client/common/microgrid/_microgrid.py index 21d6641c..d3a0788c 100644 --- a/src/frequenz/client/common/microgrid/_microgrid.py +++ b/src/frequenz/client/common/microgrid/_microgrid.py @@ -93,11 +93,14 @@ def is_active(self) -> bool: return active case 0: raise UnspecifiedEnumValueError( - f"status of microgrid {self} is unspecified" + self, "_active", f"status of microgrid {self} is unspecified" ) case int() as value: raise UnrecognizedEnumValueError( - value, f"unrecognized status of microgrid {self}: {value!r}" + self, + "_active", + value, + f"unrecognized status of microgrid {self}: {value!r}", ) case unknown: assert_never(unknown) diff --git a/src/frequenz/client/common/microgrid/electrical_components/_electrical_component.py b/src/frequenz/client/common/microgrid/electrical_components/_electrical_component.py index 6d458583..1c06514f 100644 --- a/src/frequenz/client/common/microgrid/electrical_components/_electrical_component.py +++ b/src/frequenz/client/common/microgrid/electrical_components/_electrical_component.py @@ -133,11 +133,15 @@ def provides_telemetry(self) -> bool: return provides_telemetry case 0: raise UnspecifiedEnumValueError( + self, + "_provides_telemetry", f"operational mode of {self} is unspecified; " - "telemetry availability is unknown" + "telemetry availability is unknown", ) case int() as value: raise UnrecognizedEnumValueError( + self, + "_provides_telemetry", value, f"operational mode {value!r} of {self} is not a recognized " "ElectricalComponentOperationalMode; telemetry availability " @@ -162,11 +166,15 @@ def accepts_control(self) -> bool: return accepts_control case 0: raise UnspecifiedEnumValueError( + self, + "_accepts_control", f"operational mode of {self} is unspecified; " - "control availability is unknown" + "control availability is unknown", ) case int() as value: raise UnrecognizedEnumValueError( + self, + "_accepts_control", value, f"operational mode {value!r} of {self} is not a recognized " "ElectricalComponentOperationalMode; control availability " diff --git a/tests/exception/__init__.py b/tests/exception/__init__.py new file mode 100644 index 00000000..691c2190 --- /dev/null +++ b/tests/exception/__init__.py @@ -0,0 +1,4 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for the common exception hierarchy.""" diff --git a/tests/exception/test_client_common_error.py b/tests/exception/test_client_common_error.py new file mode 100644 index 00000000..96b2f361 --- /dev/null +++ b/tests/exception/test_client_common_error.py @@ -0,0 +1,24 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for `ClientCommonError` and cross-cutting exception-module checks.""" + +import frequenz.client.common +from frequenz.client.common import ClientCommonError + + +def test_is_a_plain_exception() -> None: + """`ClientCommonError` is an `Exception` but deliberately not a `ValueError`.""" + assert issubclass(ClientCommonError, Exception) + assert not issubclass(ClientCommonError, ValueError) + + +def test_all_exports_every_exception_class() -> None: + """Every public exception class is re-exported through the package `__all__`.""" + for name in ( + "ClientCommonError", + "InvalidAttributeError", + "UnrecognizedEnumValueError", + "UnspecifiedEnumValueError", + ): + assert name in frequenz.client.common.__all__ diff --git a/tests/exception/test_unrecognized_enum_value_error.py b/tests/exception/test_unrecognized_enum_value_error.py new file mode 100644 index 00000000..87b17ac1 --- /dev/null +++ b/tests/exception/test_unrecognized_enum_value_error.py @@ -0,0 +1,41 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for `UnrecognizedEnumValueError`.""" + +from frequenz.client.common import ( + ClientCommonError, + InvalidAttributeError, + UnrecognizedEnumValueError, +) + + +def test_inherits_invalid_attribute_error() -> None: + """`UnrecognizedEnumValueError` inherits `InvalidAttributeError` (and thus `ValueError`).""" + assert issubclass(UnrecognizedEnumValueError, InvalidAttributeError) + assert issubclass(UnrecognizedEnumValueError, ClientCommonError) + assert issubclass(UnrecognizedEnumValueError, ValueError) + + +def test_stores_instance_attr_name_and_value() -> None: + """`UnrecognizedEnumValueError` stores `instance`, `attr_name`, and `value` as attributes.""" + instance = object() + error = UnrecognizedEnumValueError(instance, "attr_x", 999) + assert error.instance is instance + assert error.attr_name == "attr_x" + assert error.value == 999 + + +def test_default_message() -> None: + """The default message follows the `unrecognized enum value ...` template.""" + assert ( + str(UnrecognizedEnumValueError("some-instance", "attr_x", 7)) + == "unrecognized enum value 7 for attribute 'attr_x' in some-instance" + ) + + +def test_custom_message_replaces_the_default() -> None: + """A custom message replaces the default entirely.""" + assert ( + str(UnrecognizedEnumValueError("i", "a", 7, "explicit msg")) == "explicit msg" + ) diff --git a/tests/exception/test_unspecified_enum_value_error.py b/tests/exception/test_unspecified_enum_value_error.py new file mode 100644 index 00000000..adbf156a --- /dev/null +++ b/tests/exception/test_unspecified_enum_value_error.py @@ -0,0 +1,38 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for `UnspecifiedEnumValueError`.""" + +from frequenz.client.common import ( + ClientCommonError, + InvalidAttributeError, + UnspecifiedEnumValueError, +) + + +def test_inherits_invalid_attribute_error() -> None: + """`UnspecifiedEnumValueError` inherits `InvalidAttributeError` (and thus `ValueError`).""" + assert issubclass(UnspecifiedEnumValueError, InvalidAttributeError) + assert issubclass(UnspecifiedEnumValueError, ClientCommonError) + assert issubclass(UnspecifiedEnumValueError, ValueError) + + +def test_stores_instance_and_attr_name() -> None: + """`UnspecifiedEnumValueError` stores its `instance` and `attr_name` args.""" + instance = object() + error = UnspecifiedEnumValueError(instance, "attr_x") + assert error.instance is instance + assert error.attr_name == "attr_x" + + +def test_default_message() -> None: + """The default message follows the `unspecified enum value ...` template.""" + assert ( + str(UnspecifiedEnumValueError("some-instance", "attr_x")) + == "unspecified enum value for attribute 'attr_x' in some-instance" + ) + + +def test_custom_message_replaces_the_default() -> None: + """A custom message replaces the default entirely.""" + assert str(UnspecifiedEnumValueError("i", "a", "explicit msg")) == "explicit msg" diff --git a/tests/test_exception.py b/tests/test_exception.py deleted file mode 100644 index 0308cc6b..00000000 --- a/tests/test_exception.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Tests for common exceptions.""" - -import frequenz.client.common -from frequenz.client.common import ( - ClientCommonError, - UnrecognizedEnumValueError, - UnspecifiedEnumValueError, -) - - -def test_exceptions_exported_and_related() -> None: - """Given exception exports, then their hierarchy and string form are correct.""" - assert issubclass(UnspecifiedEnumValueError, ClientCommonError) - assert issubclass(UnspecifiedEnumValueError, ValueError) - assert issubclass(UnrecognizedEnumValueError, ClientCommonError) - assert issubclass(UnrecognizedEnumValueError, ValueError) - assert not issubclass(ClientCommonError, ValueError) - assert str(UnspecifiedEnumValueError("msg")) == "msg" - - -def test_all_is_sorted_and_exports_unrecognized() -> None: - """Given the package exports, then __all__ includes the new error and stays sorted.""" - assert "UnrecognizedEnumValueError" in frequenz.client.common.__all__ - assert list(frequenz.client.common.__all__) == sorted( - frequenz.client.common.__all__ - ) - - -def test_unrecognized_enum_value_error_carries_value() -> None: - """Given an unrecognized value, then it is stored and catchable as both base types.""" - error = UnrecognizedEnumValueError(999) - assert error.value == 999 - assert isinstance(error, ValueError) - assert isinstance(error, ClientCommonError) - - -def test_unrecognized_enum_value_error_default_message() -> None: - """Given no explicit message, then the default string contains the raw value.""" - assert "7" in str(UnrecognizedEnumValueError(7)) - - -def test_unrecognized_enum_value_error_custom_message() -> None: - """Given a custom message, then the string form is exactly that message.""" - assert str(UnrecognizedEnumValueError(7, "msg")) == "msg" From d41ea0203889a325797e1850266fa0d86dc4d111 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Tue, 7 Jul 2026 14:25:15 +0200 Subject: [PATCH 3/4] Add `MissingFieldError` exception Add a new top-level exception `MissingFieldError`, sibling of `UnspecifiedEnumValueError`, to signal that a semantic accessor was called on a wrapper field that resolves to `None` because the underlying field was not set on the wire. The first user is the upcoming `Microgrid.get_delivery_area()` accessor (delivery area may be `None`, `InvalidDeliveryArea`, or `DeliveryArea`), but the pattern is expected to recur wherever an accessor unifies a `T | ... | None` field into a plain `T` return. Like `Unspecified/UnrecognizedEnumValueError`, it multi-inherits from `ClientCommonError` and `ValueError` for convenience. Signed-off-by: Leandro Lucarella --- src/frequenz/client/common/__init__.py | 2 ++ src/frequenz/client/common/_exception.py | 32 +++++++++++++++++ tests/exception/test_client_common_error.py | 1 + tests/exception/test_missing_field_error.py | 38 +++++++++++++++++++++ 4 files changed, 73 insertions(+) create mode 100644 tests/exception/test_missing_field_error.py diff --git a/src/frequenz/client/common/__init__.py b/src/frequenz/client/common/__init__.py index 8752297e..0580c58d 100644 --- a/src/frequenz/client/common/__init__.py +++ b/src/frequenz/client/common/__init__.py @@ -6,6 +6,7 @@ from ._exception import ( ClientCommonError, InvalidAttributeError, + MissingFieldError, UnrecognizedEnumValueError, UnspecifiedEnumValueError, ) @@ -13,6 +14,7 @@ __all__ = [ "ClientCommonError", "InvalidAttributeError", + "MissingFieldError", "UnrecognizedEnumValueError", "UnspecifiedEnumValueError", ] diff --git a/src/frequenz/client/common/_exception.py b/src/frequenz/client/common/_exception.py index 6c971eb7..dd7317b4 100644 --- a/src/frequenz/client/common/_exception.py +++ b/src/frequenz/client/common/_exception.py @@ -104,3 +104,35 @@ def __init__( else f"unspecified enum value for attribute {attr_name!r} in {instance}" ), ) + + +class MissingFieldError(InvalidAttributeError): + """Raised when a semantic accessor sees a missing optional field. + + This is used by accessors that resolve a wrapper field which may be + absent (typed as `T | None` or `T | ... | None`) to a concrete value, + when the underlying field was not set on the wire. + + This is also a [`ValueError`][] for convenience. + """ + + def __init__( + self, instance: object, attr_name: str, message: str | None = None + ) -> None: + """Initialize this error. + + Args: + instance: The object instance that was missing the field. + attr_name: The name of the missing field. + message: A custom error message. If `None`, a default message + mentioning the missing field is used. + """ + super().__init__( + instance, + attr_name, + ( + message + if message is not None + else f"missing protobuf field {attr_name!r} in {instance}" + ), + ) diff --git a/tests/exception/test_client_common_error.py b/tests/exception/test_client_common_error.py index 96b2f361..2c3ed4ee 100644 --- a/tests/exception/test_client_common_error.py +++ b/tests/exception/test_client_common_error.py @@ -18,6 +18,7 @@ def test_all_exports_every_exception_class() -> None: for name in ( "ClientCommonError", "InvalidAttributeError", + "MissingFieldError", "UnrecognizedEnumValueError", "UnspecifiedEnumValueError", ): diff --git a/tests/exception/test_missing_field_error.py b/tests/exception/test_missing_field_error.py new file mode 100644 index 00000000..a4146e77 --- /dev/null +++ b/tests/exception/test_missing_field_error.py @@ -0,0 +1,38 @@ +# License: MIT +# Copyright © 2026 Frequenz Energy-as-a-Service GmbH + +"""Tests for `MissingFieldError`.""" + +from frequenz.client.common import ( + ClientCommonError, + InvalidAttributeError, + MissingFieldError, +) + + +def test_inherits_invalid_attribute_error() -> None: + """`MissingFieldError` inherits `InvalidAttributeError` (and thus `ValueError`).""" + assert issubclass(MissingFieldError, InvalidAttributeError) + assert issubclass(MissingFieldError, ClientCommonError) + assert issubclass(MissingFieldError, ValueError) + + +def test_stores_instance_and_attr_name() -> None: + """`MissingFieldError` stores its `instance` and `attr_name` args.""" + instance = object() + error = MissingFieldError(instance, "field_x") + assert error.instance is instance + assert error.attr_name == "field_x" + + +def test_default_message() -> None: + """The default message follows the `missing protobuf field ...` template.""" + assert ( + str(MissingFieldError("some-instance", "field_x")) + == "missing protobuf field 'field_x' in some-instance" + ) + + +def test_custom_message_replaces_the_default() -> None: + """A custom message replaces the default entirely.""" + assert str(MissingFieldError("i", "a", "explicit msg")) == "explicit msg" From 00b8b764fbe43c0c9ef96b7b58bd729fecab5652 Mon Sep 17 00:00:00 2001 From: Leandro Lucarella Date: Thu, 9 Jul 2026 15:04:11 +0200 Subject: [PATCH 4/4] Update release notes Signed-off-by: Leandro Lucarella --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 51c99511..084c6c96 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -63,6 +63,8 @@ * Added new exceptions: * `frequenz.client.common.ClientCommonError` as a base exception for the package. + * `frequenz.client.common.InvalidAttributeError` as a base for all exceptions raised when an invalid attribute is encountered. Inherits also from `ValueError` for convenience. + * `frequenz.client.common.MissingFieldError` for accessors that resolve a `T | ... | None` wrapper field to a concrete value and see `None` because the underlying field was not set on the wire. * `frequenz.client.common.UnspecifiedEnumValueError` for unspecified enum values (raw `0` or the deprecated member). * `frequenz.client.common.UnrecognizedEnumValueError` for enum members not yet recognized by the library. Carries the raw integer value in its `value` attribute.