Skip to content

Commit 503c889

Browse files
committed
Raise a new ClientError instead of AioRpcError
We want to avoid exposing the underlying details of the client, so in the future we can change the libraries used to connect to the service, or even the protocol. This also removes the re-building of the exceptions, which was pretty unnecessary and needed some `type: ignore`s due to typing issues in `grpcio`. We should probably create a separate error for timeouts in the future. Signed-off-by: Leandro Lucarella <[email protected]>
1 parent b83979a commit 503c889

File tree

5 files changed

+55
-56
lines changed

5 files changed

+55
-56
lines changed

RELEASE_NOTES.md

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

77
## Upgrading
88

9-
- The client is now using [`grpclib`](https://pypi.org/project/grpclib/) to connect to the server instead of [`grpcio`](https://pypi.org/project/grpcio/). You might need to adapt the way you connect to the server in your code, using `grpcio.client.Channel` and catching `grpcio.GRPCError` exceptions.
9+
- The client is now using [`grpclib`](https://pypi.org/project/grpclib/) to connect to the server instead of [`grpcio`](https://pypi.org/project/grpcio/). You might need to adapt the way you connect to the server in your code, using `grpcio.client.Channel`.
10+
- The client now doesn't raise `grpc.aio.RpcError` exceptions anymore. Instead, it raises `ClientError` exceptions that have the `grpc.aio.RpcError` as their `__cause__`. You might need to adapt your error handling code to catch `ClientError` exceptions instead of `grpc.aio.RpcError` exceptions.
1011

1112
## New Features
1213

src/frequenz/client/microgrid/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
)
2828
from ._component_states import EVChargerCableState, EVChargerComponentState
2929
from ._connection import Connection
30+
from ._exception import ClientError
3031
from ._metadata import Location, Metadata
3132

3233
__all__ = [
3334
"ApiClient",
3435
"BatteryData",
36+
"ClientError",
3537
"Component",
3638
"ComponentCategory",
3739
"ComponentData",

src/frequenz/client/microgrid/_client.py

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
)
5151
from ._connection import Connection
5252
from ._constants import RECEIVER_MAX_SIZE
53+
from ._exception import ClientError
5354
from ._metadata import Location, Metadata
5455

5556
DEFAULT_GRPC_CALL_TIMEOUT = 60.0
@@ -96,8 +97,8 @@ async def components(self) -> Iterable[Component]:
9697
Iterator whose elements are all the components in the microgrid.
9798
9899
Raises:
99-
AioRpcError: if connection to Microgrid API cannot be established or
100-
when the api call exceeded timeout
100+
ClientError: If the connection to the Microgrid API cannot be established or
101+
when the api call exceeded the timeout.
101102
"""
102103
try:
103104
# grpc.aio is missing types and mypy thinks this is not awaitable,
@@ -111,21 +112,9 @@ async def components(self) -> Iterable[Component]:
111112
)
112113

113114
except grpc.aio.AioRpcError as err:
114-
msg = f"Failed to list components. Microgrid API: {self.target}. Err: {err.details()}"
115-
raise grpc.aio.AioRpcError(
116-
code=err.code(),
117-
initial_metadata=err.initial_metadata(),
118-
# We need to ignore these errors for some reason, otherwise we get this
119-
# mypy error:
120-
# Argument "trailing_metadata" to "AioRpcError" has incompatible type
121-
# "tuple[_Metadatum, ...]"; expected "Metadata"
122-
# According to grpc.aio documentation, both should have the type
123-
# Metadata.
124-
# https://grpc.github.io/grpc/python/grpc_asyncio.html#grpc.aio.AioRpcError
125-
trailing_metadata=err.trailing_metadata(), # type: ignore[arg-type]
126-
details=msg,
127-
debug_error_string=err.debug_error_string(),
128-
)
115+
raise ClientError(
116+
f"Failed to list components. Microgrid API: {self.target}. Err: {err.details()}"
117+
) from err
129118
components_only = filter(
130119
lambda c: c.category is not PbComponentCategory.COMPONENT_CATEGORY_SENSOR,
131120
component_list.components,
@@ -192,8 +181,8 @@ async def connections(
192181
Microgrid connections matching the provided start and end filters.
193182
194183
Raises:
195-
AioRpcError: if connection to Microgrid API cannot be established or
196-
when the api call exceeded timeout
184+
ClientError: If the connection to the Microgrid API cannot be established or
185+
when the api call exceeded the timeout.
197186
"""
198187
connection_filter = PbConnectionFilter(starts=starts, ends=ends)
199188
try:
@@ -210,15 +199,9 @@ async def connections(
210199
),
211200
)
212201
except grpc.aio.AioRpcError as err:
213-
msg = f"Failed to list connections. Microgrid API: {self.target}. Err: {err.details()}"
214-
raise grpc.aio.AioRpcError(
215-
code=err.code(),
216-
initial_metadata=err.initial_metadata(),
217-
# See the comment in def components() for why we need to ignore
218-
trailing_metadata=err.trailing_metadata(), # type: ignore[arg-type]
219-
details=msg,
220-
debug_error_string=err.debug_error_string(),
221-
)
202+
raise ClientError(
203+
f"Failed to list connections. Microgrid API: {self.target}. Err: {err.details()}"
204+
) from err
222205
# Filter out the components filtered in `components` method.
223206
# id=0 is an exception indicating grid component.
224207
valid_ids = {c.component_id for c in valid_components}
@@ -422,8 +405,8 @@ async def set_power(self, component_id: int, power_w: float) -> None:
422405
power_w: power to set for the component.
423406
424407
Raises:
425-
AioRpcError: if connection to Microgrid API cannot be established or
426-
when the api call exceeded timeout
408+
ClientError: If the connection to the Microgrid API cannot be established or
409+
when the api call exceeded the timeout.
427410
"""
428411
try:
429412
await cast(
@@ -434,15 +417,9 @@ async def set_power(self, component_id: int, power_w: float) -> None:
434417
),
435418
)
436419
except grpc.aio.AioRpcError as err:
437-
msg = f"Failed to set power. Microgrid API: {self.target}. Err: {err.details()}"
438-
raise grpc.aio.AioRpcError(
439-
code=err.code(),
440-
initial_metadata=err.initial_metadata(),
441-
# See the comment in def components() for why we need to ignore
442-
trailing_metadata=err.trailing_metadata(), # type: ignore[arg-type]
443-
details=msg,
444-
debug_error_string=err.debug_error_string(),
445-
)
420+
raise ClientError(
421+
f"Failed to set power. Microgrid API: {self.target}. Err: {err.details()}"
422+
) from err
446423

447424
async def set_bounds(
448425
self,
@@ -460,8 +437,8 @@ async def set_bounds(
460437
Raises:
461438
ValueError: when upper bound is less than 0, or when lower bound is
462439
greater than 0.
463-
grpc.aio.AioRpcError: if connection to Microgrid API cannot be established
464-
or when the api call exceeded timeout
440+
ClientError: If the connection to the Microgrid API cannot be established or
441+
when the api call exceeded the timeout.
465442
"""
466443
api_details = f"Microgrid API: {self.target}."
467444
if upper < 0:
@@ -490,4 +467,7 @@ async def set_bounds(
490467
api_details,
491468
err.details(),
492469
)
493-
raise
470+
raise ClientError(
471+
f"Failed to set inclusion bounds. Microgrid API: {self.target}. "
472+
f"Err: {err.details()}"
473+
) from err
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# License: MIT
2+
# Copyright © 2022 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Exceptions raised by the microgrid API client."""
5+
6+
7+
class ClientError(Exception):
8+
"""There was an error in the microgrid API client."""

tests/test_timeout.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
# pylint: enable=no-name-in-module
2626
from pytest_mock import MockerFixture
2727

28-
from frequenz.client.microgrid import ApiClient
28+
from frequenz.client.microgrid import ApiClient, ClientError
2929

3030
from .mock_api import MockGrpcServer, MockMicrogridServicer
3131

@@ -38,7 +38,7 @@
3838
def fake_grpc_call_timeout() -> Iterator[float]:
3939
"""Patch the default gRPC call timeout."""
4040
# Timeout applied to all gRPC calls under test. It is expected after that the gRPC
41-
# calls will raise an AioRpcError with status code equal to DEADLINE_EXCEEDED.
41+
# calls will raise an ClientError with status code equal to DEADLINE_EXCEEDED.
4242
grpc_call_timeout: float = 0.1
4343

4444
with patch(
@@ -49,7 +49,7 @@ def fake_grpc_call_timeout() -> Iterator[float]:
4949

5050

5151
async def test_components_timeout(mocker: MockerFixture) -> None:
52-
"""Test if the components() method properly raises a timeeout AioRpcError."""
52+
"""Test if the components() method properly raises a timeeout ClientError."""
5353
servicer = MockMicrogridServicer()
5454

5555
def mock_list_components(
@@ -66,14 +66,16 @@ def mock_list_components(
6666
grpc_channel = grpc.aio.insecure_channel(target)
6767
client = ApiClient(grpc_channel=grpc_channel, target=target)
6868

69-
with pytest.raises(grpc.aio.AioRpcError) as err_ctx:
69+
with pytest.raises(ClientError) as err_ctx:
7070
_ = await client.components()
71-
assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
71+
cause = err_ctx.value.__cause__
72+
assert isinstance(cause, grpc.aio.AioRpcError)
73+
assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED
7274
assert await server.graceful_shutdown()
7375

7476

7577
async def test_connections_timeout(mocker: MockerFixture) -> None:
76-
"""Test if the connections() method properly raises a timeout AioRpcError."""
78+
"""Test if the connections() method properly raises a timeout ClientError."""
7779
servicer = MockMicrogridServicer()
7880

7981
def mock_list_connections(
@@ -90,14 +92,16 @@ def mock_list_connections(
9092
grpc_channel = grpc.aio.insecure_channel(target)
9193
client = ApiClient(grpc_channel=grpc_channel, target=target)
9294

93-
with pytest.raises(grpc.aio.AioRpcError) as err_ctx:
95+
with pytest.raises(ClientError) as err_ctx:
9496
_ = await client.connections()
95-
assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
97+
cause = err_ctx.value.__cause__
98+
assert isinstance(cause, grpc.aio.AioRpcError)
99+
assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED
96100
assert await server.graceful_shutdown()
97101

98102

99103
async def test_set_power_timeout(mocker: MockerFixture) -> None:
100-
"""Test if the set_power() method properly raises a timeout AioRpcError."""
104+
"""Test if the set_power() method properly raises a timeout ClientError."""
101105
servicer = MockMicrogridServicer()
102106

103107
def mock_set_power(
@@ -116,9 +120,11 @@ def mock_set_power(
116120

117121
power_values = [-100, 100]
118122
for power_w in power_values:
119-
with pytest.raises(grpc.aio.AioRpcError) as err_ctx:
123+
with pytest.raises(ClientError) as err_ctx:
120124
await client.set_power(component_id=1, power_w=power_w)
121-
assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
125+
cause = err_ctx.value.__cause__
126+
assert isinstance(cause, grpc.aio.AioRpcError)
127+
assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED
122128

123129
assert await server.graceful_shutdown()
124130

@@ -144,8 +150,10 @@ def mock_add_inclusion_bounds(
144150
bounds_values = [{"lower": 0.0, "upper": 100.0}, {"lower": -10.0, "upper": 1.0}]
145151

146152
for bounds in bounds_values:
147-
with pytest.raises(grpc.aio.AioRpcError) as err_ctx:
153+
with pytest.raises(ClientError) as err_ctx:
148154
await client.set_bounds(component_id=1, **bounds)
149-
assert err_ctx.value.code() == grpc.StatusCode.DEADLINE_EXCEEDED
155+
cause = err_ctx.value.__cause__
156+
assert isinstance(cause, grpc.aio.AioRpcError)
157+
assert cause.code() == grpc.StatusCode.DEADLINE_EXCEEDED
150158

151159
assert await server.graceful_shutdown()

0 commit comments

Comments
 (0)