Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@

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.

* `frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto` is now deprecated; use `delivery_area_from_proto2` instead.

The new converter returns `DeliveryArea | InvalidDeliveryArea` and surfaces malformed wire data at the type level rather than silently constructing a `DeliveryArea` with invalid content. The old converter continues to work but emits a `DeprecationWarning`.

* `frequenz.client.common.grid.DeliveryArea` construction with invalid data is deprecated. Please construct only valid `DeliveryArea` objects.

A well-formed `DeliveryArea` has a non-empty `code` and a non-`UNSPECIFIED` `code_type`. Constructing one with invalid data currently emits a `DeprecationWarning`; a future release will replace the warning with a hard `ValueError`. Prefer `delivery_area_from_proto2` to load delivery areas from the wire — malformed messages become `InvalidDeliveryArea` instances instead.

## New Features

* Added 4 new electrical component classes for categories that previously collapsed into `UnrecognizedElectricalComponent`:
Expand All @@ -63,15 +71,24 @@
* Added new exceptions:

* `frequenz.client.common.ClientCommonError` as a base exception for the package.
* `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.

* Added safe convenience getters that raise the new exceptions for unspecified or unrecognized values:
* Added safe convenience getters that raise the new exceptions for unspecified, unrecognized, missing or invalid values:

* `frequenz.client.common.grid.DeliveryArea.get_code_type()`
* `frequenz.client.common.metrics.MetricConnection.get_category()`
* `frequenz.client.common.metrics.MetricSample.get_metric()`

* Added new delivery-area class hierarchy:

* `frequenz.client.common.grid.BaseDeliveryArea` — abstract common supertype of the two concrete leaves; not directly instantiable.
* `frequenz.client.common.grid.DeliveryArea` — well-formed delivery area (retroactively made a subclass of `BaseDeliveryArea`; its shared `__str__` and `get_code_type()` are hoisted to the base).
* `frequenz.client.common.grid.InvalidDeliveryArea` — malformed wire data; same fields as `DeliveryArea` with no invariants enforced, so callers can inspect whatever the server actually sent.

* Added `frequenz.client.common.grid.proto.v1alpha8.delivery_area_from_proto2` returning `DeliveryArea | InvalidDeliveryArea`. This is the replacement for the now-deprecated `delivery_area_from_proto` and is what `microgrid_from_proto` uses internally.

* Added a new `frequenz.client.common.types.Lifetime` type together with the `frequenz.client.common.types.proto.v1alpha8.lifetime_from_proto` conversion function.

* Added a new `frequenz.client.common.types.Location` type together with the `frequenz.client.common.types.proto.v1alpha8.location_from_proto` conversion function.
Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ filterwarnings = [
# characters as this is a regex.
'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning',
]
addopts = "-vv"
testpaths = ["tests", "src"]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
Expand Down
4 changes: 4 additions & 0 deletions src/frequenz/client/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@

from ._exception import (
ClientCommonError,
InvalidAttributeError,
MissingFieldError,
UnrecognizedEnumValueError,
UnspecifiedEnumValueError,
)

__all__ = [
"ClientCommonError",
"InvalidAttributeError",
"MissingFieldError",
"UnrecognizedEnumValueError",
"UnspecifiedEnumValueError",
]
104 changes: 100 additions & 4 deletions src/frequenz/client/common/_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,37 @@ class ClientCommonError(Exception):
"""Base class for all errors raised by frequenz-client-common."""


class UnrecognizedEnumValueError(ClientCommonError, ValueError):
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(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
Expand All @@ -19,25 +49,91 @@ 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 "
f"instance {instance!r}"
),
)


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
[`UnrecognizedEnumValueError`][..UnrecognizedEnumValueError].

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 {instance!r}"
),
)


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"attribute {attr_name!r} for {instance} missing in protobuf message"
),
)
11 changes: 10 additions & 1 deletion src/frequenz/client/common/grid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@

"""Grid definitions for the energy market."""

from ._delivery_area import DeliveryArea, EnergyMarketCodeType
from ._delivery_area import (
BaseDeliveryArea,
DeliveryArea,
EnergyMarketCodeType,
InvalidDeliveryArea,
InvalidDeliveryAreaError,
)

__all__ = [
"BaseDeliveryArea",
"DeliveryArea",
"EnergyMarketCodeType",
"InvalidDeliveryArea",
"InvalidDeliveryAreaError",
]
Loading
Loading