diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 80e5f0cb..063a2b57 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,16 +2,13 @@ ## Summary - +This is a small release to allow for easier interoperability between different APIs. ## Upgrading - +- Some minimum dependency versions are bumped, so you might need to update your dependencies as well. +- The IDs (`MicrogridId`, `ComponentId`, `SensorId`) are now imported from `frequenz-client-common`. Please add it to your dependencies if you haven't already, then you can replace your imports: -## New Features - - - -## Bug Fixes - - + * `from frequenz.client.microgrid import MicrogridId` -> `from frequenz.client.common.microgrid import MicrogridId` + * `from frequenz.client.microgrid import ComponentId` -> `from frequenz.client.common.microgrid.components import ComponentId` + * `from frequenz.client.microgrid import SensorId` -> `from frequenz.client.common.microgrid.sensors import SensorId` diff --git a/pyproject.toml b/pyproject.toml index 2a0290e6..d3015749 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,13 +36,14 @@ classifiers = [ ] requires-python = ">= 3.11, < 4" dependencies = [ - "frequenz-api-microgrid >= 0.15.3, < 0.16.0", + "frequenz-api-microgrid >= 0.15.5, < 0.16.0", "frequenz-channels >= 1.0.0-rc1, < 2.0.0", "frequenz-client-base >= 0.8.0, < 0.12.0", - "grpcio >= 1.59.0, < 2", - "protobuf >= 4.21.6, < 7", + "frequenz-client-common >= 0.3.2, < 0.4.0", + "grpcio >= 1.63.0, < 2", + "protobuf >= 5.26.1, < 7", "timezonefinder >= 6.2.0, < 7", - "typing-extensions >= 4.6.0, < 5", + "typing-extensions >= 4.13.0, < 5", ] dynamic = ["version"] @@ -161,7 +162,15 @@ disable = [ ] [tool.pytest.ini_options] -addopts = "-W=all -Werror -Wdefault::DeprecationWarning -Wdefault::PendingDeprecationWarning -vv" +addopts = "-vv" +filterwarnings = [ + "error", + "once::DeprecationWarning", + "once::PendingDeprecationWarning", + # We use a raw string (single quote) to avoid the need to escape special + # chars as this is a regex + 'ignore:Protobuf gencode version .*exactly one major version older.*:UserWarning', +] testpaths = ["tests", "src"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" diff --git a/src/frequenz/client/microgrid/_client.py b/src/frequenz/client/microgrid/_client.py index f1cb65c7..4f8cdcb7 100644 --- a/src/frequenz/client/microgrid/_client.py +++ b/src/frequenz/client/microgrid/_client.py @@ -17,6 +17,9 @@ from frequenz.api.microgrid import microgrid_pb2, microgrid_pb2_grpc, sensor_pb2 from frequenz.channels import Receiver from frequenz.client.base import channel, client, retry, streaming +from frequenz.client.common.microgrid import MicrogridId +from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.sensors import SensorId from google.protobuf.empty_pb2 import Empty from typing_extensions import override @@ -39,7 +42,6 @@ from ._exception import ApiClientError, ClientNotConnected from ._metadata import Location, Metadata from ._sensor_proto import sensor_data_samples_from_proto, sensor_from_proto -from .id import ComponentId, MicrogridId, SensorId from .sensor import Sensor, SensorDataSamples, SensorMetric DEFAULT_GRPC_CALL_TIMEOUT = 60.0 diff --git a/src/frequenz/client/microgrid/_component.py b/src/frequenz/client/microgrid/_component.py index ceed5013..6aa42a20 100644 --- a/src/frequenz/client/microgrid/_component.py +++ b/src/frequenz/client/microgrid/_component.py @@ -8,8 +8,7 @@ from frequenz.api.common import components_pb2 from frequenz.api.microgrid import grid_pb2, inverter_pb2 - -from .id import ComponentId +from frequenz.client.common.microgrid.components import ComponentId class ComponentType(Enum): diff --git a/src/frequenz/client/microgrid/_component_data.py b/src/frequenz/client/microgrid/_component_data.py index 983021f8..b72b4d2e 100644 --- a/src/frequenz/client/microgrid/_component_data.py +++ b/src/frequenz/client/microgrid/_component_data.py @@ -9,6 +9,7 @@ from typing import Self from frequenz.api.microgrid import microgrid_pb2 +from frequenz.client.common.microgrid.components import ComponentId from ._component_error import BatteryError, InverterError from ._component_states import ( @@ -18,7 +19,6 @@ EVChargerComponentState, InverterComponentState, ) -from .id import ComponentId @dataclass(frozen=True) diff --git a/src/frequenz/client/microgrid/_connection.py b/src/frequenz/client/microgrid/_connection.py index 343098b2..b0b3dac7 100644 --- a/src/frequenz/client/microgrid/_connection.py +++ b/src/frequenz/client/microgrid/_connection.py @@ -6,7 +6,7 @@ from dataclasses import dataclass -from .id import ComponentId +from frequenz.client.common.microgrid.components import ComponentId @dataclass(frozen=True) diff --git a/src/frequenz/client/microgrid/_metadata.py b/src/frequenz/client/microgrid/_metadata.py index 3af91ecb..135dafca 100644 --- a/src/frequenz/client/microgrid/_metadata.py +++ b/src/frequenz/client/microgrid/_metadata.py @@ -6,10 +6,9 @@ from dataclasses import dataclass from zoneinfo import ZoneInfo +from frequenz.client.common.microgrid import MicrogridId from timezonefinder import TimezoneFinder -from .id import MicrogridId - _timezone_finder = TimezoneFinder() diff --git a/src/frequenz/client/microgrid/_sensor_proto.py b/src/frequenz/client/microgrid/_sensor_proto.py index 1a0c5eee..9df23299 100644 --- a/src/frequenz/client/microgrid/_sensor_proto.py +++ b/src/frequenz/client/microgrid/_sensor_proto.py @@ -10,10 +10,10 @@ from frequenz.api.common import components_pb2 from frequenz.api.microgrid import common_pb2, microgrid_pb2, sensor_pb2 from frequenz.client.base import conversion +from frequenz.client.common.microgrid.sensors import SensorId from ._lifetime import Lifetime from ._util import enum_from_proto -from .id import SensorId from .sensor import ( Sensor, SensorDataSamples, diff --git a/src/frequenz/client/microgrid/id.py b/src/frequenz/client/microgrid/id.py deleted file mode 100644 index b1b1aa21..00000000 --- a/src/frequenz/client/microgrid/id.py +++ /dev/null @@ -1,218 +0,0 @@ -# License: MIT -# Copyright © 2025 Frequenz Energy-as-a-Service GmbH - -r'''Provides strongly-typed unique identifiers for entities. - -This module offers a base class, -[`BaseId`][frequenz.client.microgrid.id.BaseId], which can be subclassed to -create distinct ID types for different components or concepts within a system. -These IDs ensure type safety, meaning that an ID for one type of entity (e.g., a -sensor) cannot be mistakenly used where an ID for another type (e.g., a -microgrid) is expected. - -# Creating Custom ID Types - -To define a new ID type, create a class that inherits from -[`BaseId`][frequenz.client.microgrid.id.BaseId] and provide a unique -`str_prefix` as a keyword argument in the class definition. This prefix is used -in the string representation of the ID and must be unique across all ID types. - -Note: - The `str_prefix` must be unique across all ID types. If you try to use a - prefix that is already registered, a `ValueError` will be raised when defining - the class. - -To encourage consistency, the class name must end with the suffix "Id" (e.g., -`MyNewId`). This check can be bypassed by passing `allow_custom_name=True` when -defining the class (e.g., `class MyCustomName(BaseId, str_prefix="MCN", -allow_custom_name=True):`). - -Tip: - Use the [`@typing.final`][typing.final] decorator to prevent subclassing of - ID classes. - -Example: Creating a standard ID type - ```python - from typing import final - from frequenz.client.microgrid.id import BaseId - - @final - class InverterId(BaseId, str_prefix="INV"): - """A unique identifier for an inverter.""" - - inv_id = InverterId(123) - print(inv_id) # Output: INV123 - print(int(inv_id)) # Output: 123 - ``` - -Example: Creating an ID type with a non-standard name - ```python - from typing import final - from frequenz.client.microgrid.id import BaseId - - @final - class CustomNameForId(BaseId, str_prefix="CST", allow_custom_name=True): - """An ID with a custom name, not ending in 'Id'.""" - - custom_id = CustomNameForId(456) - print(custom_id) # Output: CST456 - print(int(custom_id)) # Output: 456 - ``` - -# Predefined ID Types - -This module predefines the following ID types: - -- [`ComponentId`][frequenz.client.microgrid.id.ComponentId]: For identifying - generic components. -- [`MicrogridId`][frequenz.client.microgrid.id.MicrogridId]: For identifying - microgrids. -- [`SensorId`][frequenz.client.microgrid.id.SensorId]: For identifying sensors. -''' - - -from typing import Any, ClassVar, Self, cast, final - - -class BaseId: - """A base class for unique identifiers. - - Subclasses must provide a unique `str_prefix` keyword argument during - definition, which is used in the string representation of the ID. - - By default, subclass names must end with "Id". This can be overridden by - passing `allow_custom_name=True` during class definition. - - For more information and examples, see the [module's - documentation][frequenz.client.microgrid.id]. - """ - - _id: int - _str_prefix: ClassVar[str] - _registered_prefixes: ClassVar[set[str]] = set() - - def __new__(cls, *_: Any, **__: Any) -> Self: - """Create a new instance of the ID class, only if it is a subclass of BaseId.""" - if cls is BaseId: - raise TypeError("BaseId cannot be instantiated directly. Use a subclass.") - return super().__new__(cls) - - def __init_subclass__( - cls, - *, - str_prefix: str, - allow_custom_name: bool = False, - **kwargs: Any, - ) -> None: - """Initialize a subclass, set its string prefix, and perform checks. - - Args: - str_prefix: The string prefix for the ID type (e.g., "MID"). - Must be unique across all ID types. - allow_custom_name: If True, bypasses the check that the class name - must end with "Id". Defaults to False. - **kwargs: Forwarded to the parent's __init_subclass__. - - Raises: - ValueError: If the `str_prefix` is already registered by another - ID type. - TypeError: If `allow_custom_name` is False and the class name - does not end with "Id". - """ - super().__init_subclass__(**kwargs) - - if str_prefix in BaseId._registered_prefixes: - raise ValueError( - f"Prefix '{str_prefix}' is already registered. " - "ID prefixes must be unique." - ) - BaseId._registered_prefixes.add(str_prefix) - - if not allow_custom_name and not cls.__name__.endswith("Id"): - raise TypeError( - f"Class name '{cls.__name__}' for an ID class must end with 'Id' " - "(e.g., 'SomeId'), or use `allow_custom_name=True`." - ) - - cls._str_prefix = str_prefix - - def __init__(self, id_: int, /) -> None: - """Initialize this instance. - - Args: - id_: The numeric unique identifier. - - Raises: - ValueError: If the ID is negative. - """ - if id_ < 0: - raise ValueError(f"{type(self).__name__} can't be negative.") - self._id = id_ - - @property - def str_prefix(self) -> str: - """The prefix used for the string representation of this ID.""" - return self._str_prefix - - def __int__(self) -> int: - """Return the numeric ID of this instance.""" - return self._id - - def __eq__(self, other: object) -> bool: - """Check if this instance is equal to another object. - - Equality is defined as being of the exact same type and having the same - underlying ID. - """ - # pylint thinks this is not an unidiomatic typecheck, but in this case - # it is not. isinstance() returns True for subclasses, which is not - # what we want here, as different ID types should never be equal. - # pylint: disable-next=unidiomatic-typecheck - if type(other) is not type(self): - return NotImplemented - # We already checked type(other) is type(self), but mypy doesn't - # understand that, so we need to cast it to Self. - other_id = cast(Self, other) - return self._id == other_id._id - - def __lt__(self, other: object) -> bool: - """Check if this instance is less than another object. - - Comparison is only defined between instances of the exact same type. - """ - # pylint: disable-next=unidiomatic-typecheck - if type(other) is not type(self): - return NotImplemented - other_id = cast(Self, other) - return self._id < other_id._id - - def __hash__(self) -> int: - """Return the hash of this instance. - - The hash is based on the exact type and the underlying ID to ensure - that IDs of different types but with the same numeric value have different hashes. - """ - return hash((type(self), self._id)) - - def __repr__(self) -> str: - """Return the string representation of this instance.""" - return f"{type(self).__name__}({self._id!r})" - - def __str__(self) -> str: - """Return the short string representation of this instance.""" - return f"{type(self)._str_prefix}{self._id}" - - -@final -class MicrogridId(BaseId, str_prefix="MID"): - """A unique identifier for a microgrid.""" - - -@final -class ComponentId(BaseId, str_prefix="CID"): - """A unique identifier for a microgrid component.""" - - -@final -class SensorId(BaseId, str_prefix="SID"): - """A unique identifier for a microgrid sensor.""" diff --git a/src/frequenz/client/microgrid/sensor.py b/src/frequenz/client/microgrid/sensor.py index f057d39e..a2a1ba4f 100644 --- a/src/frequenz/client/microgrid/sensor.py +++ b/src/frequenz/client/microgrid/sensor.py @@ -36,9 +36,9 @@ from typing import assert_never from frequenz.api.microgrid import sensor_pb2 +from frequenz.client.common.microgrid.sensors import SensorId from ._lifetime import Lifetime -from .id import SensorId from .metrics import AggregatedMetricValue, AggregationMethod diff --git a/tests/test_client.py b/tests/test_client.py index e3aff688..d1029839 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -17,6 +17,9 @@ from frequenz.api.common import components_pb2, metrics_pb2 from frequenz.api.microgrid import grid_pb2, inverter_pb2, microgrid_pb2, sensor_pb2 from frequenz.client.base import conversion, retry +from frequenz.client.common.microgrid import MicrogridId +from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.sensors import SensorId from google.protobuf.empty_pb2 import Empty from frequenz.client.microgrid import ( @@ -34,7 +37,6 @@ MeterData, MicrogridApiClient, ) -from frequenz.client.microgrid.id import ComponentId, MicrogridId, SensorId from frequenz.client.microgrid.sensor import ( Sensor, SensorDataSamples, diff --git a/tests/test_component.py b/tests/test_component.py index 624e16d8..0fdc527d 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -5,13 +5,13 @@ import pytest from frequenz.api.common import components_pb2 +from frequenz.client.common.microgrid.components import ComponentId from frequenz.client.microgrid import ( Component, ComponentCategory, ) from frequenz.client.microgrid._component import component_category_from_protobuf -from frequenz.client.microgrid.id import ComponentId def test_component_category_from_protobuf() -> None: diff --git a/tests/test_component_data.py b/tests/test_component_data.py index f3205ed7..ce6ad559 100644 --- a/tests/test_component_data.py +++ b/tests/test_component_data.py @@ -9,6 +9,7 @@ from frequenz.api.common import metrics_pb2 from frequenz.api.common.metrics import electrical_pb2 from frequenz.api.microgrid import inverter_pb2, microgrid_pb2 +from frequenz.client.common.microgrid.components import ComponentId from google.protobuf import timestamp_pb2 from frequenz.client.microgrid import ( @@ -17,7 +18,6 @@ InverterData, InverterError, ) -from frequenz.client.microgrid.id import ComponentId def test_component_data_abstract_class() -> None: diff --git a/tests/test_connection.py b/tests/test_connection.py index 630c6e9a..0621a844 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -3,8 +3,9 @@ """Tests for the microgrid Connection type.""" +from frequenz.client.common.microgrid.components import ComponentId + from frequenz.client.microgrid import Connection -from frequenz.client.microgrid.id import ComponentId # pylint: disable=invalid-name diff --git a/tests/test_id.py b/tests/test_id.py index fa860ec3..d340413c 100644 --- a/tests/test_id.py +++ b/tests/test_id.py @@ -6,8 +6,9 @@ from dataclasses import dataclass import pytest - -from frequenz.client.microgrid.id import ComponentId, MicrogridId, SensorId +from frequenz.client.common.microgrid import MicrogridId +from frequenz.client.common.microgrid.components import ComponentId +from frequenz.client.common.microgrid.sensors import SensorId @dataclass(frozen=True) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 7fcb5373..1276d2b5 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -8,9 +8,9 @@ from zoneinfo import ZoneInfo import pytest +from frequenz.client.common.microgrid import MicrogridId from frequenz.client.microgrid import Location, Metadata -from frequenz.client.microgrid.id import MicrogridId @pytest.fixture diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 66067f7a..f86a2c11 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -7,9 +7,9 @@ from typing import Any import pytest +from frequenz.client.common.microgrid.sensors import SensorId from frequenz.client.microgrid import Lifetime -from frequenz.client.microgrid.id import SensorId from frequenz.client.microgrid.metrics import ( AggregatedMetricValue, AggregationMethod, diff --git a/tests/test_sensor_proto.py b/tests/test_sensor_proto.py index 1dbe57d0..3afa56df 100644 --- a/tests/test_sensor_proto.py +++ b/tests/test_sensor_proto.py @@ -12,6 +12,7 @@ from frequenz.api.common import components_pb2 from frequenz.api.microgrid import common_pb2, microgrid_pb2, sensor_pb2 from frequenz.client.base import conversion +from frequenz.client.common.microgrid.sensors import SensorId from frequenz.client.microgrid import Lifetime from frequenz.client.microgrid._sensor_proto import ( @@ -21,7 +22,6 @@ sensor_metric_sample_from_proto, sensor_state_sample_from_proto, ) -from frequenz.client.microgrid.id import SensorId from frequenz.client.microgrid.sensor import ( Sensor, SensorDataSamples,