Skip to content

Commit cc8c143

Browse files
authored
Typing improvements (#274)
Earlier there were a number of `getattr` that was hiding the actual types of values going into `MetricSample`s. This PR replaces all such instances, so that only strongly typed values go into `MetricSample`s.
2 parents bb41c2d + ae36b7b commit cc8c143

File tree

2 files changed

+102
-29
lines changed

2 files changed

+102
-29
lines changed

RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
## Upgrading
88

9-
<!-- Here goes notes on how to upgrade from previous versions, including deprecations and what they should be replaced with -->
9+
- `MetricSample` values now have correct type-hints.
1010

1111
## New Features
1212

src/frequenz/client/reporting/_types.py

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,39 @@
44
"""Types for the Reporting API client."""
55

66
import math
7-
from collections.abc import Iterable, Iterator
7+
from collections.abc import Iterator, MutableSequence
88
from dataclasses import dataclass
99
from datetime import datetime
10-
from typing import Any, NamedTuple
10+
from typing import Any, Callable, Generic, NamedTuple, Protocol, TypeVar, cast
1111

1212
# pylint: disable=no-name-in-module
13+
from frequenz.api.common.v1alpha8.metrics.metrics_pb2 import (
14+
MetricSample as PbMetricSample,
15+
)
16+
from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import (
17+
ElectricalComponentDiagnostic as PbElectricalComponentDiagnostic,
18+
)
19+
from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import (
20+
ElectricalComponentStateCode as PbElectricalComponentStateCode,
21+
)
22+
from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import (
23+
ElectricalComponentStateSnapshot as PbElectricalComponentStateSnapshot,
24+
)
25+
from frequenz.api.common.v1alpha8.microgrid.electrical_components.electrical_components_pb2 import (
26+
ElectricalComponentTelemetry as PbElectricalComponentTelemetry,
27+
)
28+
from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import (
29+
SensorDiagnostic as PbSensorDiagnostic,
30+
)
31+
from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import (
32+
SensorStateCode as PbSensorStateCode,
33+
)
34+
from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import (
35+
SensorStateSnapshot as PbSensorStateSnapshot,
36+
)
37+
from frequenz.api.common.v1alpha8.microgrid.sensors.sensors_pb2 import (
38+
SensorTelemetry as PbSensorTelemetry,
39+
)
1340
from frequenz.api.reporting.v1alpha10.reporting_pb2 import (
1441
ReceiveAggregatedMicrogridComponentsDataStreamResponse as PBAggregatedStreamResponse,
1542
)
@@ -35,13 +62,46 @@ class MetricSample(NamedTuple):
3562

3663
timestamp: datetime
3764
microgrid_id: int
38-
component_id: str
65+
component_id: int | str
3966
metric: str
40-
value: float
67+
value: (
68+
float
69+
| PbElectricalComponentStateCode.ValueType
70+
| PbSensorStateCode.ValueType
71+
| PbElectricalComponentDiagnostic
72+
| PbSensorDiagnostic
73+
)
74+
75+
76+
class _PbMicrogridTelem(Protocol):
77+
"""Protocol for microgrid telemetry from the Reporting API client."""
78+
79+
@property
80+
def microgrid_id(self) -> int:
81+
"""Return the microgrid ID of the telemetry batch."""
82+
83+
84+
class _PbComponentTelem(Protocol):
85+
"""Protocol for telemetry items in the Reporting API client."""
86+
87+
@property
88+
def metric_samples(self) -> MutableSequence[PbMetricSample]:
89+
"""Return the metric samples of the telemetry item."""
90+
91+
@property
92+
def state_snapshots(self) -> MutableSequence[Any]:
93+
"""List of state snapshots associated with this telemetry item."""
94+
95+
96+
_MicrogridTelemT = TypeVar("_MicrogridTelemT", bound=_PbMicrogridTelem)
97+
_ComponentTelemT = TypeVar("_ComponentTelemT", bound=_PbComponentTelem)
98+
_StateSnapshotT = TypeVar(
99+
"_StateSnapshotT", bound=PbElectricalComponentStateSnapshot | PbSensorStateSnapshot
100+
)
41101

42102

43103
@dataclass(frozen=True)
44-
class GenericDataBatch:
104+
class GenericDataBatch(Generic[_MicrogridTelemT, _ComponentTelemT, _StateSnapshotT]):
45105
"""Base class for batches of microgrid data (components or sensors).
46106
47107
This class serves as a base for handling batches of data related to microgrid
@@ -50,9 +110,9 @@ class GenericDataBatch:
50110
functionality to work with bounds if applicable.
51111
"""
52112

53-
_data_pb: Any
54-
id_attr: str
55-
items_attr: str
113+
_data_pb: _MicrogridTelemT
114+
id_fetcher: Callable[[_ComponentTelemT], int]
115+
items_fetcher: Callable[[_MicrogridTelemT], MutableSequence[_ComponentTelemT]]
56116
has_bounds: bool = False
57117

58118
def is_empty(self) -> bool:
@@ -61,15 +121,13 @@ def is_empty(self) -> bool:
61121
Returns:
62122
True if the batch contains no valid data.
63123
"""
64-
items = getattr(self._data_pb, self.items_attr, [])
124+
items = self.items_fetcher(self._data_pb)
65125
if not items:
66126
return True
67127
for item in items:
68-
if not getattr(item, "metric_samples", []) and not getattr(
69-
item, "states", []
70-
):
71-
return True
72-
return False
128+
if item.metric_samples or item.state_snapshots:
129+
return False
130+
return True
73131

74132
# pylint: disable=too-many-locals
75133
# pylint: disable=too-many-branches
@@ -89,11 +147,11 @@ def __iter__(self) -> Iterator[MetricSample]:
89147
* value: The metric value.
90148
"""
91149
mid = self._data_pb.microgrid_id
92-
items = getattr(self._data_pb, self.items_attr)
150+
items = self.items_fetcher(self._data_pb)
93151

94152
for item in items:
95-
cid = getattr(item, self.id_attr)
96-
for sample in getattr(item, "metric_samples", []):
153+
cid = self.id_fetcher(item)
154+
for sample in item.metric_samples:
97155
ts = datetime_from_proto(sample.sample_time)
98156
met = enum_from_proto(sample.metric, Metric, allow_invalid=False).name
99157

@@ -128,21 +186,26 @@ def __iter__(self) -> Iterator[MetricSample]:
128186
ts, mid, cid, f"{met}_bound_{i}_upper", upper
129187
)
130188

131-
for state in getattr(item, "state_snapshots", []):
189+
for state in item.state_snapshots:
190+
state = cast(_StateSnapshotT, state)
132191
ts = datetime_from_proto(state.origin_time)
133192
for category, category_items in {
134-
"state": getattr(state, "states", []),
135-
"warning": getattr(state, "warnings", []),
136-
"error": getattr(state, "errors", []),
193+
"state": state.states,
194+
"warning": state.warnings,
195+
"error": state.errors,
137196
}.items():
138-
if not isinstance(category_items, Iterable):
139-
continue
140197
for s in category_items:
141198
yield MetricSample(ts, mid, cid, category, s)
142199

143200

144201
@dataclass(frozen=True)
145-
class ComponentsDataBatch(GenericDataBatch):
202+
class ComponentsDataBatch(
203+
GenericDataBatch[
204+
PBReceiveMicrogridComponentsDataStreamResponse,
205+
PbElectricalComponentTelemetry,
206+
PbElectricalComponentStateSnapshot,
207+
]
208+
):
146209
"""Batch of microgrid components data."""
147210

148211
def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse):
@@ -153,14 +216,20 @@ def __init__(self, data_pb: PBReceiveMicrogridComponentsDataStreamResponse):
153216
"""
154217
super().__init__(
155218
data_pb,
156-
id_attr="electrical_component_id",
157-
items_attr="components",
219+
id_fetcher=lambda item: item.electrical_component_id,
220+
items_fetcher=lambda pb: pb.components,
158221
has_bounds=True,
159222
)
160223

161224

162225
@dataclass(frozen=True)
163-
class SensorsDataBatch(GenericDataBatch):
226+
class SensorsDataBatch(
227+
GenericDataBatch[
228+
PBReceiveMicrogridSensorsDataStreamResponse,
229+
PbSensorTelemetry,
230+
PbSensorStateSnapshot,
231+
]
232+
):
164233
"""Batch of microgrid sensors data."""
165234

166235
def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse):
@@ -169,7 +238,11 @@ def __init__(self, data_pb: PBReceiveMicrogridSensorsDataStreamResponse):
169238
Args:
170239
data_pb: The underlying protobuf message.
171240
"""
172-
super().__init__(data_pb, id_attr="sensor_id", items_attr="sensors")
241+
super().__init__(
242+
data_pb,
243+
id_fetcher=lambda item: item.sensor_id,
244+
items_fetcher=lambda pb: pb.sensors,
245+
)
173246

174247

175248
@dataclass(frozen=True)

0 commit comments

Comments
 (0)