Skip to content

Add an async manager #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
build/
dist/
docs/_build/
.idea
1 change: 1 addition & 0 deletions jsonrpc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .manager import JSONRPCResponseManager
from .managerasync import JSONRPCResponseManagerAsync
from .dispatcher import Dispatcher

__version = (1, 15, 0)
Expand Down
144 changes: 144 additions & 0 deletions jsonrpc/managerasync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
import logging
from .utils import is_invalid_params
from .exceptions import (
JSONRPCInvalidParams,
JSONRPCInvalidRequest,
JSONRPCInvalidRequestException,
JSONRPCMethodNotFound,
JSONRPCParseError,
JSONRPCServerError,
JSONRPCDispatchException,
)
from .jsonrpc1 import JSONRPC10Response
from .jsonrpc2 import (
JSONRPC20BatchRequest,
JSONRPC20BatchResponse,
JSONRPC20Response,
)
from .jsonrpc import JSONRPCRequest

logger = logging.getLogger(__name__)


class JSONRPCResponseManagerAsync(object):
""" JSON-RPC response manager.

Method brings syntactic sugar into library. Given dispatcher it handles
request (both single and batch) and handles errors.
Request could be handled in parallel, it is server responsibility.

TODO refactor later, this is copy paste of manager.py with async added to methods

:param str request_str: json string. Will be converted into
JSONRPC20Request, JSONRPC20BatchRequest or JSONRPC10Request

:param dict dispatcher: dict<function_name:function>.

"""

RESPONSE_CLASS_MAP = {
"1.0": JSONRPC10Response,
"2.0": JSONRPC20Response,
}

@classmethod
async def handle(cls, request_str, dispatcher, context=None):
if isinstance(request_str, bytes):
request_str = request_str.decode("utf-8")

try:
data = json.loads(request_str)
except (TypeError, ValueError):
return JSONRPC20Response(error=JSONRPCParseError()._data)

try:
request = JSONRPCRequest.from_data(data)
except JSONRPCInvalidRequestException:
return JSONRPC20Response(error=JSONRPCInvalidRequest()._data)

return await cls.handle_request(request, dispatcher, context)

@classmethod
async def handle_request(cls, request, dispatcher, context=None):
""" Handle request data.

At this moment request has correct jsonrpc format.

:param dict request: data parsed from request_str.
:param jsonrpc.dispatcher.Dispatcher dispatcher:

.. versionadded: 1.8.0

"""
rs = request if isinstance(request, JSONRPC20BatchRequest) \
else [request]
responses = [r async for r in cls._get_responses(rs, dispatcher, context)
if r is not None]

# notifications
if not responses:
return

if isinstance(request, JSONRPC20BatchRequest):
response = JSONRPC20BatchResponse(*responses)
response.request = request
return response
else:
return responses[0]

@classmethod
async def _get_responses(cls, requests, dispatcher, context=None):
""" Response to each single JSON-RPC Request.

:return iterator(JSONRPC20Response):

.. versionadded: 1.9.0
TypeError inside the function is distinguished from Invalid Params.

"""
for request in requests:
def make_response(**kwargs):
response = cls.RESPONSE_CLASS_MAP[request.JSONRPC_VERSION](
_id=request._id, **kwargs)
response.request = request
return response

output = None
try:
method = dispatcher[request.method]
except KeyError:
output = make_response(error=JSONRPCMethodNotFound()._data)
else:
try:
kwargs = request.kwargs
if context is not None:
context_arg = dispatcher.context_arg_for_method.get(
request.method)
if context_arg:
context["request"] = request
kwargs[context_arg] = context
result = await method(*request.args, **kwargs)
except JSONRPCDispatchException as e:
output = make_response(error=e.error._data)
except Exception as e:
data = {
"type": e.__class__.__name__,
"args": e.args,
"message": str(e),
}

logger.exception("API Exception: {0}".format(data))

if isinstance(e, TypeError) and is_invalid_params(
method, *request.args, **request.kwargs):
output = make_response(
error=JSONRPCInvalidParams(data=data)._data)
else:
output = make_response(
error=JSONRPCServerError(data=data)._data)
else:
output = make_response(result=result)
finally:
if not request.is_notification:
yield output
188 changes: 188 additions & 0 deletions jsonrpc/tests/test_managerasync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import sys

from ..dispatcher import Dispatcher
from ..managerasync import JSONRPCResponseManagerAsync
from ..jsonrpc2 import (
JSONRPC20BatchRequest,
JSONRPC20BatchResponse,
JSONRPC20Request,
JSONRPC20Response,
)
from ..jsonrpc1 import JSONRPC10Request, JSONRPC10Response
from ..exceptions import JSONRPCDispatchException

if sys.version_info < (3, 3):
from mock import MagicMock
else:
from unittest.mock import MagicMock

if sys.version_info < (2, 7):
import unittest2 as unittest
else:
import unittest


class TestJSONRPCResponseManagerAsync(unittest.TestCase):
def setUp(self):
def raise_(e):
raise e

self.long_time_method = MagicMock()
self.dispatcher = Dispatcher()
self.dispatcher["add"] = sum
self.dispatcher["multiply"] = lambda a, b: a * b
self.dispatcher["list_len"] = len
self.dispatcher["101_base"] = lambda **kwargs: int("101", **kwargs)
self.dispatcher["error"] = lambda: raise_(
KeyError("error_explanation"))
self.dispatcher["type_error"] = lambda: raise_(
TypeError("TypeError inside method"))
self.dispatcher["long_time_method"] = self.long_time_method
self.dispatcher["dispatch_error"] = lambda x: raise_(
JSONRPCDispatchException(code=4000, message="error",
data={"param": 1}))

@self.dispatcher.add_method(context_arg="context")
def return_json_rpc_id(context):
return context["request"]._id

async def test_dispatch_error(self):
request = JSONRPC20Request("dispatch_error", ["test"], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "error")
self.assertEqual(response.error["code"], 4000)
self.assertEqual(response.error["data"], {"param": 1})

async def test_returned_type_response(self):
request = JSONRPC20Request("add", [[]], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))

async def test_returned_type_butch_response(self):
request = JSONRPC20BatchRequest(
JSONRPC20Request("add", [[]], _id=0))
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20BatchResponse))

async def test_returned_type_response_rpc10(self):
request = JSONRPC10Request("add", [[]], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC10Response))

async def test_parse_error(self):
req = '{"jsonrpc": "2.0", "method": "foobar, "params": "bar", "baz]'
response = await JSONRPCResponseManagerAsync.handle(req, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Parse error")
self.assertEqual(response.error["code"], -32700)

async def test_invalid_request(self):
req = '{"jsonrpc": "2.0", "method": 1, "params": "bar"}'
response = await JSONRPCResponseManagerAsync.handle(req, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid Request")
self.assertEqual(response.error["code"], -32600)

async def test_method_not_found(self):
request = JSONRPC20Request("does_not_exist", [[]], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Method not found")
self.assertEqual(response.error["code"], -32601)

async def test_invalid_params(self):
request = JSONRPC20Request("add", {"a": 0}, _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)
self.assertIn(response.error["data"]["message"], [
'sum() takes no keyword arguments',
"sum() got an unexpected keyword argument 'a'",
'sum() takes at least 1 positional argument (0 given)',
])

async def test_invalid_params_custom_function(self):
request = JSONRPC20Request("multiply", [0], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)

request = JSONRPC20Request("multiply", [0, 1, 2], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)

request = JSONRPC20Request("multiply", {"a": 1}, _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)

request = JSONRPC20Request("multiply", {"a": 1, "b": 2, "c": 3}, _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)

async def test_server_error(self):
request = JSONRPC20Request("error", _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Server error")
self.assertEqual(response.error["code"], -32000)
self.assertEqual(response.error["data"]['type'], "KeyError")
self.assertEqual(
response.error["data"]['args'], ('error_explanation',))
self.assertEqual(
response.error["data"]['message'], "'error_explanation'")

async def test_notification_calls_method(self):
request = JSONRPC20Request("long_time_method", is_notification=True)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertEqual(response, None)
self.long_time_method.assert_called_once_with()

async def test_notification_does_not_return_error_does_not_exist(self):
request = JSONRPC20Request("does_not_exist", is_notification=True)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertEqual(response, None)

async def test_notification_does_not_return_error_invalid_params(self):
request = JSONRPC20Request("add", {"a": 0}, is_notification=True)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertEqual(response, None)

async def test_notification_does_not_return_error(self):
request = JSONRPC20Request("error", is_notification=True)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertEqual(response, None)

async def test_type_error_inside_method(self):
request = JSONRPC20Request("type_error", _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Server error")
self.assertEqual(response.error["code"], -32000)
self.assertEqual(response.error["data"]['type'], "TypeError")
self.assertEqual(
response.error["data"]['args'], ('TypeError inside method',))
self.assertEqual(
response.error["data"]['message'], 'TypeError inside method')

async def test_invalid_params_before_dispatcher_error(self):
request = JSONRPC20Request(
"dispatch_error", ["invalid", "params"], _id=0)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher)
self.assertTrue(isinstance(response, JSONRPC20Response))
self.assertEqual(response.error["message"], "Invalid params")
self.assertEqual(response.error["code"], -32602)

async def test_setting_json_rpc_id_in_context(self):
request = JSONRPC20Request("return_json_rpc_id", _id=42)
response = await JSONRPCResponseManagerAsync.handle(request.json, self.dispatcher,
context={})
self.assertEqual(response.data["result"], 42)