Skip to content

Commit 11557e0

Browse files
committed
rpc errors
1 parent 1121e07 commit 11557e0

File tree

4 files changed

+65
-40
lines changed

4 files changed

+65
-40
lines changed

packages/service-library/src/servicelib/rabbitmq/__init__.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from ._constants import BIND_TO_ALL_TOPICS, RPC_REQUEST_DEFAULT_TIMEOUT_S
66
from ._errors import (
77
RemoteMethodNotRegisteredError,
8+
RPCInterfaceError,
89
RPCNotInitializedError,
910
RPCServerError,
1011
)
@@ -14,18 +15,19 @@
1415

1516
__all__: tuple[str, ...] = (
1617
"BIND_TO_ALL_TOPICS",
18+
"RPC_REQUEST_DEFAULT_TIMEOUT_S",
1719
"ConsumerTag",
1820
"ExchangeName",
19-
"is_rabbitmq_responsive",
2021
"QueueName",
21-
"RabbitMQClient",
22-
"RabbitMQRPCClient",
23-
"RemoteMethodNotRegisteredError",
24-
"RPC_REQUEST_DEFAULT_TIMEOUT_S",
22+
"RPCInterfaceError",
2523
"RPCNamespace",
2624
"RPCNotInitializedError",
2725
"RPCRouter",
2826
"RPCServerError",
27+
"RabbitMQClient",
28+
"RabbitMQRPCClient",
29+
"RemoteMethodNotRegisteredError",
30+
"is_rabbitmq_responsive",
2931
"wait_till_rabbitmq_responsive",
3032
)
3133

packages/service-library/src/servicelib/rabbitmq/_errors.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,16 @@
55
_ERROR_PREFIX: Final[str] = "rabbitmq_error"
66

77

8-
class BaseRPCError(OsparcErrorMixin, RuntimeError):
9-
...
8+
class BaseRPCError(OsparcErrorMixin, RuntimeError): ...
109

1110

1211
class RPCNotInitializedError(BaseRPCError):
13-
code = f"{_ERROR_PREFIX}.not_started" # type: ignore[assignment]
12+
code = f"{_ERROR_PREFIX}.not_started" # type: ignore[assignment]
1413
msg_template = "Please check that the RabbitMQ RPC backend was initialized!"
1514

1615

1716
class RemoteMethodNotRegisteredError(BaseRPCError):
18-
code = f"{_ERROR_PREFIX}.remote_not_registered" # type: ignore[assignment]
17+
code = f"{_ERROR_PREFIX}.remote_not_registered" # type: ignore[assignment]
1918
msg_template = (
2019
"Could not find a remote method named: '{method_name}'. "
2120
"Message from remote server was returned: {incoming_message}. "
@@ -27,3 +26,23 @@ class RPCServerError(BaseRPCError):
2726
"While running method '{method_name}' raised "
2827
"'{exc_type}': '{exc_message}'\n{traceback}"
2928
)
29+
30+
31+
class RPCInterfaceError(RPCServerError):
32+
"""
33+
Base class for RPC interface exceptions.
34+
35+
Avoid using domain exceptions directly; if a one-to-one mapping is required,
36+
prefer using the `from_domain_error` transformation function.
37+
"""
38+
39+
msg_template = "{domain_error_message} [{domain_error_code}]"
40+
41+
@classmethod
42+
def from_domain_error(cls, err: OsparcErrorMixin):
43+
domain_err_ctx = err.error_context()
44+
return cls(
45+
domain_error_message=domain_err_ctx.pop("message"),
46+
domain_error_code=domain_err_ctx.pop("code"),
47+
**domain_err_ctx, # same context as domain
48+
)
Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,7 @@
1-
from common_library.errors_classes import OsparcErrorMixin
1+
from ..._errors import RPCInterfaceError
22

3-
from ..._errors import RPCServerError
43

4+
class ProjectNotFoundRpcError(RPCInterfaceError): ...
55

6-
class WebServerRpcError(RPCServerError):
7-
msg_template = "{domain_error_nessage} [{domain_error_type_name}]"
86

9-
@classmethod
10-
def from_domain_error(cls, err: OsparcErrorMixin):
11-
return cls(
12-
# composes a message
13-
domain_error_nessage=err.message,
14-
domain_error_type_name=f"{err.__class__.__name__}",
15-
# copies context
16-
**err.error_context(),
17-
)
18-
19-
20-
class ProjectNotFoundRpcError(WebServerRpcError): ...
21-
22-
23-
class ProjectForbiddenRpcError(WebServerRpcError): ...
7+
class ProjectForbiddenRpcError(RPCInterfaceError): ...

packages/service-library/tests/rabbitmq/test_rabbitmq_rpc_router.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
# pylint:disable=unused-argument
33

44
from collections.abc import Awaitable, Callable
5+
from typing import cast
56

67
import pytest
8+
from common_library.errors_classes import OsparcErrorMixin
79
from faker import Faker
810
from models_library.rabbitmq_basic_types import RPCMethodName
911
from servicelib.rabbitmq import (
1012
RabbitMQRPCClient,
13+
RPCInterfaceError,
1114
RPCNamespace,
1215
RPCRouter,
1316
RPCServerError,
@@ -18,15 +21,21 @@
1821
]
1922

2023

21-
router = RPCRouter()
24+
class MyServiceError(OsparcErrorMixin, Exception): ...
2225

2326

24-
class MyBaseError(Exception):
25-
...
27+
class MyDomainError(MyServiceError):
28+
msg_template = "This could happen"
2629

2730

28-
class MyExpectedError(MyBaseError):
29-
...
31+
def raise_my_expected_error():
32+
raise MyDomainError(user_id=33, project_id=3)
33+
34+
35+
router = RPCRouter() # Server-side
36+
37+
38+
class MyExpectedRpcError(RPCInterfaceError): ...
3039

3140

3241
@router.expose()
@@ -41,10 +50,13 @@ async def an_int_method(a_global_arg: str, *, a_global_kwarg: str) -> int:
4150
return 34
4251

4352

44-
@router.expose(reraise_if_error_type=(MyBaseError,))
45-
async def raising_expected_error(a_global_arg: str, *, a_global_kwarg: str) -> int:
46-
msg = "This could happen"
47-
raise MyExpectedError(msg)
53+
@router.expose(reraise_if_error_type=(MyExpectedRpcError,))
54+
async def raising_expected_error(a_global_arg: str, *, a_global_kwarg: str):
55+
try:
56+
raise_my_expected_error()
57+
except MyDomainError as exc:
58+
# NOTE how it is adapted from a domain exception to an interface exception
59+
raise MyExpectedRpcError.from_domain_error(exc) from exc
4860

4961

5062
@router.expose()
@@ -55,7 +67,7 @@ async def raising_unexpected_error(a_global_arg: str, *, a_global_kwarg: str) ->
5567

5668
@pytest.fixture
5769
def router_namespace(faker: Faker) -> RPCNamespace:
58-
return faker.pystr()
70+
return cast(RPCNamespace, faker.pystr())
5971

6072

6173
async def test_exposed_methods(
@@ -100,10 +112,18 @@ async def test_exposed_methods(
100112
assert "builtins.ValueError" in f"{exc_info.value}"
101113

102114
# This error was classified int he interface
103-
with pytest.raises(MyBaseError) as exc_info:
115+
with pytest.raises(RPCInterfaceError) as exc_info:
104116
await rpc_client.request(
105117
router_namespace,
106118
RPCMethodName(raising_expected_error.__name__),
107119
)
108120

109-
assert isinstance(exc_info.value, MyExpectedError)
121+
assert isinstance(exc_info.value, MyExpectedRpcError)
122+
assert exc_info.value.error_context() == {
123+
"message": "This could happen [MyServiceError.MyDomainError]",
124+
"code": "RuntimeError.BaseRPCError.RPCServerError",
125+
"domain_error_message": "This could happen",
126+
"domain_error_code": "MyServiceError.MyDomainError",
127+
"user_id": 33,
128+
"project_id": 3,
129+
}

0 commit comments

Comments
 (0)