Skip to content

Commit 6039c13

Browse files
authored
Merge pull request #73 from eduardiazf/fix/parsing-component-connection-errors
feat: Add `raise_on_errors` parameter for strict parsing validation
2 parents 23f020f + c06469d commit 6039c13

3 files changed

Lines changed: 313 additions & 29 deletions

File tree

src/frequenz/client/assets/_client.py

Lines changed: 128 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@
1818
from frequenz.client.common.microgrid.electrical_components import ElectricalComponentId
1919

2020
from ._microgrid import Microgrid
21-
from ._microgrid_proto import microgrid_from_proto
21+
from ._microgrid_proto import microgrid_from_proto, microgrid_from_proto_with_issues
2222
from .electrical_component._connection import ComponentConnection
23-
from .electrical_component._connection_proto import component_connection_from_proto
23+
from .electrical_component._connection_proto import (
24+
component_connection_from_proto,
25+
component_connection_from_proto_with_issues,
26+
)
2427
from .electrical_component._electrical_component import ElectricalComponent
25-
from .electrical_component._electrical_component_proto import electrical_component_proto
26-
from .exceptions import ClientNotConnected
28+
from .electrical_component._electrical_component_proto import (
29+
electrical_component_from_proto_with_issues,
30+
electrical_component_proto,
31+
)
32+
from .exceptions import (
33+
ClientNotConnected,
34+
InvalidConnectionError,
35+
InvalidElectricalComponentError,
36+
InvalidMicrogridError,
37+
)
2738

2839
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
2940
"""The default timeout for gRPC calls made by this client (in seconds)."""
@@ -88,21 +99,30 @@ def stub(self) -> assets_pb2_grpc.PlatformAssetsAsyncStub:
8899
# use the async stub, so we cast the sync stub to the async stub.
89100
return self._stub # type: ignore
90101

91-
async def get_microgrid( # noqa: DOC502 (raises ApiClientError indirectly)
92-
self, microgrid_id: MicrogridId
102+
async def get_microgrid( # noqa: DOC502,DOC503 (raises indirectly)
103+
self,
104+
microgrid_id: MicrogridId,
105+
*,
106+
raise_on_errors: bool = False,
93107
) -> Microgrid:
94108
"""
95109
Get the details of a microgrid.
96110
97111
Args:
98112
microgrid_id: The ID of the microgrid to get the details of.
113+
raise_on_errors: If True, raise an
114+
[InvalidMicrogridError][frequenz.client.assets.exceptions.InvalidMicrogridError]
115+
when major validation issues are found instead of just
116+
logging them.
99117
100118
Returns:
101119
The details of the microgrid.
102120
103121
Raises:
104122
ApiClientError: If there are any errors communicating with the Assets API,
105123
most likely a subclass of [GrpcError][frequenz.client.base.exception.GrpcError].
124+
InvalidMicrogridError: If `raise_on_errors` is True and major
125+
validation issues are found.
106126
"""
107127
response = await call_stub_method(
108128
self,
@@ -113,19 +133,48 @@ async def get_microgrid( # noqa: DOC502 (raises ApiClientError indirectly)
113133
method_name="GetMicrogrid",
114134
)
115135

136+
if raise_on_errors:
137+
major_issues: list[str] = []
138+
minor_issues: list[str] = []
139+
microgrid = microgrid_from_proto_with_issues(
140+
response.microgrid,
141+
major_issues=major_issues,
142+
minor_issues=minor_issues,
143+
)
144+
if major_issues:
145+
raise InvalidMicrogridError(
146+
microgrid=microgrid,
147+
major_issues=major_issues,
148+
minor_issues=minor_issues,
149+
raw_message=response.microgrid,
150+
)
151+
return microgrid
152+
116153
return microgrid_from_proto(response.microgrid)
117154

118155
async def list_microgrid_electrical_components(
119-
self, microgrid_id: MicrogridId
156+
self,
157+
microgrid_id: MicrogridId,
158+
*,
159+
raise_on_errors: bool = False,
120160
) -> list[ElectricalComponent]:
121161
"""
122162
Get the electrical components of a microgrid.
123163
124164
Args:
125165
microgrid_id: The ID of the microgrid to get the electrical components of.
166+
raise_on_errors: If True, raise an
167+
`ExceptionGroup[InvalidElectricalComponentError]`
168+
when major validation issues are found in any component instead
169+
of just logging them.
126170
127171
Returns:
128172
The electrical components of the microgrid.
173+
174+
Raises:
175+
ExceptionGroup: If `raise_on_errors` is True and major validation
176+
issues are found. All exceptions in the group are
177+
[InvalidElectricalComponentError][frequenz.client.assets.exceptions.InvalidElectricalComponentError].
129178
"""
130179
response = await call_stub_method(
131180
self,
@@ -138,6 +187,35 @@ async def list_microgrid_electrical_components(
138187
method_name="ListMicrogridElectricalComponents",
139188
)
140189

190+
if raise_on_errors:
191+
components: list[ElectricalComponent] = []
192+
exceptions: list[InvalidElectricalComponentError] = []
193+
for component_pb in response.components:
194+
major_issues: list[str] = []
195+
minor_issues: list[str] = []
196+
component = electrical_component_from_proto_with_issues(
197+
component_pb,
198+
major_issues=major_issues,
199+
minor_issues=minor_issues,
200+
)
201+
if major_issues:
202+
exceptions.append(
203+
InvalidElectricalComponentError(
204+
component=component,
205+
major_issues=major_issues,
206+
minor_issues=minor_issues,
207+
raw_message=component_pb,
208+
)
209+
)
210+
else:
211+
components.append(component)
212+
if exceptions:
213+
raise ExceptionGroup(
214+
f"{len(exceptions)} electrical component(s) failed validation",
215+
exceptions,
216+
)
217+
return components
218+
141219
return [
142220
electrical_component_proto(component) for component in response.components
143221
]
@@ -147,7 +225,9 @@ async def list_microgrid_electrical_component_connections(
147225
microgrid_id: MicrogridId,
148226
source_component_ids: Iterable[ElectricalComponentId] = (),
149227
destination_component_ids: Iterable[ElectricalComponentId] = (),
150-
) -> list[ComponentConnection | None]:
228+
*,
229+
raise_on_errors: bool = False,
230+
) -> list[ComponentConnection]:
151231
"""
152232
Get the electrical component connections of a microgrid.
153233
@@ -158,9 +238,18 @@ async def list_microgrid_electrical_component_connections(
158238
these component IDs. If None or empty, no filtering is applied.
159239
destination_component_ids: Only return connections that terminate at
160240
these component IDs. If None or empty, no filtering is applied.
241+
raise_on_errors: If True, raise an
242+
`ExceptionGroup[InvalidConnectionError]`
243+
when major validation issues are found in any connection instead
244+
of just logging them.
161245
162246
Returns:
163247
The electrical component connections of the microgrid.
248+
249+
Raises:
250+
ExceptionGroup: If `raise_on_errors` is True and major validation
251+
issues are found. All exceptions in the group are
252+
[InvalidConnectionError][frequenz.client.assets.exceptions.InvalidConnectionError].
164253
"""
165254
request = assets_pb2.ListMicrogridElectricalComponentConnectionsRequest(
166255
microgrid_id=int(microgrid_id),
@@ -177,9 +266,34 @@ async def list_microgrid_electrical_component_connections(
177266
method_name="ListMicrogridElectricalComponentConnections",
178267
)
179268

180-
return list(
181-
map(
182-
component_connection_from_proto,
183-
filter(bool, response.connections),
184-
)
185-
)
269+
if raise_on_errors:
270+
valid_connections: list[ComponentConnection] = []
271+
exceptions: list[InvalidConnectionError] = []
272+
for conn_pb in filter(bool, response.connections):
273+
major_issues: list[str] = []
274+
connection = component_connection_from_proto_with_issues(
275+
conn_pb, major_issues=major_issues
276+
)
277+
if major_issues:
278+
exceptions.append(
279+
InvalidConnectionError(
280+
connection=connection,
281+
major_issues=major_issues,
282+
minor_issues=[],
283+
raw_message=conn_pb,
284+
)
285+
)
286+
elif connection is not None:
287+
valid_connections.append(connection)
288+
if exceptions:
289+
raise ExceptionGroup(
290+
f"{len(exceptions)} connection(s) failed validation",
291+
exceptions,
292+
)
293+
return valid_connections
294+
295+
return [
296+
c
297+
for c in map(component_connection_from_proto, response.connections)
298+
if c is not None
299+
]

src/frequenz/client/assets/_microgrid_proto.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,46 @@ def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid:
3232
major_issues: list[str] = []
3333
minor_issues: list[str] = []
3434

35+
microgrid = microgrid_from_proto_with_issues(
36+
message, major_issues=major_issues, minor_issues=minor_issues
37+
)
38+
39+
if major_issues:
40+
_logger.warning(
41+
"Found issues in microgrid: %s | Protobuf message:\n%s",
42+
", ".join(major_issues),
43+
message,
44+
)
45+
46+
if minor_issues:
47+
_logger.debug(
48+
"Found minor issues in microgrid: %s | Protobuf message:\n%s",
49+
", ".join(minor_issues),
50+
message,
51+
)
52+
53+
return microgrid
54+
55+
56+
def microgrid_from_proto_with_issues(
57+
message: microgrid_pb2.Microgrid,
58+
*,
59+
major_issues: list[str],
60+
minor_issues: list[str],
61+
) -> Microgrid:
62+
"""Convert a protobuf microgrid message to a microgrid object, collecting issues.
63+
64+
This function is useful when you want to collect issues during parsing
65+
rather than logging them immediately.
66+
67+
Args:
68+
message: The protobuf message to convert.
69+
major_issues: A list to collect major issues found during validation.
70+
minor_issues: A list to collect minor issues found during validation.
71+
72+
Returns:
73+
The resulting microgrid object.
74+
"""
3575
delivery_area: DeliveryArea | None = None
3676
if message.HasField("delivery_area"):
3777
delivery_area = delivery_area_from_proto(message.delivery_area)
@@ -54,20 +94,6 @@ def microgrid_from_proto(message: microgrid_pb2.Microgrid) -> Microgrid:
5494
elif isinstance(status, int):
5595
major_issues.append("status is unrecognized")
5696

57-
if major_issues:
58-
_logger.warning(
59-
"Found issues in microgrid: %s | Protobuf message:\n%s",
60-
", ".join(major_issues),
61-
message,
62-
)
63-
64-
if minor_issues:
65-
_logger.debug(
66-
"Found minor issues in microgrid: %s | Protobuf message:\n%s",
67-
", ".join(minor_issues),
68-
message,
69-
)
70-
7197
return Microgrid(
7298
id=MicrogridId(message.id),
7399
enterprise_id=EnterpriseId(message.enterprise_id),

0 commit comments

Comments
 (0)