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
28 changes: 26 additions & 2 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,38 @@

## Upgrading

<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
* The `UNSPECIFIED` members in the following enums are now deprecated:

* `frequenz.client.common.grid.EnergyMarketCodeType`
* `frequenz.client.common.metrics.Metric`
* `frequenz.client.common.metrics.MetricConnectionCategory`

When loading these types from protobuf using dataclass-level converters (e.g., `delivery_area_from_proto`, `metric_sample_from_proto`), the low-level fields (`code_type`, `category`, `metric`) now store the raw integer `0` for unspecified values instead of the deprecated member. Unspecified values should be rare errors, so it is better to expose them only via the low-level interface.

Lower-level enum-level converters still return the deprecated member.

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.

## New Features

* Added new exceptions:

* `frequenz.client.common.ClientCommonError` as a base exception for the package.
* `frequenz.client.common.UnspecifiedValueError` for unspecified values (raw `0` or the deprecated member).
* `frequenz.client.common.UnrecognizedValueError` 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:

* `frequenz.client.common.grid.DeliveryArea.get_code_type()`
* `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.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.ClientCommonError` base exception and `UnspecifiedValueError` at the package root.

* 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.

## Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ requires-python = ">= 3.11, < 4"
dependencies = [
"typing-extensions >= 4.13.0, < 5",
"frequenz-api-common >= 0.8.4, < 1",
"frequenz-core >= 1.0.2, < 2",
"frequenz-core >= 1.3.0, < 2",
"protobuf >= 6.33.6, < 8",
]
dynamic = ["version"]
Expand Down
7 changes: 6 additions & 1 deletion src/frequenz/client/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

"""Common code and utilities for Frequenz API clients."""

from ._exception import ClientCommonError, UnspecifiedValueError
from ._exception import (
ClientCommonError,
UnrecognizedValueError,
UnspecifiedValueError,
)

__all__ = [
"ClientCommonError",
"UnrecognizedValueError",
"UnspecifiedValueError",
]
30 changes: 29 additions & 1 deletion src/frequenz/client/common/_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,36 @@ class ClientCommonError(Exception):
"""Base class for all errors raised by frequenz-client-common."""


class UnrecognizedValueError(ClientCommonError, ValueError):
"""Raised when a semantic accessor sees an unrecognized protobuf value.

This happens when the server sets an enum value that this version of the
client does not recognize, as opposed to an unspecified value (see
[`UnspecifiedValueError`][..UnspecifiedValueError]). The raw
unrecognized value is available as `value`.

This is also a ``ValueError`` for convenience.
"""

def __init__(self, value: int, message: str | None = None) -> None:
"""Initialize this error.

Args:
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
super().__init__(
message if message is not None else f"unrecognized enum value: {value!r}"
)


class UnspecifiedValueError(ClientCommonError, ValueError):
"""Raised when a semantic accessor sees an unspecified or unknown protobuf value.
"""Raised when a semantic accessor sees an unspecified protobuf value.

For a value that is set but not recognized by this client, see
[`UnrecognizedValueError`][..UnrecognizedValueError].

This is also a [`ValueError`][] for convenience.
"""
53 changes: 49 additions & 4 deletions src/frequenz/client/common/grid/_delivery_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@

"""Delivery area information for the energy market."""

import enum
import warnings
from dataclasses import dataclass
from typing import assert_never

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

@enum.unique
class EnergyMarketCodeType(enum.Enum):
from .._exception import UnrecognizedValueError, UnspecifiedValueError


@unique
class EnergyMarketCodeType(Enum):
"""The identification code types used in the energy market.

CodeType specifies the type of identification code used for uniquely
Expand All @@ -35,7 +40,11 @@ class EnergyMarketCodeType(enum.Enum):
processing errors.
"""

UNSPECIFIED = 0
UNSPECIFIED = deprecated_member(
0,
"EnergyMarketCodeType.UNSPECIFIED is deprecated; use the `int` value `0` "
"instead if you really need to check for this low-level value.",
)
"""Unspecified type. This value is a placeholder and should not be used."""

EUROPE_EIC = 1
Expand Down Expand Up @@ -74,6 +83,9 @@ class DeliveryArea:

This code could be extended in the future, in case an unknown code type is
encountered, a plain integer value is used to represent it.

This is the lower-level, forward-compatible accessor; prefer
`DeliveryArea.get_code_type()` to obtain a known member or a clear error.
"""

def __str__(self) -> str:
Expand All @@ -85,3 +97,36 @@ def __str__(self) -> str:
else self.code_type.name
)
return f"{code}[{code_type}]"

def get_code_type(self) -> EnergyMarketCodeType:
"""Return the code type as a known enum member.

This is the higher-level accessor for the `code_type` attribute: it
resolves the value to a known `EnergyMarketCodeType` member or raises a
clear, catchable error.

Returns:
The code type, when it is a known `EnergyMarketCodeType` member.

Raises:
UnspecifiedValueError: If the code type is unspecified.
UnrecognizedValueError: If the code type is a value not recognized by
this version of the client. The raw value is available on the
exception's `value` attribute.
"""
# Suppressing the deprecation warning can be removed when UNSPECIFIED is removed
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
match self.code_type:
case 0 | EnergyMarketCodeType.UNSPECIFIED:
raise UnspecifiedValueError(f"code type of {self} is unspecified")
case EnergyMarketCodeType() as code_type:
return code_type
case int() as code_type:
raise UnrecognizedValueError(
code_type,
f"code type {code_type!r} of {self} is not a recognized "
"EnergyMarketCodeType",
)
case unknown:
assert_never(unknown)
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ def delivery_area_from_proto(message: delivery_area_pb2.DeliveryArea) -> Deliver
if code is None:
issues.append("code is empty")

code_type = energy_market_code_type_from_proto(message.code_type)
if code_type is EnergyMarketCodeType.UNSPECIFIED:
raw_code_type = message.code_type
code_type: EnergyMarketCodeType | int = (
raw_code_type
if raw_code_type == 0
else energy_market_code_type_from_proto(raw_code_type)
)
if raw_code_type == 0:
issues.append("code_type is unspecified")
elif isinstance(code_type, int):
issues.append("code_type is unrecognized")
Expand Down
12 changes: 8 additions & 4 deletions src/frequenz/client/common/metrics/_metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

"""Supported metrics for microgrid components."""

import enum
from frequenz.core import enum as core_enum


@enum.unique
class Metric(enum.Enum):
@core_enum.unique
class Metric(core_enum.Enum):
"""List of supported metrics.

Metric units are as follows:
Expand Down Expand Up @@ -39,7 +39,11 @@ class Metric(enum.Enum):
period, and therefore can be inconsistent.
"""

UNSPECIFIED = 0
UNSPECIFIED = core_enum.deprecated_member(
0,
"Metric.UNSPECIFIED is deprecated; use the `int` value `0` "
"instead if you really need to check for this low-level value.",
)
"""The metric is unspecified (this should not be used)."""

DC_VOLTAGE = 1
Expand Down
97 changes: 92 additions & 5 deletions src/frequenz/client/common/metrics/_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
"""Definition to work with metric sample values."""

import enum
import warnings
from collections.abc import Sequence
from dataclasses import dataclass
from datetime import datetime
from typing import assert_never

from frequenz.core import enum as core_enum

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

Expand Down Expand Up @@ -66,11 +70,15 @@ def __str__(self) -> str:
return f"avg:{self.avg}{extra_str}"


@enum.unique
class MetricConnectionCategory(enum.Enum):
@core_enum.unique
class MetricConnectionCategory(core_enum.Enum):
"""The categories of connections from which metrics can be obtained."""

UNSPECIFIED = 0
UNSPECIFIED = core_enum.deprecated_member(
0,
"MetricConnectionCategory.UNSPECIFIED is deprecated; use the `int` value `0` "
"instead if you really need to check for this low-level value.",
)
"""The connection category was not specified (do not use)."""

OTHER = 1
Expand Down Expand Up @@ -100,7 +108,13 @@ class MetricConnection:
"""A connection from which a metric was obtained."""

category: MetricConnectionCategory | int
"""The category of the connection from which the metric was obtained."""
"""The category of the connection from which the metric was obtained.

This is the lower-level, forward-compatible accessor: it may hold a known
`MetricConnectionCategory` member, the raw `int` `0` when the category is
unspecified, or any other raw `int` not yet known to this client. Prefer
`MetricConnection.get_category()` to obtain a known member or a clear error.
"""

name: str | None = None
"""The name of the specific connection from which the metric was obtained.
Expand All @@ -122,6 +136,40 @@ def __str__(self) -> str:
return f"{category_name}({self.name})"
return category_name

def get_category(self) -> MetricConnectionCategory:
"""Return the connection category as a known enum member.

This is the higher-level accessor for the lower-level
[`category`][frequenz.client.common.metrics.MetricConnection.category]
field: it returns a known member or raises instead of exposing the raw
sentinel `0` or an unknown `int`.

Returns:
The category when it is a known `MetricConnectionCategory` member.

Raises:
UnspecifiedValueError: If the category is unspecified (the raw value
`0` or a member whose value is `0`).
UnrecognizedValueError: If the category is an `int` this client does
not recognize. The raw value is available on the error's `value`
attribute.
"""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
match self.category:
case 0 | MetricConnectionCategory.UNSPECIFIED:
raise UnspecifiedValueError("connection category is unspecified")
case MetricConnectionCategory():
return self.category
case int():
raise UnrecognizedValueError(
self.category,
f"connection category {self.category!r} is not a recognized "
"MetricConnectionCategory",
)
case unexpected:
assert_never(unexpected)


@dataclass(frozen=True, kw_only=True)
class MetricSample:
Expand All @@ -141,7 +189,13 @@ class MetricSample:
"""The moment when the metric was sampled."""

metric: Metric | int
"""The metric that was sampled."""
"""The metric that was sampled.

This is the lower-level, forward-compatible accessor: it may hold a known
`Metric` member, the raw `int` `0` when the metric is unspecified, or any
other raw `int` not yet known to this client. Prefer
`MetricSample.get_metric()` to obtain a known member or a clear error.
"""

value: float | AggregatedMetricValue | None
"""The value of the sampled metric."""
Expand Down Expand Up @@ -227,3 +281,36 @@ def as_single_value(
return None
case unexpected:
assert_never(unexpected)

def get_metric(self) -> Metric:
"""Return the sampled metric as a known enum member.

This is the higher-level accessor for the lower-level
[`metric`][frequenz.client.common.metrics.MetricSample.metric] field: it
returns a known member or raises instead of exposing the raw sentinel
`0` or an unknown `int`.

Returns:
The metric when it is a known `Metric` member.

Raises:
UnspecifiedValueError: If the metric is unspecified (the raw value
`0` or a member whose value is `0`).
UnrecognizedValueError: If the metric is an `int` this client does
not recognize. The raw value is available on the error's `value`
attribute.
"""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=DeprecationWarning)
match self.metric:
case 0 | Metric.UNSPECIFIED:
raise UnspecifiedValueError("sampled metric is unspecified")
case Metric():
return self.metric
case int():
raise UnrecognizedValueError(
self.metric,
f"sampled metric {self.metric!r} is not a recognized Metric",
)
case unexpected:
assert_never(unexpected)
Loading
Loading