Skip to content

Commit 01a741e

Browse files
authored
Add specific gRPC client errors (#53)
This makes error handling more pythonic, as one can now just catch the exception type one is interested in, without having to do a second-level matching using the status. It also helps avoiding to expose the grpclib classes to the user.
2 parents f410571 + 642c62b commit 01a741e

File tree

6 files changed

+933
-71
lines changed

6 files changed

+933
-71
lines changed

RELEASE_NOTES.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,37 @@
77
## Upgrading
88

99
- The client now uses a string URL to connect to the server, the `grpc_channel` and `target` arguments are now replaced by `server_url`. The current accepted format is `grpc://hostname[:<port:int=9090>][?ssl=<ssl:bool=false>]`, meaning that the `port` and `ssl` are optional and default to 9090 and `false` respectively. You will have to adapt the way you connect to the server in your code.
10+
1011
- 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 your code if you are using `grpcio` directly.
11-
- The client now doesn't raise `grpc.aio.RpcError` exceptions anymore. Instead, it raises `ClientError` exceptions that have the `grpclib.GRPCError` as their `__cause__`. You might need to adapt your error handling code to catch `ClientError` exceptions instead of `grpc.aio.RpcError` exceptions.
12+
13+
- The client now doesn't raise `grpc.aio.RpcError` exceptions anymore. Instead, it raises its own exceptions, one per gRPC error status code, all inheriting from `GrpcError`, which in turn inherits from `ClientError` (as any other exception raised by this library in the future). `GrpcError`s have the `grpclib.GRPCError` as their `__cause__`. You might need to adapt your error handling code to catch these specific exceptions instead of `grpc.aio.RpcError`.
14+
15+
You can also access the underlying `grpclib.GRPCError` using the `grpc_error` attribute for `GrpStatusError` exceptions, but it is discouraged because it makes downstream projects dependant on `grpclib` too
16+
1217
- The client now uses protobuf/grpc bindings generated [betterproto](https://github.com/danielgtaylor/python-betterproto) ([frequenz-microgrid-betterproto](https://github.com/frequenz-floss/frequenz-microgrid-betterproto-python)) instead of [grpcio](https://pypi.org/project/grpcio/) ([frequenz-api-microgrid](https://github.com/frequenz-floss/frequenz-api-microgrid)). If you were using the bindings directly, you might need to do some minor adjustments to your code.
1318

1419
## New Features
1520

16-
<!-- Here goes the main new features and examples or instructions on how to use them -->
21+
- The client now raises more specific exceptions based on the gRPC status code, so you can more easily handle different types of errors.
22+
23+
For example:
24+
25+
```python
26+
try:
27+
connections = await client.connections()
28+
except OperationTimedOut:
29+
...
30+
```
31+
32+
instead of:
33+
34+
```python
35+
try:
36+
connections = await client.connections()
37+
except grpc.aio.RpcError as e:
38+
if e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
39+
...
40+
```
1741

1842
## Bug Fixes
1943

src/frequenz/client/microgrid/__init__.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,27 @@
2727
)
2828
from ._component_states import EVChargerCableState, EVChargerComponentState
2929
from ._connection import Connection
30-
from ._exception import ClientError
30+
from ._exception import (
31+
ClientError,
32+
DataLoss,
33+
EntityAlreadyExists,
34+
EntityNotFound,
35+
GrpcError,
36+
InternalError,
37+
InvalidArgument,
38+
OperationAborted,
39+
OperationCancelled,
40+
OperationNotImplemented,
41+
OperationOutOfRange,
42+
OperationPreconditionFailed,
43+
OperationTimedOut,
44+
OperationUnauthenticated,
45+
PermissionDenied,
46+
ResourceExhausted,
47+
ServiceUnavailable,
48+
UnknownError,
49+
UnrecognizedGrpcStatus,
50+
)
3151
from ._metadata import Location, Metadata
3252

3353
__all__ = [
@@ -41,14 +61,32 @@
4161
"ComponentMetricId",
4262
"ComponentType",
4363
"Connection",
64+
"DataLoss",
4465
"EVChargerCableState",
4566
"EVChargerComponentState",
4667
"EVChargerData",
68+
"EntityAlreadyExists",
69+
"EntityNotFound",
4770
"Fuse",
4871
"GridMetadata",
72+
"GrpcError",
73+
"InternalError",
74+
"InvalidArgument",
4975
"InverterData",
5076
"InverterType",
5177
"Location",
5278
"Metadata",
5379
"MeterData",
80+
"OperationAborted",
81+
"OperationCancelled",
82+
"OperationNotImplemented",
83+
"OperationOutOfRange",
84+
"OperationPreconditionFailed",
85+
"OperationTimedOut",
86+
"OperationUnauthenticated",
87+
"PermissionDenied",
88+
"ResourceExhausted",
89+
"ServiceUnavailable",
90+
"UnknownError",
91+
"UnrecognizedGrpcStatus",
5492
]

src/frequenz/client/microgrid/_client.py

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,21 @@ async def components(self) -> Iterable[Component]:
9090
Iterator whose elements are all the components in the microgrid.
9191
9292
Raises:
93-
ClientError: If the connection to the Microgrid API cannot be established or
94-
when the api call exceeded the timeout.
93+
ClientError: If the are any errors communicating with the Microgrid API,
94+
most likely a subclass of
95+
[GrpcError][frequenz.client.microgrid.GrpcError].
9596
"""
9697
try:
9798
component_list = await self.api.list_components(
9899
pb_microgrid.ComponentFilter(),
99100
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
100101
)
101-
102-
except grpclib.GRPCError as err:
103-
raise ClientError(
104-
f"Failed to list components. Microgrid API: {self._server_url}. Err: {err}"
105-
) from err
102+
except grpclib.GRPCError as grpc_error:
103+
raise ClientError.from_grpc_error(
104+
server_url=self._server_url,
105+
operation="list_components",
106+
grpc_error=grpc_error,
107+
) from grpc_error
106108

107109
components_only = filter(
108110
lambda c: c.category
@@ -168,8 +170,9 @@ async def connections(
168170
Microgrid connections matching the provided start and end filters.
169171
170172
Raises:
171-
ClientError: If the connection to the Microgrid API cannot be established or
172-
when the api call exceeded the timeout.
173+
ClientError: If the are any errors communicating with the Microgrid API,
174+
most likely a subclass of
175+
[GrpcError][frequenz.client.microgrid.GrpcError].
173176
"""
174177
connection_filter = pb_microgrid.ConnectionFilter(
175178
starts=list(starts), ends=list(ends)
@@ -182,10 +185,12 @@ async def connections(
182185
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
183186
),
184187
)
185-
except grpclib.GRPCError as err:
186-
raise ClientError(
187-
f"Failed to list connections. Microgrid API: {self._server_url}. Err: {err}"
188-
) from err
188+
except grpclib.GRPCError as grpc_error:
189+
raise ClientError.from_grpc_error(
190+
server_url=self._server_url,
191+
operation="list_connections",
192+
grpc_error=grpc_error,
193+
) from grpc_error
189194
# Filter out the components filtered in `components` method.
190195
# id=0 is an exception indicating grid component.
191196
valid_ids = {c.component_id for c in valid_components}
@@ -384,8 +389,9 @@ async def set_power(self, component_id: int, power_w: float) -> None:
384389
power_w: power to set for the component.
385390
386391
Raises:
387-
ClientError: If the connection to the Microgrid API cannot be established or
388-
when the api call exceeded the timeout.
392+
ClientError: If the are any errors communicating with the Microgrid API,
393+
most likely a subclass of
394+
[GrpcError][frequenz.client.microgrid.GrpcError].
389395
"""
390396
try:
391397
await self.api.set_power_active(
@@ -394,10 +400,12 @@ async def set_power(self, component_id: int, power_w: float) -> None:
394400
),
395401
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
396402
)
397-
except grpclib.GRPCError as err:
398-
raise ClientError(
399-
f"Failed to set power. Microgrid API: {self._server_url}. Err: {err}"
400-
) from err
403+
except grpclib.GRPCError as grpc_error:
404+
raise ClientError.from_grpc_error(
405+
server_url=self._server_url,
406+
operation="set_power_active",
407+
grpc_error=grpc_error,
408+
) from grpc_error
401409

402410
async def set_bounds(
403411
self,
@@ -415,10 +423,10 @@ async def set_bounds(
415423
Raises:
416424
ValueError: when upper bound is less than 0, or when lower bound is
417425
greater than 0.
418-
ClientError: If the connection to the Microgrid API cannot be established or
419-
when the api call exceeded the timeout.
426+
ClientError: If the are any errors communicating with the Microgrid API,
427+
most likely a subclass of
428+
[GrpcError][frequenz.client.microgrid.GrpcError].
420429
"""
421-
api_details = f"Microgrid API: {self._server_url}."
422430
if upper < 0:
423431
raise ValueError(f"Upper bound {upper} must be greater than or equal to 0.")
424432
if lower > 0:
@@ -436,15 +444,9 @@ async def set_bounds(
436444
),
437445
timeout=int(DEFAULT_GRPC_CALL_TIMEOUT),
438446
)
439-
except grpclib.GRPCError as err:
440-
_logger.error(
441-
"set_bounds write failed: %s, for message: %s, api: %s. Err: %s",
442-
err,
443-
next,
444-
api_details,
445-
err,
446-
)
447-
raise ClientError(
448-
f"Failed to set inclusion bounds. Microgrid API: {self._server_url}. "
449-
f"Err: {err}"
450-
) from err
447+
except grpclib.GRPCError as grpc_error:
448+
raise ClientError.from_grpc_error(
449+
server_url=self._server_url,
450+
operation="add_inclusion_bounds",
451+
grpc_error=grpc_error,
452+
) from grpc_error

0 commit comments

Comments
 (0)