Skip to content
Closed
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
25 changes: 23 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,22 @@

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.metrics.Bounds` is now a deprecated subclass of [`Interval[float | None]`][frequenz.core.math.Interval]:

* Constructing a `Bounds` instance and reading `Bounds.lower` / `Bounds.upper` both emit `DeprecationWarning`. Use [`Interval[float | None]`][frequenz.core.math.Interval] from `frequenz-core` directly, and `Interval.start` / `Interval.end` to read endpoints.
* Existing code passing `Bounds(lower=…, upper=…)` where an `Interval` is expected continues to work via LSP: `Bounds` inherits from `Interval[float | None]`, so a `Bounds` instance _is_ an `Interval` instance, and equality (`Bounds(lower=1, upper=2) == Interval(1, 2)`) is symmetric.
* `Bounds` now inherits `Interval`'s validation and string conversion, which changes two visible behaviours:

* The `ValueError` raised for `start > end` now reads `"The start (X) can't be bigger than end (Y)"` instead of `"Lower bound (X) must be less than or equal to upper bound (Y)"`.
* `str(Bounds(lower=None, upper=None))` now returns `"[∞, ∞]"` instead of `"[None, None]"`.

* `frequenz.client.common.metrics.MetricSample.bounds` field type changed from `list[Bounds]` to `Sequence[Interval[float | None]]`. This is a soft-breaking change:

* `Sequence` is covariant, so existing code passing `list[Bounds]`, `list[Interval[float | None]]`, `tuple[Interval[float | None], ...]`, and similar containers continues to type-check. Code reading `sample.bounds` still supports iteration and indexing.
* Code that mutated the field with `list.append` (or any other list-only method) no longer type-checks. `MetricSample` was already frozen, so runtime mutation was never legal.

* `frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto` and `bounds_from_proto_with_issues` are now deprecated. Use `bounds_from_proto2` and `bounds_from_proto_with_issues2` respectively — both return [`Interval[float | None]`][frequenz.core.math.Interval].

## New Features

* Added 4 new electrical component classes for categories that previously collapsed into `UnrecognizedElectricalComponent`:
Expand Down Expand Up @@ -81,15 +97,20 @@
* `frequenz.client.common.metrics.MetricConnection.get_category()`
* `frequenz.client.common.metrics.MetricSample.get_metric()`

* 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.proto.v1alpha8.lifetime_from_proto` conversion function that parses a `lifetime_pb2.Lifetime` protobuf message into an [`Interval[datetime | None]`][frequenz.core.math.Interval] from `frequenz-core`.

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

* Added a new `frequenz.client.common.microgrid.Microgrid` type, together with the `frequenz.client.common.microgrid.proto.v1alpha8.microgrid_from_proto` conversion function.

* Added a new `frequenz.client.common.microgrid.electrical_components` package, featuring a `ElectricalComponent` class hierarchy and its families (battery, inverter, EV charger, etc.), and `ElectricalComponentConnection`, including `v1alpha8` proto conversion functions.
* Added a new `frequenz.client.common.microgrid.electrical_components` package, featuring a `ElectricalComponent` class hierarchy and its families (battery, inverter, EV charger, etc.), and `ElectricalComponentConnection`, including `v1alpha8` proto conversion functions. Their `operational_lifetime` fields are typed [`Interval[datetime | None]`][frequenz.core.math.Interval] and `metric_config_bounds` on `ElectricalComponent` is typed `Mapping[Metric | int, Interval[float | None]]`.
* Added a new `frequenz.client.common.microgrid.Microgrid` type with a raising `is_active()` method, together with the `frequenz.client.common.microgrid.proto.v1alpha8.microgrid_from_proto` conversion function.

* Added new proto conversion functions returning [`Interval[float | None]`][frequenz.core.math.Interval] to replace the deprecated `Bounds`-returning ones:

* `frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto2`
* `frequenz.client.common.metrics.proto.v1alpha8.bounds_from_proto_with_issues2`

## Bug Fixes

<!-- Here goes notable bug fixes that are worth a special mention or explanation -->
92 changes: 66 additions & 26 deletions src/frequenz/client/common/metrics/_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,82 @@

"""Definitions for bounds."""

import dataclasses
from typing import cast

from frequenz.core.math import Interval
from typing_extensions import deprecated

@dataclasses.dataclass(frozen=True, kw_only=True)
class Bounds:

@deprecated(
"`Bounds` is deprecated; use `frequenz.core.math.Interval[float | None]` "
"directly. `Bounds` is kept as a subclass of `Interval[float | None]` so "
"existing instances remain assignment-compatible with the new "
"`Interval`-typed fields."
)
class Bounds(Interval[float | None]):
"""A set of lower and upper bounds for any metric.

The lower bound must be less than or equal to the upper bound.

The units of the bounds are always the same as the related metric.
"""

lower: float | None = None
"""The lower bound.
`Bounds` is a thin, deprecated subclass of
[`Interval[float | None]`][frequenz.core.math.Interval]. It inherits all
validation, containment, equality, and string-conversion behaviour from
`Interval` and exposes the historical `lower` / `upper` attribute names
as deprecated aliases for `start` / `end`.

If `None`, there is no lower bound.
Deprecated:
Use [`Interval[float | None]`][frequenz.core.math.Interval] from
the `frequenz-core` package instead.
"""

upper: float | None = None
"""The upper bound.
def __init__(
self, *, lower: float | None = None, upper: float | None = None
) -> None:
"""Create a `Bounds` instance using the deprecated `lower`/`upper` kwargs.

If `None`, there is no upper bound.
"""
Args:
lower: Deprecated alias for
[`Interval.start`][frequenz.core.math.Interval.start].
upper: Deprecated alias for
[`Interval.end`][frequenz.core.math.Interval.end].
"""
super().__init__(lower, upper)

@property
@deprecated("`Bounds.lower` is deprecated; use `Interval.start` instead.")
def lower(self) -> float | None:
"""Return the lower bound (deprecated alias for `start`).

Deprecated:
Use [`start`][frequenz.core.math.Interval.start] instead.

Returns:
The value stored in [`start`][frequenz.core.math.Interval.start].
"""
return self.start

@property
@deprecated("`Bounds.upper` is deprecated; use `Interval.end` instead.")
def upper(self) -> float | None:
"""Return the upper bound (deprecated alias for `end`).

Deprecated:
Use [`end`][frequenz.core.math.Interval.end] instead.

Returns:
The value stored in [`end`][frequenz.core.math.Interval.end].
"""
return self.end

def __eq__(self, other: object) -> bool:
"""Return whether this bound matches another interval-like object."""
if isinstance(other, Interval):
other_interval = cast(Interval[float | None], other)
return self.start == other_interval.start and self.end == other_interval.end
return False

def __post_init__(self) -> None:
"""Validate these bounds."""
if self.lower is None:
return
if self.upper is None:
return
if self.lower > self.upper:
raise ValueError(
f"Lower bound ({self.lower}) must be less than or equal to upper "
f"bound ({self.upper})"
)

def __str__(self) -> str:
"""Return a string representation of these bounds."""
return f"[{self.lower}, {self.upper}]"
def __hash__(self) -> int:
"""Return the hash for this interval-compatible bound."""
return hash((self.start, self.end))
8 changes: 4 additions & 4 deletions src/frequenz/client/common/metrics/_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from typing import assert_never

from frequenz.core.enum import Enum, deprecated_member, unique
from frequenz.core.math import Interval

from .._exception import UnrecognizedValueError, UnspecifiedValueError
from ._bounds import Bounds
from ._metric import Metric


Expand Down Expand Up @@ -199,7 +199,7 @@ class MetricSample:
value: float | AggregatedMetricValue | None
"""The value of the sampled metric."""

bounds: list[Bounds]
bounds: Sequence[Interval[float | None]]
"""The bounds that apply to the metric sample.

These bounds adapt in real-time to reflect the operating conditions at the time of
Expand All @@ -219,9 +219,9 @@ class MetricSample:
The diagram below illustrates the relationship between the bounds.

```
bound[0].lower bound[1].upper
bound[0].start bound[1].end
<-------|============|------------------|============|--------->
bound[0].upper bound[1].lower
bound[0].end bound[1].start

---- values here are disallowed and will be rejected
==== values here are allowed and will be accepted
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

"""Conversion of metrics enums from/to protobuf v1alpha8."""

from ._bounds import bounds_from_proto, bounds_from_proto_with_issues
from ._bounds import (
bounds_from_proto,
bounds_from_proto2,
bounds_from_proto_with_issues,
bounds_from_proto_with_issues2,
)
from ._metric import metric_from_proto, metric_to_proto
from ._metric_connection_category import (
metric_connection_category_from_proto,
Expand All @@ -18,7 +23,9 @@
__all__ = [
"aggregated_metric_sample_from_proto",
"bounds_from_proto",
"bounds_from_proto2",
"bounds_from_proto_with_issues",
"bounds_from_proto_with_issues2",
"metric_connection_category_from_proto",
"metric_connection_category_to_proto",
"metric_connection_from_proto_with_issues",
Expand Down
79 changes: 75 additions & 4 deletions src/frequenz/client/common/metrics/proto/v1alpha8/_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,25 @@

"""Loading of Bounds objects from protobuf messages."""

import warnings

from frequenz.api.common.v1alpha8.metrics import bounds_pb2
from frequenz.core.math import Interval
from typing_extensions import deprecated

from ..._bounds import Bounds


@deprecated(
"`bounds_from_proto` is deprecated; use `bounds_from_proto2` "
"(returns `Interval[float | None]`) instead."
)
def bounds_from_proto(message: bounds_pb2.Bounds) -> Bounds: # noqa: DOC502
"""Create a [`Bounds`][....Bounds] object from a protobuf message.

Deprecated:
Use [`bounds_from_proto2`][..bounds_from_proto2] instead.

Args:
message: The protobuf message to convert.

Expand All @@ -20,12 +31,39 @@ def bounds_from_proto(message: bounds_pb2.Bounds) -> Bounds: # noqa: DOC502
Raises:
ValueError: If the message is not valid.
"""
return Bounds(
lower=message.lower if message.HasField("lower") else None,
upper=message.upper if message.HasField("upper") else None,
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
return Bounds(
lower=message.lower if message.HasField("lower") else None,
upper=message.upper if message.HasField("upper") else None,
)


def bounds_from_proto2( # noqa: DOC502
message: bounds_pb2.Bounds,
) -> Interval[float | None]:
"""Create an [`Interval`][frequenz.core.math.Interval] object from a protobuf message.

Args:
message: The protobuf message to convert.

Returns:
The corresponding [`Interval`][frequenz.core.math.Interval] object.

Raises:
ValueError: If the message is not valid.
"""
return Interval(
message.lower if message.HasField("lower") else None,
message.upper if message.HasField("upper") else None,
)


@deprecated(
"`bounds_from_proto_with_issues` is deprecated; use "
"`bounds_from_proto_with_issues2` (returns `Interval[float | None] | None`) "
"instead."
)
def bounds_from_proto_with_issues(
message: bounds_pb2.Bounds,
*,
Expand All @@ -34,6 +72,10 @@ def bounds_from_proto_with_issues(
) -> Bounds | None: # noqa: DOC502
"""Create a [`Bounds`][....Bounds] object from a protobuf message, collecting issues.

Deprecated:
Use [`bounds_from_proto_with_issues2`][..bounds_from_proto_with_issues2]
instead.

Args:
message: The protobuf message to convert.
major_issues: A list to append major issues to.
Expand All @@ -43,7 +85,36 @@ def bounds_from_proto_with_issues(
The corresponding [`Bounds`][....Bounds] object.
"""
try:
return bounds_from_proto(message)
# `bounds_from_proto` is itself `@deprecated`; suppress its warning so
# callers see only the outer `bounds_from_proto_with_issues` notice.
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
return bounds_from_proto(message)
except ValueError as exc:
major_issues.append(str(exc))
return None


def bounds_from_proto_with_issues2(
message: bounds_pb2.Bounds,
*,
major_issues: list[str],
minor_issues: list[str], # pylint: disable=unused-argument
) -> Interval[float | None] | None: # noqa: DOC502
"""Create an [`Interval`][frequenz.core.math.Interval] object from a protobuf message.

Collect issues.

Args:
message: The protobuf message to convert.
major_issues: A list to append major issues to.
minor_issues: A list to append minor issues to.

Returns:
The corresponding [`Interval`][frequenz.core.math.Interval] object.
"""
try:
return bounds_from_proto2(message)
except ValueError as exc:
major_issues.append(str(exc))
return None
14 changes: 7 additions & 7 deletions src/frequenz/client/common/metrics/proto/v1alpha8/_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
from collections.abc import Sequence

from frequenz.api.common.v1alpha8.metrics import bounds_pb2, metrics_pb2
from frequenz.core.math import Interval

from ....proto import datetime_from_proto
from ..._bounds import Bounds
from ..._metric import Metric
from ..._sample import (
AggregatedMetricValue,
MetricConnection,
MetricConnectionCategory,
MetricSample,
)
from ._bounds import bounds_from_proto
from ._bounds import bounds_from_proto2
from ._metric import metric_from_proto
from ._metric_connection_category import metric_connection_category_from_proto

Expand Down Expand Up @@ -130,8 +130,8 @@ def _metric_bounds_from_proto(
*,
major_issues: list[str],
minor_issues: list[str], # pylint:disable=unused-argument
) -> list[Bounds]:
"""Convert a sequence of bounds messages to a list of [`Bounds`][....Bounds].
) -> list[Interval[float | None]]:
"""Convert a sequence of bounds messages to a list of [`Interval`][frequenz.core.math.Interval].

Args:
metric: The metric for which the bounds are defined, used for logging issues.
Expand All @@ -140,12 +140,12 @@ def _metric_bounds_from_proto(
minor_issues: A list to append minor issues to.

Returns:
The resulting list of [`Bounds`][....Bounds].
The resulting list of [`Interval`][frequenz.core.math.Interval].
"""
bounds: list[Bounds] = []
bounds: list[Interval[float | None]] = []
for pb_bound in messages:
try:
bound = bounds_from_proto(pb_bound)
bound = bounds_from_proto2(pb_bound)
except ValueError as exc:
metric_name = metric if isinstance(metric, int) else metric.name
major_issues.append(
Expand Down
Loading
Loading