diff --git a/README.rst b/README.rst index db31e0e..c2d4190 100644 --- a/README.rst +++ b/README.rst @@ -359,7 +359,7 @@ which can be found in ``pjrpc.common.exceptions`` module so that error handling Default error list can be easily extended. All you need to create an error class inherited from -``pjrpc.exc.TypedError`` and define an error code and a description message. ``pjrpc`` will be automatically +``pjrpc.client.exceptions.TypedError`` and define an error code and a description message. ``pjrpc`` will be automatically deserializing custom errors for you: .. code-block:: python @@ -367,7 +367,7 @@ deserializing custom errors for you: import pjrpc from pjrpc.client.backend import requests as pjrpc_client - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.client.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -395,7 +395,7 @@ On the server side everything is also pretty straightforward: methods = pjrpc.server.MethodRegistry() - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.server.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -513,7 +513,7 @@ and Swagger UI web tool with basic auth: id: UserId - class AlreadyExistsError(pjrpc.exc.TypedError): + class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -522,7 +522,7 @@ and Swagger UI web tool with basic auth: MESSAGE = "user already exists" - class NotFoundError(pjrpc.exc.TypedError): + class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/docs/source/pjrpc/errors.rst b/docs/source/pjrpc/errors.rst index 33f539f..5ba1a21 100644 --- a/docs/source/pjrpc/errors.rst +++ b/docs/source/pjrpc/errors.rst @@ -22,7 +22,7 @@ Errors handling -32000 to -32099 , Server error , Reserved for implementation-defined server-errors. -Errors can be found in :py:mod:`pjrpc.common.exceptions` module. Having said that error handling +Errors can be found in :py:mod:`pjrpc.client.exceptions` module. Having said that error handling is very simple and "pythonic-way": .. code-block:: python @@ -34,7 +34,7 @@ is very simple and "pythonic-way": try: result = client.proxy.sum(1, 2) - except pjrpc.MethodNotFound as e: + except pjrpc.client.exceptions.MethodNotFound as e: print(e) @@ -42,7 +42,7 @@ Custom errors ------------- Default error list can be easily extended. All you need to create an error class inherited from -:py:class:`pjrpc.common.exceptions.TypedError` and define an error code and a description message. ``pjrpc`` +:py:class:`pjrpc.client.exceptions.TypedError` and define an error code and a description message. ``pjrpc`` will be automatically deserializing custom errors for you: .. code-block:: python @@ -50,7 +50,7 @@ will be automatically deserializing custom errors for you: import pjrpc from pjrpc.client.backend import requests as pjrpc_client - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.client.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -81,7 +81,7 @@ On the server side everything is also pretty straightforward: methods = pjrpc.server.MethodRegistry() - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.server.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -123,7 +123,7 @@ to set a base error class for a particular client: from pjrpc.client.backend import requests as jrpc_client - class ErrorV1(pjrpc.exc.TypeError, base=True): + class ErrorV1(pjrpc.client.exceptions.TypeError, base=True): pass @@ -132,7 +132,7 @@ to set a base error class for a particular client: MESSAGE = 'permission denied' - class ErrorV2(pjrpc.exc.TypeError, base=True): + class ErrorV2(pjrpc.client.exceptions.TypeError, base=True): pass diff --git a/docs/source/pjrpc/quickstart.rst b/docs/source/pjrpc/quickstart.rst index 41a31d7..975caa5 100644 --- a/docs/source/pjrpc/quickstart.rst +++ b/docs/source/pjrpc/quickstart.rst @@ -278,7 +278,7 @@ Error handling ______________ ``pjrpc`` implements all the errors listed in `protocol specification `_ -which can be found in :py:mod:`pjrpc.common.exceptions` module so that error handling is very simple and "pythonic-way": +which can be found in :py:mod:`pjrpc.client.exceptions` module so that error handling is very simple and "pythonic-way": .. code-block:: python @@ -294,7 +294,7 @@ which can be found in :py:mod:`pjrpc.common.exceptions` module so that error han Default error list can be easily extended. All you need to create an error class inherited from -:py:class:`pjrpc.common.exceptions.JsonRpcError` and define an error code and a description message. ``pjrpc`` +:py:class:`pjrpc.client.exceptions.TypedError` and define an error code and a description message. ``pjrpc`` will be automatically deserializing custom errors for you: .. code-block:: python @@ -302,7 +302,7 @@ will be automatically deserializing custom errors for you: import pjrpc from pjrpc.client.backend import requests as pjrpc_client - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.client.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -330,7 +330,7 @@ On the server side everything is also pretty straightforward: methods = pjrpc.server.MethodRegistry() - class UserNotFound(pjrpc.exc.TypedError): + class UserNotFound(pjrpc.server.exceptions.TypedError): CODE = 1 MESSAGE = 'user not found' @@ -448,7 +448,7 @@ and Swagger UI web tool with basic auth: id: UserId - class AlreadyExistsError(pjrpc.exc.TypedError): + class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -457,7 +457,7 @@ and Swagger UI web tool with basic auth: MESSAGE = "user already exists" - class NotFoundError(pjrpc.exc.TypedError): + class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/docs/source/pjrpc/testing.rst b/docs/source/pjrpc/testing.rst index cc4fe12..c7c5140 100644 --- a/docs/source/pjrpc/testing.rst +++ b/docs/source/pjrpc/testing.rst @@ -44,12 +44,12 @@ Look at the following test example: assert result == 2 pjrpc_aiohttp_mocker.replace( - 'http://localhost/api/v1', 'sum', error=pjrpc.exc.JsonRpcError(code=1, message='error', data='oops'), + 'http://localhost/api/v1', 'sum', error=pjrpc.client.exceptions.JsonRpcError(code=1, message='error', data='oops'), ) - with pytest.raises(pjrpc.exc.JsonRpcError) as exc_info: + with pytest.raises(pjrpc.client.exceptions.JsonRpcError) as exc_info: await client.proxy.sum(a=1, b=1) - assert exc_info.type is pjrpc.exc.JsonRpcError + assert exc_info.type is pjrpc.client.exceptions.JsonRpcError assert exc_info.value.code == 1 assert exc_info.value.message == 'error' assert exc_info.value.data == 'oops' diff --git a/docs/source/pjrpc/webui.rst b/docs/source/pjrpc/webui.rst index b4893bc..66fb1db 100644 --- a/docs/source/pjrpc/webui.rst +++ b/docs/source/pjrpc/webui.rst @@ -86,7 +86,7 @@ using flask web framework: id: UserId - class AlreadyExistsError(pjrpc.exc.TypedError): + class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -95,7 +95,7 @@ using flask web framework: MESSAGE = "user already exists" - class NotFoundError(pjrpc.exc.TypedError): + class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/examples/aiohttp_pytest.py b/examples/aiohttp_pytest.py index 0abbb94..4e6c2d5 100644 --- a/examples/aiohttp_pytest.py +++ b/examples/aiohttp_pytest.py @@ -15,12 +15,14 @@ async def test_using_fixture(pjrpc_aiohttp_mocker): assert result == 2 pjrpc_aiohttp_mocker.replace( - 'http://localhost/api/v1', 'sum', error=pjrpc.exc.JsonRpcError(code=1, message='error', data='oops'), + 'http://localhost/api/v1', + 'sum', + error=pjrpc.client.exceptions.JsonRpcError(code=1, message='error', data='oops'), ) - with pytest.raises(pjrpc.exc.JsonRpcError) as exc_info: + with pytest.raises(pjrpc.client.exceptions.JsonRpcError) as exc_info: await client.proxy.sum(a=1, b=1) - assert exc_info.type is pjrpc.exc.JsonRpcError + assert exc_info.type is pjrpc.client.exceptions.JsonRpcError assert exc_info.value.code == 1 assert exc_info.value.message == 'error' assert exc_info.value.data == 'oops' diff --git a/examples/multiple_clients.py b/examples/multiple_clients.py index a309c7c..9bd1a5e 100644 --- a/examples/multiple_clients.py +++ b/examples/multiple_clients.py @@ -4,7 +4,7 @@ from pjrpc.client.backend import requests as jrpc_client -class ErrorV1(pjrpc.exc.TypedError, base=True): +class ErrorV1(pjrpc.client.exceptions.TypedError, base=True): pass @@ -13,7 +13,7 @@ class PermissionDenied(ErrorV1): MESSAGE: ClassVar[str] = 'permission denied' -class ErrorV2(pjrpc.exc.TypedError, base=True): +class ErrorV2(pjrpc.client.exceptions.TypedError, base=True): pass diff --git a/examples/openapi_aiohttp.py b/examples/openapi_aiohttp.py index e051a10..72a4747 100644 --- a/examples/openapi_aiohttp.py +++ b/examples/openapi_aiohttp.py @@ -67,7 +67,7 @@ class UserOut(UserIn): id: UserId -class AlreadyExistsError(pjrpc.exc.TypedError): +class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -76,7 +76,7 @@ class AlreadyExistsError(pjrpc.exc.TypedError): MESSAGE = "user already exists" -class NotFoundError(pjrpc.exc.TypedError): +class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/examples/openapi_aiohttp_subendpoints.py b/examples/openapi_aiohttp_subendpoints.py index d9fb5f0..cdb5d27 100644 --- a/examples/openapi_aiohttp_subendpoints.py +++ b/examples/openapi_aiohttp_subendpoints.py @@ -7,13 +7,13 @@ import pjrpc.server.specs.extractors.pydantic import pjrpc.server.specs.openapi.ui -from pjrpc.common.exceptions import MethodNotFoundError +from pjrpc.server.exceptions import MethodNotFoundError from pjrpc.server.integration import aiohttp as integration from pjrpc.server.specs import extractors, openapi, openrpc from pjrpc.server.validators import pydantic as validators -class AlreadyExistsError(pjrpc.exc.TypedError): +class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ diff --git a/examples/openapi_flask.py b/examples/openapi_flask.py index 606013b..3b86074 100644 --- a/examples/openapi_flask.py +++ b/examples/openapi_flask.py @@ -60,7 +60,7 @@ class UserOut(UserIn): id: UserId -class AlreadyExistsError(pjrpc.exc.TypedError): +class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -69,7 +69,7 @@ class AlreadyExistsError(pjrpc.exc.TypedError): MESSAGE = "user already exists" -class NotFoundError(pjrpc.exc.TypedError): +class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/examples/openrpc_aiohttp.py b/examples/openrpc_aiohttp.py index 83a939d..64ee45c 100644 --- a/examples/openrpc_aiohttp.py +++ b/examples/openrpc_aiohttp.py @@ -63,7 +63,7 @@ class UserOut(UserIn, title="User data"): id: UserId -class AlreadyExistsError(pjrpc.exc.TypedError): +class AlreadyExistsError(pjrpc.server.exceptions.TypedError): """ User already registered error. """ @@ -72,7 +72,7 @@ class AlreadyExistsError(pjrpc.exc.TypedError): MESSAGE = "user already exists" -class NotFoundError(pjrpc.exc.TypedError): +class NotFoundError(pjrpc.server.exceptions.TypedError): """ User not found error. """ diff --git a/examples/requests_pytest.py b/examples/requests_pytest.py index 3149da3..995c5db 100644 --- a/examples/requests_pytest.py +++ b/examples/requests_pytest.py @@ -15,12 +15,14 @@ def test_using_fixture(pjrpc_requests_mocker): assert result == 2 pjrpc_requests_mocker.replace( - 'http://localhost/api/v1', 'sum', error=pjrpc.exc.JsonRpcError(code=1, message='error', data='oops'), + 'http://localhost/api/v1', + 'sum', + error=pjrpc.client.exceptions.JsonRpcError(code=1, message='error', data='oops'), ) - with pytest.raises(pjrpc.exc.JsonRpcError) as exc_info: + with pytest.raises(pjrpc.client.exceptions.JsonRpcError) as exc_info: client.proxy.sum(a=1, b=1) - assert exc_info.type is pjrpc.exc.JsonRpcError + assert exc_info.type is pjrpc.client.exceptions.JsonRpcError assert exc_info.value.code == 1 assert exc_info.value.message == 'error' assert exc_info.value.data == 'oops' @@ -52,5 +54,5 @@ def test_using_resource_manager(): assert localhost_calls[('2.0', 'div')].mock_calls == [mock.call(4, 2)] assert localhost_calls[('2.0', 'mult')].mock_calls == [mock.call(2, 2)] - with pytest.raises(pjrpc.exc.MethodNotFoundError): + with pytest.raises(pjrpc.client.exceptions.MethodNotFoundError): client.proxy.sub(4, 2) diff --git a/examples/sentry.py b/examples/sentry.py index f7a6b5c..ad04da6 100644 --- a/examples/sentry.py +++ b/examples/sentry.py @@ -17,7 +17,7 @@ async def sum(request: web.Request, a: int, b: int) -> int: async def sentry_middleware(request: Request, context: web.Request, handler: AsyncHandlerType) -> Response: try: return await handler(request, context) - except pjrpc.exceptions.JsonRpcError as e: + except pjrpc.server.exceptions.JsonRpcError as e: sentry_sdk.capture_exception(e) raise diff --git a/pjrpc/client/__init__.py b/pjrpc/client/__init__.py index 1743d53..67282fd 100644 --- a/pjrpc/client/__init__.py +++ b/pjrpc/client/__init__.py @@ -2,9 +2,10 @@ JSON-RPC client. """ -from . import validators +from . import exceptions, validators from .client import AbstractAsyncClient, AbstractClient, AsyncMiddleware, AsyncMiddlewareHandler, Batch, Middleware from .client import MiddlewareHandler +from .exceptions import JsonRpcError __all__ = [ 'AbstractAsyncClient', @@ -12,6 +13,8 @@ 'AsyncMiddleware', 'AsyncMiddlewareHandler', 'Batch', + 'exceptions', + 'JsonRpcError', 'Middleware', 'MiddlewareHandler', 'validators', diff --git a/pjrpc/client/backend/aio_pika.py b/pjrpc/client/backend/aio_pika.py index feaac4a..23d4054 100644 --- a/pjrpc/client/backend/aio_pika.py +++ b/pjrpc/client/backend/aio_pika.py @@ -9,10 +9,9 @@ from aio_pika.abc import AbstractIncomingMessage from yarl import URL -import pjrpc -from pjrpc.client import AbstractAsyncClient, AsyncMiddleware -from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, JsonRpcError -from pjrpc.common import Request, Response, generators +from pjrpc.client import AbstractAsyncClient, AsyncMiddleware, exceptions +from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, Request, Response +from pjrpc.common import generators from pjrpc.common.typedefs import JsonRpcRequestIdT logger = logging.getLogger(__package__) @@ -77,7 +76,7 @@ def __init__( result_queue_name: Optional[str] = None, result_queue_args: Optional[QueueArgs] = None, id_gen_impl: Callable[..., Generator[JsonRpcRequestIdT, None, None]] = generators.sequential, - error_cls: type[JsonRpcError] = JsonRpcError, + error_cls: type[exceptions.JsonRpcError] = exceptions.JsonRpcError, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -196,7 +195,7 @@ async def _on_result_message(self, message: AbstractIncomingMessage) -> None: if message.content_type not in self._response_content_types: future.set_exception( - pjrpc.exc.DeserializationError(f"unexpected response content type: {message.content_type}"), + exceptions.DeserializationError(f"unexpected response content type: {message.content_type}"), ) else: future.set_result(message.body.decode(message.content_encoding or 'utf8')) diff --git a/pjrpc/client/backend/aiohttp.py b/pjrpc/client/backend/aiohttp.py index c52b42c..3fde383 100644 --- a/pjrpc/client/backend/aiohttp.py +++ b/pjrpc/client/backend/aiohttp.py @@ -7,10 +7,9 @@ from aiohttp.typedefs import LooseCookies, LooseHeaders, StrOrURL from multidict import MultiDict -import pjrpc -from pjrpc.client import AbstractAsyncClient, AsyncMiddleware -from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, JsonRpcError -from pjrpc.common import Request, Response, generators +from pjrpc.client import AbstractAsyncClient, AsyncMiddleware, exceptions +from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, Request, Response +from pjrpc.common import generators from pjrpc.common.typedefs import JsonRpcRequestIdT @@ -60,7 +59,7 @@ def __init__( session: Optional[client.ClientSession] = None, raise_for_status: bool = True, id_gen_impl: Callable[..., Generator[JsonRpcRequestIdT, None, None]] = generators.sequential, - error_cls: type[JsonRpcError] = JsonRpcError, + error_cls: type[exceptions.JsonRpcError] = exceptions.JsonRpcError, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -130,7 +129,7 @@ async def _request( content_type = resp.headers.get('Content-Type', '') if response_text and content_type.split(';')[0] not in self._response_content_types: - raise pjrpc.exc.DeserializationError(f"unexpected response content type: {content_type}") + raise exceptions.DeserializationError(f"unexpected response content type: {content_type}") return response_text diff --git a/pjrpc/client/backend/httpx.py b/pjrpc/client/backend/httpx.py index 5d1da43..f543015 100644 --- a/pjrpc/client/backend/httpx.py +++ b/pjrpc/client/backend/httpx.py @@ -4,10 +4,9 @@ import httpx -import pjrpc -from pjrpc.client import AbstractAsyncClient, AbstractClient, AsyncMiddleware, Middleware -from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, JsonRpcError -from pjrpc.common import Request, Response, generators +from pjrpc.client import AbstractAsyncClient, AbstractClient, AsyncMiddleware, Middleware, exceptions +from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, Request, Response +from pjrpc.common import generators from pjrpc.common.typedefs import JsonRpcRequestIdT @@ -42,7 +41,7 @@ def __init__( http_client: Optional[httpx.Client] = None, raise_for_status: bool = True, id_gen_impl: Callable[..., Generator[JsonRpcRequestIdT, None, None]] = generators.sequential, - error_cls: type[JsonRpcError] = JsonRpcError, + error_cls: type[exceptions.JsonRpcError] = exceptions.JsonRpcError, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -111,7 +110,7 @@ def _request( response_text = resp.text content_type = resp.headers.get('Content-Type', '') if response_text and content_type.split(';')[0] not in self._response_content_types: - raise pjrpc.exc.DeserializationError(f"unexpected response content type: {content_type}") + raise exceptions.DeserializationError(f"unexpected response content type: {content_type}") return response_text @@ -156,7 +155,7 @@ def __init__( http_client: Optional[httpx.AsyncClient] = None, raise_for_status: bool = True, id_gen_impl: Callable[..., Generator[JsonRpcRequestIdT, None, None]] = generators.sequential, - error_cls: type[JsonRpcError] = JsonRpcError, + error_cls: type[exceptions.JsonRpcError] = exceptions.JsonRpcError, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -230,7 +229,7 @@ async def _request( content_type = resp.headers.get('Content-Type', '') if response_text and content_type.split(';')[0] not in self._response_content_types: - raise pjrpc.exc.DeserializationError(f"unexpected response content type: {content_type}") + raise exceptions.DeserializationError(f"unexpected response content type: {content_type}") return response_text diff --git a/pjrpc/client/backend/requests.py b/pjrpc/client/backend/requests.py index fc634ac..91e06e9 100644 --- a/pjrpc/client/backend/requests.py +++ b/pjrpc/client/backend/requests.py @@ -5,10 +5,9 @@ import requests.auth import requests.cookies -import pjrpc -from pjrpc.client import AbstractClient, Middleware -from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, JsonRpcError -from pjrpc.common import Request, Response, generators +from pjrpc.client import AbstractClient, Middleware, exceptions +from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, JSONEncoder, Request, Response +from pjrpc.common import generators from pjrpc.common.typedefs import JsonRpcRequestIdT @@ -45,7 +44,7 @@ def __init__( session: Optional[requests.Session] = None, raise_for_status: bool = True, id_gen_impl: Callable[..., Generator[JsonRpcRequestIdT, None, None]] = generators.sequential, - error_cls: type[JsonRpcError] = JsonRpcError, + error_cls: type[exceptions.JsonRpcError] = exceptions.JsonRpcError, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -113,7 +112,7 @@ def _request( response_text = resp.text content_type = resp.headers.get('Content-Type', '') if response_text and content_type.split(';')[0] not in self._response_content_types: - raise pjrpc.exc.DeserializationError(f"unexpected response content type: {content_type}") + raise exceptions.DeserializationError(f"unexpected response content type: {content_type}") return response_text diff --git a/pjrpc/client/client.py b/pjrpc/client/client.py index bf42f30..d58590a 100644 --- a/pjrpc/client/client.py +++ b/pjrpc/client/client.py @@ -7,9 +7,11 @@ from pjrpc import common from pjrpc.common import UNSET, AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, MaybeSet, Request -from pjrpc.common import Response, exceptions, generators +from pjrpc.common import Response, generators from pjrpc.common.typedefs import JsonRpcRequestIdT, JsonT +from . import exceptions + logger = logging.getLogger(__package__) diff --git a/pjrpc/client/exceptions.py b/pjrpc/client/exceptions.py new file mode 100644 index 0000000..e3dd4fa --- /dev/null +++ b/pjrpc/client/exceptions.py @@ -0,0 +1,100 @@ +import dataclasses as dc +from typing import Any, ClassVar, Optional + +from pjrpc.common import UNSET, JsonT, MaybeSet, exceptions +from pjrpc.common.exceptions import BaseError, DeserializationError, IdentityError, ProtocolError + +__all__ = [ + 'BaseError', + 'DeserializationError', + 'IdentityError', + 'InternalError', + 'InvalidParamsError', + 'InvalidRequestError', + 'JsonRpcError', + 'MethodNotFoundError', + 'ParseError', + 'ProtocolError', + 'ServerError', + 'TypedError', +] + + +@dc.dataclass(frozen=True) +class JsonRpcError(exceptions.JsonRpcError): + """ + Client JSON-RPC error. + """ + + # typed subclasses error mapping + __TYPED_ERRORS__: ClassVar[dict[int, type['TypedError']]] = {} + + @classmethod + def get_typed_error_by_code(cls, code: int, message: str, data: MaybeSet[JsonT]) -> Optional['JsonRpcError']: + if error_cls := cls.__TYPED_ERRORS__.get(code): + return error_cls(message, data) + else: + return None + + +class TypedError(JsonRpcError): + """ + Typed JSON-RPC error. + Must not be instantiated directly, only subclassed. + """ + + # a number that indicates the error type that occurred + CODE: ClassVar[int] + + # a string providing a short description of the error. + # the message SHOULD be limited to a concise single sentence. + MESSAGE: ClassVar[str] + + def __init_subclass__(cls, base: bool = False, **kwargs: Any): + super().__init_subclass__(**kwargs) + if base: + cls.__TYPED_ERRORS__ = cls.__TYPED_ERRORS__.copy() + + if issubclass(cls, TypedError) and (code := getattr(cls, 'CODE', None)) is not None: + cls.__TYPED_ERRORS__[code] = cls + + def __init__(self, message: Optional[str] = None, data: MaybeSet[JsonT] = UNSET): + super().__init__(self.CODE, message or self.MESSAGE, data) + + +class ParseError(TypedError, exceptions.ParseError): + """ + Invalid JSON was received by the server. + An error occurred on the server while parsing the JSON text. + """ + + +class InvalidRequestError(TypedError, exceptions.InvalidRequestError): + """ + The JSON sent is not a valid request object. + """ + + +class MethodNotFoundError(TypedError, exceptions.MethodNotFoundError): + """ + The method does not exist / is not available. + """ + + +class InvalidParamsError(TypedError, exceptions.InvalidParamsError): + """ + Invalid method parameter(s). + """ + + +class InternalError(TypedError, exceptions.InternalError): + """ + Internal JSON-RPC error. + """ + + +class ServerError(TypedError, exceptions.ServerError): + """ + Reserved for implementation-defined server-errors. + Codes from -32000 to -32099. + """ diff --git a/pjrpc/client/integrations/pytest.py b/pjrpc/client/integrations/pytest.py index e28667e..9eb0fca 100644 --- a/pjrpc/client/integrations/pytest.py +++ b/pjrpc/client/integrations/pytest.py @@ -12,6 +12,7 @@ import pjrpc from pjrpc import Response +from pjrpc.client import exceptions from pjrpc.common import UNSET, MaybeSet from pjrpc.common.typedefs import JsonRpcParamsT, JsonRpcRequestIdT @@ -264,7 +265,7 @@ def _match_request( ) -> Response: matches = self._matches[endpoint].get((version, method_name)) if matches is None: - return pjrpc.Response(id=id, error=pjrpc.exc.MethodNotFoundError(data=method_name)) + return pjrpc.Response(id=id, error=exceptions.MethodNotFoundError(data=method_name).to_json()) match = matches.pop(0) if not match.once: diff --git a/pjrpc/client/validators.py b/pjrpc/client/validators.py index f8a54b7..5658fe6 100644 --- a/pjrpc/client/validators.py +++ b/pjrpc/client/validators.py @@ -1,7 +1,8 @@ from typing import Any, Mapping, Optional +from pjrpc.client import exceptions from pjrpc.client.client import AsyncMiddlewareHandler, MiddlewareHandler -from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, Request, Response, exceptions +from pjrpc.common import AbstractRequest, AbstractResponse, BatchRequest, BatchResponse, Request, Response def validate_response_id_middleware( diff --git a/pjrpc/common/exceptions.py b/pjrpc/common/exceptions.py index 3161058..115efdd 100644 --- a/pjrpc/common/exceptions.py +++ b/pjrpc/common/exceptions.py @@ -47,16 +47,13 @@ class JsonRpcError(BaseError): :param data: value that contains additional information about the error. May be omitted. """ - # typed subclasses error mapping - __TYPED_ERRORS__: ClassVar[dict[int, type['TypedError']]] = {} - code: int message: str data: MaybeSet[JsonT] = dc.field(repr=False, default=UNSET) @classmethod - def get_error_by_code(cls, code: int) -> Optional[type['TypedError']]: - return cls.__TYPED_ERRORS__.get(code) + def get_typed_error_by_code(cls, code: int, message: str, data: MaybeSet[JsonT]) -> Optional['JsonRpcError']: + return None @classmethod def from_json(cls, json_data: JsonT) -> 'JsonRpcError': @@ -83,10 +80,10 @@ def from_json(cls, json_data: JsonT) -> 'JsonRpcError': data = json_data.get('data', UNSET) - if error_class := cls.get_error_by_code(code): - return error_class(message, data) + if typed_error := cls.get_typed_error_by_code(code, message, data): + return typed_error else: - return JsonRpcError(code, message, data) + return cls(code, message, data) except KeyError as e: raise DeserializationError(f"required field {e} not found") from e @@ -111,39 +108,7 @@ def __str__(self) -> str: return f"({self.code}) {self.message}" -class TypedError(JsonRpcError): - """ - Typed JSON-RPC error. - Must not be instantiated directly, only subclassed. - """ - - # a number that indicates the error type that occurred - CODE: ClassVar[int] - - # a string providing a short description of the error. - # the message SHOULD be limited to a concise single sentence. - MESSAGE: ClassVar[str] - - def __init_subclass__(cls, base: bool = False, **kwargs: Any): - super().__init_subclass__(**kwargs) - if base: - cls.__TYPED_ERRORS__ = cls.__TYPED_ERRORS__.copy() - - if issubclass(cls, TypedError) and (code := getattr(cls, 'CODE', None)) is not None: - cls.__TYPED_ERRORS__[code] = cls - - def __init__(self, message: Optional[str] = None, data: MaybeSet[JsonT] = UNSET): - super().__init__(self.CODE, message or self.MESSAGE, data) - - -class ClientBaseError(TypedError): - """ - Raised when a client sent an incorrect request. - Must not be instantiated directly, only subclassed. - """ - - -class ParseError(ClientBaseError): +class ParseError: """ Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text. @@ -153,7 +118,7 @@ class ParseError(ClientBaseError): MESSAGE: ClassVar[str] = 'Parse error' -class InvalidRequestError(ClientBaseError): +class InvalidRequestError: """ The JSON sent is not a valid request object. """ @@ -162,7 +127,7 @@ class InvalidRequestError(ClientBaseError): MESSAGE: ClassVar[str] = 'Invalid Request' -class MethodNotFoundError(ClientBaseError): +class MethodNotFoundError: """ The method does not exist / is not available. """ @@ -171,7 +136,7 @@ class MethodNotFoundError(ClientBaseError): MESSAGE: ClassVar[str] = 'Method not found' -class InvalidParamsError(ClientBaseError): +class InvalidParamsError: """ Invalid method parameter(s). """ @@ -180,14 +145,7 @@ class InvalidParamsError(ClientBaseError): MESSAGE: ClassVar[str] = 'Invalid params' -class ServerBaseError(TypedError): - """ - Raised when a server failed to process a reqeust. - Must not be instantiated directly, only subclassed. - """ - - -class InternalError(ServerBaseError): +class InternalError: """ Internal JSON-RPC error. """ @@ -196,7 +154,7 @@ class InternalError(ServerBaseError): MESSAGE: ClassVar[str] = 'Internal error' -class ServerError(ServerBaseError): +class ServerError: """ Reserved for implementation-defined server-errors. Codes from -32000 to -32099. diff --git a/pjrpc/common/response.py b/pjrpc/common/response.py index 976b722..37122c2 100644 --- a/pjrpc/common/response.py +++ b/pjrpc/common/response.py @@ -212,7 +212,7 @@ def from_json( except KeyError as e: raise DeserializationError(f"required field {e} not found") from e - responses = tuple(Response.from_json(item) for item in data) + responses = tuple(Response.from_json(item, error_cls=error_cls) for item in data) if check_ids: cls._check_response_id_uniqueness(responses) diff --git a/pjrpc/server/__init__.py b/pjrpc/server/__init__.py index ad6fcd4..5849aab 100644 --- a/pjrpc/server/__init__.py +++ b/pjrpc/server/__init__.py @@ -2,8 +2,9 @@ JSON-RPC server package. """ -from . import typedefs +from . import exceptions, typedefs from .dispatcher import AsyncDispatcher, BaseDispatcher, Dispatcher, JSONEncoder, Method, MethodRegistry +from .exceptions import JsonRpcError from .typedefs import AsyncHandlerType, AsyncMiddlewareType, HandlerType, MiddlewareType from .utils import exclude_named_param, exclude_positional_param @@ -13,10 +14,12 @@ 'AsyncMiddlewareType', 'BaseDispatcher', 'Dispatcher', + 'exceptions', 'exclude_named_param', 'exclude_positional_param', 'HandlerType', 'JSONEncoder', + 'JsonRpcError', 'Method', 'MethodRegistry', 'MiddlewareType', diff --git a/pjrpc/server/dispatcher.py b/pjrpc/server/dispatcher.py index 43863e1..077a8f4 100644 --- a/pjrpc/server/dispatcher.py +++ b/pjrpc/server/dispatcher.py @@ -10,7 +10,7 @@ import pjrpc from pjrpc.common import UNSET, AbstractResponse, BatchRequest, BatchResponse, MaybeSet, Request, Response, UnsetType from pjrpc.common.typedefs import JsonRpcParamsT -from pjrpc.server import utils +from pjrpc.server import exceptions, utils from pjrpc.server.typedefs import AsyncMiddlewareType, MiddlewareType from . import validators @@ -263,8 +263,8 @@ class BaseDispatcher: :param request_class: JSON-RPC request class :param response_class: JSON-RPC response class - :param batch_request: JSON-RPC batch request class - :param batch_response: JSON-RPC batch response class + :param batch_request_class: JSON-RPC batch request class + :param batch_response_class: JSON-RPC batch response class :param json_loader: request json loader :param json_dumper: response json dumper :param json_encoder: response json encoder @@ -277,8 +277,8 @@ def __init__( *, request_class: type[Request] = Request, response_class: type[Response] = Response, - batch_request: type[BatchRequest] = BatchRequest, - batch_response: type[BatchResponse] = BatchResponse, + batch_request_class: type[BatchRequest] = BatchRequest, + batch_response_class: type[BatchResponse] = BatchResponse, json_loader: Callable[..., Any] = json.loads, json_dumper: Callable[..., str] = json.dumps, json_encoder: type[JSONEncoder] = JSONEncoder, @@ -291,8 +291,8 @@ def __init__( self._json_decoder = json_decoder self._request_class = request_class self._response_class = response_class - self._batch_request = batch_request - self._batch_response = batch_response + self._batch_request_class = batch_request_class + self._batch_response_class = batch_response_class self._middlewares = list(middlewares) self._registry = MethodRegistry() @@ -386,29 +386,29 @@ def dispatch(self, request_text: str, context: ContextType) -> Optional[tuple[st request_json = self._json_loader(request_text, cls=self._json_decoder) request: Union[Request, BatchRequest] if isinstance(request_json, (list, tuple)): - request = self._batch_request.from_json(request_json) + request = self._batch_request_class.from_json(request_json) else: request = self._request_class.from_json(request_json) except json.JSONDecodeError as e: - response = self._response_class(id=None, error=pjrpc.exceptions.ParseError(data=str(e))) + response = self._response_class(id=None, error=exceptions.ParseError(data=str(e))) - except (pjrpc.exceptions.DeserializationError, pjrpc.exceptions.IdentityError) as e: - response = self._response_class(id=None, error=pjrpc.exceptions.InvalidRequestError(data=str(e))) + except (exceptions.DeserializationError, exceptions.IdentityError) as e: + response = self._response_class(id=None, error=exceptions.InvalidRequestError(data=str(e))) else: if isinstance(request, BatchRequest): if self._max_batch_size and len(request) > self._max_batch_size: response = self._response_class( id=None, - error=pjrpc.exceptions.InvalidRequestError(data="batch too large"), + error=exceptions.InvalidRequestError(data="batch too large"), ) else: responses = ( resp for resp in self._executor.execute(self._request_handler, request, context) if not isinstance(resp, UnsetType) ) - response = self._batch_response(tuple(responses)) + response = self._batch_response_class(tuple(responses)) else: response = self._request_handler(request, context) @@ -438,13 +438,13 @@ def _wrap_handle_request(self) -> Callable[[Request, ContextType], MaybeSet[Resp def _handle_request(self, request: Request, /, context: ContextType) -> MaybeSet[Response]: try: return self._handle_rpc_request(request, context) - except pjrpc.exceptions.JsonRpcError as e: + except exceptions.JsonRpcError as e: logger.info("method execution error %s(%r): %r", request.method, request.params, e) error = e except Exception as e: logger.exception("internal server error: %r", e) - error = pjrpc.exceptions.InternalError() + error = exceptions.InternalError() if request.id is None: return UNSET @@ -466,22 +466,22 @@ def _handle_rpc_method( ) -> Any: method = self._registry.get(method_name) if method is None: - raise pjrpc.exceptions.MethodNotFoundError(data=f"method '{method_name}' not found") + raise exceptions.MethodNotFoundError(data=f"method '{method_name}' not found") try: bound_method = method.bind(params, context=context) except validators.ValidationError as e: - raise pjrpc.exceptions.InvalidParamsError(data=e) from e + raise exceptions.InvalidParamsError(data=e) from e try: return bound_method() - except pjrpc.exceptions.JsonRpcError: + except exceptions.JsonRpcError: raise except Exception as e: logger.exception("method unhandled exception %s(%r): %r", method_name, params, e) - raise pjrpc.exceptions.ServerError() from e + raise exceptions.ServerError() from e class AsyncExecutor(abc.ABC): @@ -577,29 +577,29 @@ async def dispatch(self, request_text: str, context: ContextType) -> Optional[tu request_json = self._json_loader(request_text, cls=self._json_decoder) request: Union[Request, BatchRequest] if isinstance(request_json, (list, tuple)): - request = self._batch_request.from_json(request_json) + request = self._batch_request_class.from_json(request_json) else: request = self._request_class.from_json(request_json) except json.JSONDecodeError as e: - response = self._response_class(id=None, error=pjrpc.exceptions.ParseError(data=str(e))) + response = self._response_class(id=None, error=exceptions.ParseError(data=str(e))) except (pjrpc.exceptions.DeserializationError, pjrpc.exceptions.IdentityError) as e: - response = self._response_class(id=None, error=pjrpc.exceptions.InvalidRequestError(data=str(e))) + response = self._response_class(id=None, error=exceptions.InvalidRequestError(data=str(e))) else: if isinstance(request, BatchRequest): if self._max_batch_size and len(request) > self._max_batch_size: response = self._response_class( id=None, - error=pjrpc.exceptions.InvalidRequestError(data="batch too large"), + error=exceptions.InvalidRequestError(data="batch too large"), ) else: responses = ( resp for resp in await self._executor.execute(self._handle_request, request, context) if not isinstance(resp, UnsetType) ) - response = self._batch_response(tuple(responses)) + response = self._batch_response_class(tuple(responses)) else: response = await self._handle_request(request, context) @@ -630,13 +630,13 @@ def _wrap_handle_rpc_request(self) -> Callable[[Request, ContextType], Awaitable async def _handle_request(self, request: Request, context: ContextType) -> MaybeSet[Response]: try: return await self._rpc_request_handler(request, context) - except pjrpc.exceptions.JsonRpcError as e: + except exceptions.JsonRpcError as e: logger.info("method execution error %s(%r): %r", request.method, request.params, e) error = e except Exception as e: logger.exception("internal server error: %r", e) - error = pjrpc.exceptions.InternalError() + error = exceptions.InternalError() if request.id is None: return UNSET @@ -655,12 +655,12 @@ async def _handle_rpc_method( ) -> Any: method = self._registry.get(method_name) if method is None: - raise pjrpc.exceptions.MethodNotFoundError(data=f"method '{method_name}' not found") + raise exceptions.MethodNotFoundError(data=f"method '{method_name}' not found") try: bound_method = method.bind(params, context=context) except validators.ValidationError as e: - raise pjrpc.exceptions.InvalidParamsError(data=e) from e + raise exceptions.InvalidParamsError(data=e) from e try: result = bound_method() @@ -669,9 +669,9 @@ async def _handle_rpc_method( return result - except pjrpc.exceptions.JsonRpcError: + except exceptions.JsonRpcError: raise except Exception as e: logger.exception("method unhandled exception %s(%r): %r", method_name, params, e) - raise pjrpc.exceptions.ServerError() from e + raise exceptions.ServerError() from e diff --git a/pjrpc/server/exceptions.py b/pjrpc/server/exceptions.py new file mode 100644 index 0000000..1474c98 --- /dev/null +++ b/pjrpc/server/exceptions.py @@ -0,0 +1,98 @@ +import dataclasses as dc +from typing import Any, ClassVar, Optional + +from pjrpc.common import UNSET, JsonT, MaybeSet, exceptions +from pjrpc.common.exceptions import BaseError, DeserializationError, IdentityError, ProtocolError + +__all__ = [ + 'BaseError', + 'DeserializationError', + 'IdentityError', + 'InternalError', + 'InvalidParamsError', + 'InvalidRequestError', + 'JsonRpcError', + 'MethodNotFoundError', + 'ParseError', + 'ProtocolError', + 'ServerError', + 'TypedError', +] + + +@dc.dataclass(frozen=True) +class JsonRpcError(exceptions.JsonRpcError): + """ + Server JSON-RPC error. + """ + + # typed subclasses error mapping + __TYPED_ERRORS__: ClassVar[dict[int, type['TypedError']]] = {} + + @classmethod + def get_typed_error_by_code(cls, code: int, message: str, data: MaybeSet[JsonT]) -> Optional['JsonRpcError']: + if error_cls := cls.__TYPED_ERRORS__.get(code): + return error_cls(message, data) + else: + return None + + +class TypedError(JsonRpcError): + """ + Typed JSON-RPC error. + Must not be instantiated directly, only subclassed. + """ + + # a number that indicates the error type that occurred + CODE: ClassVar[int] + + # a string providing a short description of the error. + # the message SHOULD be limited to a concise single sentence. + MESSAGE: ClassVar[str] + + def __init_subclass__(cls, base: bool = False, **kwargs: Any): + super().__init_subclass__(**kwargs) + + if issubclass(cls, TypedError) and (code := getattr(cls, 'CODE', None)) is not None: + cls.__TYPED_ERRORS__[code] = cls + + def __init__(self, message: Optional[str] = None, data: MaybeSet[JsonT] = UNSET): + super().__init__(self.CODE, message or self.MESSAGE, data) + + +class ParseError(TypedError, exceptions.ParseError): + """ + Invalid JSON was received by the server. + An error occurred on the server while parsing the JSON text. + """ + + +class InvalidRequestError(TypedError, exceptions.InvalidRequestError): + """ + The JSON sent is not a valid request object. + """ + + +class MethodNotFoundError(TypedError, exceptions.MethodNotFoundError): + """ + The method does not exist / is not available. + """ + + +class InvalidParamsError(TypedError, exceptions.InvalidParamsError): + """ + Invalid method parameter(s). + """ + + +class InternalError(TypedError, exceptions.InternalError): + """ + Internal JSON-RPC error. + """ + + +class ServerError(TypedError, exceptions.ServerError): + """ + Reserved for implementation-defined server-errors. + Codes from -32000 to -32099. + """ diff --git a/pjrpc/server/specs/extractors/__init__.py b/pjrpc/server/specs/extractors/__init__.py index d0e9d8d..c230351 100644 --- a/pjrpc/server/specs/extractors/__init__.py +++ b/pjrpc/server/specs/extractors/__init__.py @@ -3,7 +3,7 @@ from typing import Any, Callable, Iterable, Optional from pjrpc.common import UNSET, MaybeSet, UnsetType -from pjrpc.common.exceptions import TypedError +from pjrpc.server import exceptions __all__ = [ 'BaseMethodInfoExtractor', @@ -59,7 +59,7 @@ def extract_response_schema( method_name: str, method: MethodType, ref_template: str, - errors: Optional[Iterable[type[TypedError]]] = None, + errors: Optional[Iterable[type[exceptions.TypedError]]] = None, ) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]: """ Extracts response schema. @@ -72,7 +72,7 @@ def extract_error_response_schema( method_name: str, method: MethodType, ref_template: str, - errors: Optional[Iterable[type[TypedError]]] = None, + errors: Optional[Iterable[type[exceptions.TypedError]]] = None, ) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]: """ Extracts error response schema. @@ -109,7 +109,7 @@ def extract_summary(self, method: MethodType) -> MaybeSet[str]: return summary - def extract_errors(self, method: MethodType) -> MaybeSet[list[type[TypedError]]]: + def extract_errors(self, method: MethodType) -> MaybeSet[list[type[exceptions.TypedError]]]: """ Extracts method errors. """ diff --git a/pjrpc/server/specs/extractors/pydantic.py b/pjrpc/server/specs/extractors/pydantic.py index d29b745..9710cb1 100644 --- a/pjrpc/server/specs/extractors/pydantic.py +++ b/pjrpc/server/specs/extractors/pydantic.py @@ -3,7 +3,7 @@ import pydantic as pd -from pjrpc.common import exceptions +from pjrpc.server import exceptions from pjrpc.server.specs.extractors import BaseMethodInfoExtractor, ExcludeFunc __all__ = [ diff --git a/pjrpc/server/specs/extractors/pydantic_v1.py b/pjrpc/server/specs/extractors/pydantic_v1.py index ef74b35..dec3cee 100644 --- a/pjrpc/server/specs/extractors/pydantic_v1.py +++ b/pjrpc/server/specs/extractors/pydantic_v1.py @@ -4,7 +4,7 @@ import pydantic as pd import pydantic.generics -from pjrpc.common import exceptions +from pjrpc.server import exceptions from pjrpc.server.specs.extractors import BaseMethodInfoExtractor, ExcludeFunc MethodType = Callable[..., Any] diff --git a/pjrpc/server/specs/openapi/__init__.py b/pjrpc/server/specs/openapi/__init__.py index 52c43cf..73168b2 100644 --- a/pjrpc/server/specs/openapi/__init__.py +++ b/pjrpc/server/specs/openapi/__init__.py @@ -6,8 +6,8 @@ from collections import defaultdict from typing import Any, Callable, Iterable, Mapping, Optional, Union -from pjrpc.common import UNSET, MaybeSet, UnsetType, exceptions -from pjrpc.server import Method, utils +from pjrpc.common import UNSET, MaybeSet, UnsetType +from pjrpc.server import Method, exceptions, utils from pjrpc.server.specs import Specification from pjrpc.server.specs.extractors import BaseMethodInfoExtractor from pjrpc.server.specs.schemas import build_request_schema, build_response_schema diff --git a/pjrpc/server/specs/openrpc/__init__.py b/pjrpc/server/specs/openrpc/__init__.py index d698527..7df7e52 100644 --- a/pjrpc/server/specs/openrpc/__init__.py +++ b/pjrpc/server/specs/openrpc/__init__.py @@ -5,8 +5,8 @@ import dataclasses as dc from typing import Any, Callable, Iterable, Mapping, Union -from pjrpc.common import UNSET, MaybeSet, UnsetType, exceptions -from pjrpc.server.dispatcher import Method +from pjrpc.common import UNSET, MaybeSet, UnsetType +from pjrpc.server.dispatcher import Method, exceptions from pjrpc.server.specs import Specification from pjrpc.server.specs.extractors import BaseMethodInfoExtractor diff --git a/pjrpc/server/specs/schemas.py b/pjrpc/server/specs/schemas.py index ac1c5a4..4f8b19f 100644 --- a/pjrpc/server/specs/schemas.py +++ b/pjrpc/server/specs/schemas.py @@ -1,7 +1,7 @@ import copy from typing import Any, Dict, Iterable, List, Type -from pjrpc.common.exceptions import TypedError +from pjrpc.server.exceptions import TypedError REQUEST_SCHEMA: Dict[str, Any] = { 'title': 'Request', diff --git a/tests/client/test_client_response.py b/tests/client/test_client_response.py new file mode 100644 index 0000000..f62ff41 --- /dev/null +++ b/tests/client/test_client_response.py @@ -0,0 +1,62 @@ +from pjrpc.client import exceptions +from pjrpc.common import BatchResponse, Response + + +def test_response_error_serialization(): + response = Response(error=exceptions.MethodNotFoundError()) + actual_dict = response.to_json() + expected_dict = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + + assert actual_dict == expected_dict + + +def test_batch_response_error_serialization(): + response = BatchResponse(error=exceptions.MethodNotFoundError()) + actual_dict = response.to_json() + expected_dict = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + + assert actual_dict == expected_dict + + +def test_response_error_deserialization(): + data = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + response = Response.from_json(data, error_cls=exceptions.JsonRpcError) + + assert response.is_error + assert response.error == exceptions.MethodNotFoundError() + + +def test_batch_response_error_deserialization(): + data = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + response = BatchResponse.from_json(data, error_cls=exceptions.JsonRpcError) + + assert response.is_error + assert response.error == exceptions.MethodNotFoundError() diff --git a/tests/client/test_pytest_plugin.py b/tests/client/test_pytest_plugin.py index e8731bb..6d2c034 100644 --- a/tests/client/test_pytest_plugin.py +++ b/tests/client/test_pytest_plugin.py @@ -3,6 +3,7 @@ import pytest import pjrpc +from pjrpc.client import exceptions from pjrpc.client.integrations.pytest import PjRpcMocker @@ -41,17 +42,19 @@ def test_pjrpc_mocker_result_error_id(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method1').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result' - mocker.add(endpoint, 'method2', error=pjrpc.exc.JsonRpcError(code=1, message='message')) + mocker.add(endpoint, 'method2', error=exceptions.JsonRpcError(code=1, message='message')) response = pjrpc.Response.from_json( json.loads( cli._request(json.dumps(pjrpc.Request(method='method2').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) - assert response.error == pjrpc.exc.JsonRpcError(code=1, message='message') + assert response.error == exceptions.JsonRpcError(code=1, message='message') def test_pjrpc_mocker_once_param(cli, endpoint): @@ -61,6 +64,7 @@ def test_pjrpc_mocker_once_param(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result' @@ -68,6 +72,7 @@ def test_pjrpc_mocker_once_param(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'original_result' @@ -81,6 +86,7 @@ def test_pjrpc_mocker_round_robin(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result1' @@ -88,6 +94,7 @@ def test_pjrpc_mocker_round_robin(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result2' @@ -95,6 +102,7 @@ def test_pjrpc_mocker_round_robin(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result1' @@ -106,6 +114,7 @@ def test_pjrpc_replace_remove(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result1' @@ -114,6 +123,7 @@ def test_pjrpc_replace_remove(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result2' @@ -122,6 +132,7 @@ def test_pjrpc_replace_remove(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'original_result' @@ -161,6 +172,7 @@ def callback(**kwargs): {}, ), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'result' @@ -174,6 +186,7 @@ def test_pjrpc_mocker_passthrough(cli, endpoint): json.loads( cli._request(json.dumps(pjrpc.Request(method='method2').to_json()), False, {}), ), + error_cls=exceptions.JsonRpcError, ) assert response.result == 'original_result' diff --git a/tests/client/test_retry.py b/tests/client/test_retry.py index 1c955ee..d16ec91 100644 --- a/tests/client/test_retry.py +++ b/tests/client/test_retry.py @@ -2,8 +2,7 @@ import responses from aioresponses import aioresponses -import pjrpc -from pjrpc.client import retry +from pjrpc.client import exceptions, retry from pjrpc.client.backend import aiohttp as aiohttp_backend from pjrpc.client.backend import requests as requests_backend @@ -75,7 +74,7 @@ async def test_async_client_retry_strategy_by_code(resp_code, resp_errors, retry actual_result = await client.proxy.method() assert actual_result == expected_result else: - with pytest.raises(pjrpc.exceptions.JsonRpcError) as err: + with pytest.raises(exceptions.JsonRpcError) as err: await client.proxy.method() assert err.value.code == resp_code @@ -181,7 +180,7 @@ def test_client_retry_strategy_by_code(resp_code, resp_errors, retry_codes, retr actual_result = client.proxy.method() assert actual_result == expected_result else: - with pytest.raises(pjrpc.exceptions.JsonRpcError) as err: + with pytest.raises(exceptions.JsonRpcError) as err: client.proxy.method() assert err.value.code == resp_code diff --git a/tests/client/test_sync_clients.py b/tests/client/test_sync_clients.py index aa4b5f6..aa7bb51 100644 --- a/tests/client/test_sync_clients.py +++ b/tests/client/test_sync_clients.py @@ -5,6 +5,7 @@ import respx import pjrpc +from pjrpc.client import exceptions from pjrpc.client.backend import httpx as httpx_backend from pjrpc.client.backend import requests as requests_backend @@ -325,7 +326,7 @@ def test_batch(Client, mocker): @pytest.mark.parametrize( 'Client, mocker', [ (requests_backend.Client, ResponsesMocker), - # (httpx_backend.Client, RespxMocker), + (httpx_backend.Client, RespxMocker), ], ) def test_error(Client, mocker): @@ -345,7 +346,7 @@ def test_error(Client, mocker): client = Client(test_url) - with pytest.raises(pjrpc.exceptions.MethodNotFoundError): + with pytest.raises(exceptions.MethodNotFoundError): client('method', 1, 2) assert mock.requests[0].url == test_url @@ -367,7 +368,7 @@ def test_error(Client, mocker): }, ) - with pytest.raises(pjrpc.exceptions.InvalidRequestError): + with pytest.raises(exceptions.InvalidRequestError): with client.batch() as batch: batch.call('method', 'param') @@ -391,7 +392,7 @@ def test_error(Client, mocker): client = Client(test_url) - with pytest.raises(pjrpc.exceptions.IdentityError): + with pytest.raises(pjrpc.common.exceptions.IdentityError): with client.batch() as batch: batch.call('method1', 'param') batch.call('method2', 'param') diff --git a/tests/common/test_error.py b/tests/common/test_error.py index 2abb94f..16c4a6f 100644 --- a/tests/common/test_error.py +++ b/tests/common/test_error.py @@ -1,10 +1,11 @@ import pytest import pjrpc +from pjrpc.client import exceptions def test_error_serialization(): - error = pjrpc.exc.ServerError() + error = exceptions.ServerError() actual_dict = error.to_json() expected_dict = { @@ -21,14 +22,14 @@ def test_error_deserialization(): 'message': 'Server error', } - error = pjrpc.exc.ServerError.from_json(data) + error = exceptions.ServerError.from_json(data) assert error.code == -32000 assert error.message == 'Server error' def test_error_data_serialization(): - error = pjrpc.exc.MethodNotFoundError(data='method_name') + error = exceptions.MethodNotFoundError(data='method_name') actual_dict = error.to_json() expected_dict = { @@ -41,7 +42,7 @@ def test_error_data_serialization(): def test_custom_error_data_serialization(): - error = pjrpc.exc.JsonRpcError(code=2001, message='Custom error', data='additional data') + error = exceptions.JsonRpcError(code=2001, message='Custom error', data='additional data') actual_dict = error.to_json() expected_dict = { @@ -60,7 +61,7 @@ def test_custom_error_data_deserialization(): 'data': 'method_name', } - error = pjrpc.exc.JsonRpcError.from_json(data) + error = exceptions.JsonRpcError.from_json(data) assert error.code == -32601 assert error.message == 'Method not found' @@ -69,24 +70,24 @@ def test_custom_error_data_deserialization(): def test_error_deserialization_errors(): with pytest.raises(pjrpc.exc.DeserializationError, match="data must be of type dict"): - pjrpc.exc.JsonRpcError.from_json([]) + exceptions.JsonRpcError.from_json([]) with pytest.raises(pjrpc.exc.DeserializationError, match="required field 'message' not found"): - pjrpc.exc.JsonRpcError.from_json({'code': 1}) + exceptions.JsonRpcError.from_json({'code': 1}) with pytest.raises(pjrpc.exc.DeserializationError, match="required field 'code' not found"): - pjrpc.exc.JsonRpcError.from_json({'message': ""}) + exceptions.JsonRpcError.from_json({'message': ""}) with pytest.raises(pjrpc.exc.DeserializationError, match="field 'code' must be of type integer"): - pjrpc.exc.JsonRpcError.from_json({'code': "1", 'message': ""}) + exceptions.JsonRpcError.from_json({'code': "1", 'message': ""}) with pytest.raises(pjrpc.exc.DeserializationError, match="field 'message' must be of type string"): - pjrpc.exc.JsonRpcError.from_json({'code': 1, 'message': 2}) + exceptions.JsonRpcError.from_json({'code': 1, 'message': 2}) def test_error_repr(): - assert repr(pjrpc.exc.ServerError()) == "ServerError(code=-32000, message='Server error')" - assert str(pjrpc.exc.ServerError()) == "(-32000) Server error" + assert repr(exceptions.ServerError()) == "ServerError(code=-32000, message='Server error')" + assert str(exceptions.ServerError()) == "(-32000) Server error" def test_custom_error_registration(): @@ -96,11 +97,11 @@ def test_custom_error_registration(): 'data': 'custom_data', } - class CustomError(pjrpc.exc.TypedError): + class CustomError(exceptions.TypedError): CODE = 2000 MESSAGE = 'Custom error' - error = pjrpc.exc.JsonRpcError.from_json(data) + error = exceptions.JsonRpcError.from_json(data) assert isinstance(error, CustomError) assert error.code == 2000 diff --git a/tests/common/test_response_v2.py b/tests/common/test_response_v2.py index 9a21f2b..e8f5760 100644 --- a/tests/common/test_response_v2.py +++ b/tests/common/test_response_v2.py @@ -119,7 +119,7 @@ def test_batch_response_serialization(): assert actual_dict == expected_dict - response = BatchResponse(error=pjrpc.exc.MethodNotFoundError()) + response = BatchResponse(error=pjrpc.exc.JsonRpcError(code=-32601, message="Method not found")) actual_dict = response.to_json() expected_dict = { 'jsonrpc': '2.0', @@ -174,7 +174,7 @@ def test_batch_response_deserialization(): response = BatchResponse.from_json(data) assert response.is_error - assert response.error == pjrpc.exc.MethodNotFoundError() + assert response.error == pjrpc.exc.JsonRpcError(code=-32601, message='Method not found') def test_batch_response_methods(): diff --git a/tests/server/test_aiohttp.py b/tests/server/test_aiohttp.py index 4c2d063..ce90222 100644 --- a/tests/server/test_aiohttp.py +++ b/tests/server/test_aiohttp.py @@ -1,9 +1,8 @@ import pytest from aiohttp import web -from pjrpc import exc from pjrpc.common import BatchRequest, Request, Response -from pjrpc.server import MethodRegistry +from pjrpc.server import MethodRegistry, exceptions from pjrpc.server.integration import aiohttp as integration from tests.common import _ @@ -46,7 +45,7 @@ async def test_request(json_rpc, path, mocker, aiohttp_client, request_id, param raw = await cli.post(path, json=Request(method=method_name, params=params, id=request_id).to_json()) assert raw.status == 200 - resp = Response.from_json(await raw.json()) + resp = Response.from_json(await raw.json(), error_cls=exceptions.JsonRpcError) if isinstance(params, dict): mock.assert_called_once_with(kwargs=params) @@ -79,7 +78,7 @@ async def test_errors(json_rpc, path, mocker, aiohttp_client): method_name = 'test_method' def error_method(*args, **kwargs): - raise exc.JsonRpcError(code=1, message='message') + raise exceptions.JsonRpcError(code=1, message='message') mock = mocker.Mock(name=method_name, side_effect=error_method) @@ -92,20 +91,20 @@ def error_method(*args, **kwargs): raw = await cli.post(path, json=Request(method='unknown_method', params=params, id=request_id).to_json()) assert raw.status == 200 - resp = Response.from_json(await raw.json()) + resp = Response.from_json(await raw.json(), error_cls=exceptions.JsonRpcError) assert resp.id is request_id assert resp.is_error is True - assert resp.error == exc.MethodNotFoundError(data="method 'unknown_method' not found") + assert resp.error == exceptions.MethodNotFoundError(data="method 'unknown_method' not found") # customer error raw = await cli.post(path, json=Request(method=method_name, params=params, id=request_id).to_json()) assert raw.status == 200 - resp = Response.from_json(await raw.json()) + resp = Response.from_json(await raw.json(), error_cls=exceptions.JsonRpcError) mock.assert_called_once_with(args=params) assert resp.id == request_id assert resp.is_error is True - assert resp.error == exc.JsonRpcError(code=1, message='message') + assert resp.error == exceptions.JsonRpcError(code=1, message='message') # content type error raw = await cli.post(path, data='') @@ -114,10 +113,10 @@ def error_method(*args, **kwargs): # malformed json raw = await cli.post(path, headers={'Content-Type': 'application/json'}, data='') assert raw.status == 200 - resp = Response.from_json(await raw.json()) + resp = Response.from_json(await raw.json(), error_cls=exceptions.JsonRpcError) assert resp.id is None assert resp.is_error is True - assert resp.error == exc.ParseError(data=_) + assert resp.error == exceptions.ParseError(data=_) # decoding error raw = await cli.post(path, headers={'Content-Type': 'application/json'}, data=b'\xff') diff --git a/tests/server/test_flask.py b/tests/server/test_flask.py index 4e5f0bb..317cb6c 100644 --- a/tests/server/test_flask.py +++ b/tests/server/test_flask.py @@ -1,8 +1,8 @@ import flask import pytest -from pjrpc import exc from pjrpc.common import BatchRequest, Request, Response +from pjrpc.server import exceptions from pjrpc.server.dispatcher import MethodRegistry from pjrpc.server.integration import flask as integration from tests.common import _ @@ -51,7 +51,7 @@ def test_request(app, json_rpc, path, mocker, request_id, params, result): raw = cli.post(path, json=Request(method=method_name, params=params, id=request_id).to_json()) assert raw.status_code == 200 - resp = Response.from_json(raw.json) + resp = Response.from_json(raw.json, error_cls=exceptions.JsonRpcError) if isinstance(params, dict): mock.assert_called_once_with(kwargs=params) @@ -84,7 +84,7 @@ def test_errors(app, json_rpc, path, mocker): method_name = 'test_method' def error_method(*args, **kwargs): - raise exc.JsonRpcError(code=1, message='message') + raise exceptions.JsonRpcError(code=1, message='message') mock = mocker.Mock(name=method_name, side_effect=error_method) @@ -97,20 +97,20 @@ def error_method(*args, **kwargs): raw = cli.post(path, json=Request(method='unknown_method', params=params, id=request_id).to_json()) assert raw.status_code == 200 - resp = Response.from_json(raw.json) + resp = Response.from_json(raw.json, error_cls=exceptions.JsonRpcError) assert resp.id is request_id assert resp.is_error is True - assert resp.error == exc.MethodNotFoundError(data="method 'unknown_method' not found") + assert resp.error == exceptions.MethodNotFoundError(data="method 'unknown_method' not found") # customer error raw = cli.post(path, json=Request(method=method_name, params=params, id=request_id).to_json()) assert raw.status_code == 200 - resp = Response.from_json(raw.json) + resp = Response.from_json(raw.json, error_cls=exceptions.JsonRpcError) mock.assert_called_once_with(args=params) assert resp.id == request_id assert resp.is_error is True - assert resp.error == exc.JsonRpcError(code=1, message='message') + assert resp.error == exceptions.JsonRpcError(code=1, message='message') # content type error raw = cli.post(path, data='') @@ -119,10 +119,10 @@ def error_method(*args, **kwargs): # malformed json raw = cli.post(path, headers={'Content-Type': 'application/json'}, data='') assert raw.status_code == 200 - resp = Response.from_json(raw.json) + resp = Response.from_json(raw.json, error_cls=exceptions.JsonRpcError) assert resp.id is None assert resp.is_error is True - assert resp.error == exc.ParseError(data=_) + assert resp.error == exceptions.ParseError(data=_) async def test_http_status(app, path): diff --git a/tests/server/test_server_response.py b/tests/server/test_server_response.py new file mode 100644 index 0000000..f3864d2 --- /dev/null +++ b/tests/server/test_server_response.py @@ -0,0 +1,62 @@ +from pjrpc.common import BatchResponse, Response +from pjrpc.server import exceptions + + +def test_response_error_serialization(): + response = Response(error=exceptions.MethodNotFoundError()) + actual_dict = response.to_json() + expected_dict = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + + assert actual_dict == expected_dict + + +def test_batch_response_error_serialization(): + response = BatchResponse(error=exceptions.MethodNotFoundError()) + actual_dict = response.to_json() + expected_dict = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + + assert actual_dict == expected_dict + + +def test_response_error_deserialization(): + data = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + response = Response.from_json(data, error_cls=exceptions.JsonRpcError) + + assert response.is_error + assert response.error == exceptions.MethodNotFoundError() + + +def test_batch_response_error_deserialization(): + data = { + 'jsonrpc': '2.0', + 'id': None, + 'error': { + 'code': -32601, + 'message': 'Method not found', + }, + } + response = BatchResponse.from_json(data, error_cls=exceptions.JsonRpcError) + + assert response.is_error + assert response.error == exceptions.MethodNotFoundError() diff --git a/tests/server/test_werkzeug.py b/tests/server/test_werkzeug.py index 1be9e39..dd16ed9 100644 --- a/tests/server/test_werkzeug.py +++ b/tests/server/test_werkzeug.py @@ -3,8 +3,8 @@ import pytest import werkzeug -from pjrpc import exc from pjrpc.common import Request, Response +from pjrpc.server import exceptions from pjrpc.server.integration import werkzeug as integration from tests.common import _ @@ -50,7 +50,7 @@ def test_request(json_rpc, path, mocker, request_id, params, result): body, code = (test_response.data, test_response.status) assert code == '200 OK' - resp = Response.from_json(json.loads(body)) + resp = Response.from_json(json.loads(body), error_cls=exceptions.JsonRpcError) if isinstance(params, dict): mock.assert_called_once_with(kwargs=params) @@ -87,7 +87,7 @@ def test_errors(json_rpc, path, mocker): method_name = 'test_method' def error_method(*args, **kwargs): - raise exc.JsonRpcError(code=1, message='message') + raise exceptions.JsonRpcError(code=1, message='message') mock = mocker.Mock(name=method_name, side_effect=error_method) @@ -105,10 +105,10 @@ def error_method(*args, **kwargs): body, code = (test_response.data, test_response.status) assert code == '200 OK' - resp = Response.from_json(json.loads(body)) + resp = Response.from_json(json.loads(body), error_cls=exceptions.JsonRpcError) assert resp.id is request_id assert resp.is_error is True - assert resp.error == exc.MethodNotFoundError(data="method 'unknown_method' not found") + assert resp.error == exceptions.MethodNotFoundError(data="method 'unknown_method' not found") # customer error test_response = cli.post( @@ -121,11 +121,11 @@ def error_method(*args, **kwargs): body, code = (test_response.data, test_response.status) assert code == '200 OK' - resp = Response.from_json(json.loads(body)) + resp = Response.from_json(json.loads(body), error_cls=exceptions.JsonRpcError) mock.assert_called_once_with(args=params) assert resp.id == request_id assert resp.is_error is True - assert resp.error == exc.JsonRpcError(code=1, message='message') + assert resp.error == exceptions.JsonRpcError(code=1, message='message') # malformed json test_response = cli.post( @@ -137,7 +137,7 @@ def error_method(*args, **kwargs): else: body, code = (test_response.data, test_response.status) assert code == '200 OK' - resp = Response.from_json(json.loads(body)) + resp = Response.from_json(json.loads(body), error_cls=exceptions.JsonRpcError) assert resp.id is None assert resp.is_error is True - assert resp.error == exc.ParseError(data=_) + assert resp.error == exceptions.ParseError(data=_)