pax_global_header00006660000000000000000000000064145356406020014517gustar00rootroot0000000000000052 comment=45b9790936a1531d84726e29466c601843a9df0a emlove-jsonrpc-base-45b9790/000077500000000000000000000000001453564060200156575ustar00rootroot00000000000000emlove-jsonrpc-base-45b9790/.coveragerc000066400000000000000000000001371453564060200200010ustar00rootroot00000000000000[run] branch = True source = jsonrpc_base relative_files = True [report] show_missing = True emlove-jsonrpc-base-45b9790/.github/000077500000000000000000000000001453564060200172175ustar00rootroot00000000000000emlove-jsonrpc-base-45b9790/.github/workflows/000077500000000000000000000000001453564060200212545ustar00rootroot00000000000000emlove-jsonrpc-base-45b9790/.github/workflows/main.yml000066400000000000000000000024221453564060200227230ustar00rootroot00000000000000name: tests on: push: pull_request: schedule: - cron: '0 0 1 * *' jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.7, 3.8, 3.9, "3.10"] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements-test.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 jsonrpc_base tests.py --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 jsonrpc_base tests.py - name: Test with pytest run: | pytest --cov-report term-missing --cov=jsonrpc_base tests.py - name: Coveralls uses: AndreMiras/coveralls-python-action@develop with: parallel: true coveralls_finish: needs: test runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: AndreMiras/coveralls-python-action@develop with: parallel-finished: true emlove-jsonrpc-base-45b9790/.gitignore000066400000000000000000000014411453564060200176470ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class .mypy_cache # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # Swap files *.swp # Core dumps core.* # Frame dumps frame* emlove-jsonrpc-base-45b9790/.mailmap000066400000000000000000000004641453564060200173040ustar00rootroot00000000000000# Git .mailmap file. This file changes the display of names in git log, and # other locations. This lets us change how old names and emails are displayed # without rewriting git history. # # See https://blog.developer.atlassian.com/aliasing-authors-in-git/ Emily Mills emlove-jsonrpc-base-45b9790/LICENSE.txt000066400000000000000000000026671453564060200175150ustar00rootroot00000000000000Copyright (c) 2014 Giuseppe Ciotta Copyright (c) 2016 Emily Mills All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. emlove-jsonrpc-base-45b9790/MANIFEST.in000066400000000000000000000000241453564060200174110ustar00rootroot00000000000000include *.txt *.rst emlove-jsonrpc-base-45b9790/README.rst000066400000000000000000000055361453564060200173570ustar00rootroot00000000000000jsonrpc-base: a compact JSON-RPC client library interface supporting multiple backends ======================================================================================================= .. image:: https://github.com/emlove/jsonrpc-base/actions/workflows/main.yml/badge.svg :target: https://github.com/emlove/jsonrpc-base/actions/workflows/main.yml .. image:: https://coveralls.io/repos/emlove/jsonrpc-base/badge.svg :target: https://coveralls.io/r/emlove/jsonrpc-base This is a compact and simple JSON-RPC client implementation interface python code. This code is forked from https://github.com/gciotta/jsonrpc-requests Main Features ------------- * Python 3.6, 3.7, 3.8 & 3.9 compatible * Supports nested namespaces (eg. `app.users.getUsers()`) * 100% test coverage Usage ----- See `jsonrpc-async `_ and `jsonrpc-websocket `_ for example implementations. Tests ----- Install the Python tox package and run ``tox``, it'll test this package with various versions of Python. Changelog --------- 2.2.0 (2023-12-11) ~~~~~~~~~~~~~~~~~~ - Omit params attribute when empty `(#9) `_ `@Makman2 `_ 2.1.1 (2022-05-03) ~~~~~~~~~~~~~~~~~~ - Unpin test dependencies 2.1.0 (2021-05-03) ~~~~~~~~~~~~~~~~~~ - Use uuid4 for request IDs 2.0.0 (2021-03-16) ~~~~~~~~~~~~~~~~~~ - BREAKING CHANGE: `Allow single mapping as a positional parameter. `_ Previously, when calling with a single dict as a parameter (example: ``server.foo({'bar': 0})``), the mapping was used as the JSON-RPC keyword parameters. This made it impossible to send a mapping as the first and only positional parameter. If you depended on the old behavior, you can recreate it by spreading the mapping as your method's kwargs. (example: ``server.foo(**{'bar': 0})``) 1.1.0 (2020-08-24) ~~~~~~~~~~~~~~~~~~ - Support for async server request handlers 1.0.3 (2019-11-12) ~~~~~~~~~~~~~~~~~~ - Forwards compatibility for Python 3.9. `(#4) `_ `@a1fred `_ 1.0.2 (2018-08-23) ~~~~~~~~~~~~~~~~~~ - Improved support for JSON-RPC v1 servers. `(#2) `_ `@tdivis `_ 1.0.1 (2018-07-06) ~~~~~~~~~~~~~~~~~~ - Falsey values are no longer treated as None for message IDs, or request parameters. Credits ------- `@gciotta `_ for creating the base project `jsonrpc-requests `_. `@mbroadst `_ for providing full support for nested method calls, JSON-RPC RFC compliance and other improvements. `@vaab `_ for providing api and tests improvements, better RFC compliance. emlove-jsonrpc-base-45b9790/jsonrpc_base/000077500000000000000000000000001453564060200203275ustar00rootroot00000000000000emlove-jsonrpc-base-45b9790/jsonrpc_base/__init__.py000066400000000000000000000002051453564060200224350ustar00rootroot00000000000000from .jsonrpc import ( # noqa: F401, F403 Server, Message, Request, Response, JSONRPCError, ProtocolError, TransportError) emlove-jsonrpc-base-45b9790/jsonrpc_base/jsonrpc.py000066400000000000000000000231331453564060200223610ustar00rootroot00000000000000import inspect import json import logging import uuid _LOGGER = logging.getLogger(__name__) class JSONRPCError(Exception): """Root exception for all errors related to this library""" class TransportError(JSONRPCError): """An error occurred while performing a connection to the server""" def __init__(self, exception_text, message=None, *args): """Create the transport error for the attempted message.""" if message: super(TransportError, self).__init__( '%s: %s' % (message.transport_error_text, exception_text), *args) else: super(TransportError, self).__init__(exception_text, *args) class ProtocolError(JSONRPCError): """An error occurred while dealing with the JSON-RPC protocol""" class Server(object): """A connection to a JSON-RPC server""" def __init__(self): """Initialize the json-rpc server object.""" self._server_request_handlers = {} def send_message(self, message): """Issue the request to the server and return the method result. This method must be implemented by the child class. """ raise NotImplementedError() def receive_request(self, request): """Called when a request is received from the server.""" result = None error = None args, kwargs = request.get_args() if request.method in self._server_request_handlers: try: handler = self._server_request_handlers[request.method] if inspect.iscoroutinefunction(handler): raise TypeError( "Async handlers are not supported in " "synchronous sever implementations") else: result = handler(*args, **kwargs) except Exception as exc: _LOGGER.error(exc, exc_info=exc) error = { 'code': -32000, 'message': 'Server Error: %s' % exc, } else: error = { 'code': -32601, 'message': 'Method not found', } if request.msg_id is not None: return Response(request, result, error) else: return None async def async_receive_request(self, request): """Called when a request is received from the server. If the implementation calls async_receive_request instead of receive_request, asynchronous request handlers are also supported """ result = None error = None args, kwargs = request.get_args() if request.method in self._server_request_handlers: try: handler = self._server_request_handlers[request.method] if inspect.iscoroutinefunction(handler): result = await handler(*args, **kwargs) else: result = handler(*args, **kwargs) except Exception as exc: _LOGGER.error(exc, exc_info=exc) error = { 'code': -32000, 'message': 'Server Error: %s' % exc, } else: error = { 'code': -32601, 'message': 'Method not found', } if request.msg_id is not None: return Response(request, result, error) else: return None def __getattr__(self, method_name): if method_name.startswith("_"): # prevent calls for private methods raise AttributeError("invalid attribute '%s'" % method_name) return Method(self.__request, self.__register, method_name) def __setattr__(self, method_name, callback): if method_name.startswith("_"): # prevent calls for private methods return super(Server, self).__setattr__(method_name, callback) return self.__register(method_name, callback) def __request(self, method_name, args=None, kwargs=None): """Perform the actual RPC call. If _notification=True, don't wait for a response """ if kwargs.pop('_notification', False): msg_id = None else: msg_id = str(uuid.uuid4()) if args and kwargs: raise ProtocolError( 'JSON-RPC spec forbids mixing arguments and keyword arguments') return self.send_message(Request(method_name, args or kwargs, msg_id)) def __register(self, method_name, callback): """Register a callback for if the server sends this request.""" self._server_request_handlers[method_name] = callback class Message(object): """Message to be sent to the jsonrpc server.""" @property def response_id(self): return None def serialize(self): """Generate the raw JSON message to be sent to the server""" raise NotImplementedError() def parse_response(self, response): """Parse the response from the server and return the result.""" raise NotImplementedError() @property def transport_error_text(self): """Exception text for a transport error.""" raise NotImplementedError() def __str__(self): return self.serialize() class Request(Message): """Request a method call on the server.""" def __init__(self, method=None, params=None, msg_id=None): self.method = method self.params = params self.msg_id = msg_id @staticmethod def parse(data): """Generate a request object by parsing the json data.""" if 'method' not in data: raise ProtocolError('Request from server does not contain method') method = data.get('method') params = data.get('params') msg_id = data.get('id') if ( not isinstance(params, list) and not isinstance(params, dict) and params is not None): raise ProtocolError( 'Parameters must either be a positional list or named dict.') return Request(method, params, msg_id) @property def response_id(self): return self.msg_id def serialize(self): """Generate the raw JSON message to be sent to the server""" data = {'jsonrpc': '2.0', 'method': self.method} if self.params: data['params'] = self.params if self.msg_id is not None: data['id'] = self.msg_id return json.dumps(data) def parse_response(self, data): """Parse the response from the server and return the result.""" if self.msg_id is None: # Don't parse results for notification requests return None if not isinstance(data, dict): raise ProtocolError('Response is not a dictionary') if data.get('error') is not None: code = data['error'].get('code', '') message = data['error'].get('message', '') raise ProtocolError(code, message, data) elif 'result' not in data: raise ProtocolError('Response without a result field') else: return data['result'] @property def transport_error_text(self): """Exception text for a transport error.""" return 'Error calling method %r' % self.method def get_args(self): """Transform the request parameters into args/kwargs""" args = [] kwargs = {} if isinstance(self.params, list): args = self.params elif isinstance(self.params, dict): kwargs = self.params elif self.params is not None: raise ProtocolError( 'Parameters must either be a positional list or named dict.') return args, kwargs class Response(Message): """Respond to a method call from the server.""" def __init__(self, request, result=None, error=None): self.request = request self.result = result self.error = error def serialize(self): """Generate the raw JSON message to be sent to the server""" data = {'jsonrpc': '2.0', 'id': self.request.msg_id} if self.error is not None: data['error'] = self.error else: data['result'] = self.result return json.dumps(data) def parse_response(self, response): """Parse the response from the server and return the result.""" # Don't parse results from response messages return None @property def transport_error_text(self): """Exception text for a transport error.""" return 'Error responding to server method %r' % self.request.method class Method(object): """Map the methods called on the server to json-rpc methods.""" def __init__(self, request_method, register_method, method_name): self.__request_method = request_method self.__register_method = register_method self.__method_name = method_name def __getattr__(self, method_name): if method_name.startswith("_"): # prevent calls for private methods raise AttributeError("invalid attribute '%s'" % method_name) return Method(self.__request_method, self.__register_method, "%s.%s" % (self.__method_name, method_name)) def __call__(self, *args, **kwargs): return self.__request_method(self.__method_name, args, kwargs) def __setattr__(self, method_name, callback): if method_name.startswith("_"): # prevent calls for private methods return super(Method, self).__setattr__(method_name, callback) return self.__register_method( "%s.%s" % (self.__method_name, method_name), callback) emlove-jsonrpc-base-45b9790/requirements-test.txt000066400000000000000000000001611453564060200221160ustar00rootroot00000000000000flake8>=3.7.8 pep8>=1.7.1 coverage>=5.5 coveralls>=3.0.1 pytest>=6.2.2 pytest-cov>=2.11.1 pytest-asyncio>=0.14.0 emlove-jsonrpc-base-45b9790/setup.py000066400000000000000000000020771453564060200173770ustar00rootroot00000000000000from __future__ import print_function try: from setuptools import setup except ImportError: import sys print("Please install the `setuptools` package in order to install this library", file=sys.stderr) raise setup( name='jsonrpc-base', version='2.2.0', author='Emily Love Mills', author_email='emily@emlove.me', packages=('jsonrpc_base',), license='BSD', keywords='json-rpc base', url='http://github.com/emlove/jsonrpc-base', description='''A JSON-RPC client library base interface''', long_description=open('README.rst').read(), install_requires=[], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', ], ) emlove-jsonrpc-base-45b9790/tests.py000066400000000000000000000345701453564060200174040ustar00rootroot00000000000000import json import pytest from unittest import mock import jsonrpc_base from jsonrpc_base import Server, ProtocolError, TransportError pytestmark = pytest.mark.asyncio class MockTransportError(ValueError): """Test exception representing a transport library error.""" class MockServer(Server): def __init__(self, url): super(MockServer, self).__init__() self._url = url self._handler = None def send_message(self, message): """Issue the request to the server and return the method result""" try: if isinstance(message, jsonrpc_base.Request): data = jsonrpc_base.Request.parse( json.loads(message.serialize())) else: data = message.serialize() response = json.loads(json.dumps(self._handler(data))) except Exception as requests_exception: raise TransportError( 'Transport Error', message, requests_exception) return message.parse_response(response) def assertSameJSON(json1, json2): """Tells whether two json strings, once decoded, are the same dictionary""" assert json.loads(json1) == json.loads(json2) @pytest.fixture def server(): """Get the mock server object""" return MockServer('http://mock/xmlrpc') def test_dumps(server): # test no args assertSameJSON( '''{"jsonrpc": "2.0", "method": "my_method_name", "id": 1}''', jsonrpc_base.Request( 'my_method_name', params=None, msg_id=1).serialize() ) # test zero message ID assertSameJSON( '''{"jsonrpc": "2.0", "method": "my_method_name", "id": 0}''', jsonrpc_base.Request( 'my_method_name', params=None, msg_id=0).serialize() ) # test empty args dict assertSameJSON( '''{"jsonrpc": "2.0", "method": "my_method_name", "id": 1}''', jsonrpc_base.Request( 'my_method_name', params={}, msg_id=1).serialize() ) # test empty args array assertSameJSON( '''{"jsonrpc": "2.0", "method": "my_method_name", "id": 1}''', jsonrpc_base.Request( 'my_method_name', params=[], msg_id=1).serialize() ) # test keyword args assertSameJSON( '''{"params": {"foo": "bar"}, "jsonrpc": "2.0", "method": "my_method_name", "id": 1}''', jsonrpc_base.Request( 'my_method_name', params={'foo': 'bar'}, msg_id=1).serialize() ) # test positional args assertSameJSON( '''{"params": ["foo", "bar"], "jsonrpc": "2.0", "method": "my_method_name", "id": 1}''', jsonrpc_base.Request( 'my_method_name', params=('foo', 'bar'), msg_id=1).serialize() ) # test notification assertSameJSON( '''{"params": ["foo", "bar"], "jsonrpc": "2.0", "method": "my_method_name"}''', jsonrpc_base.Request( 'my_method_name', params=('foo', 'bar'), msg_id=None).serialize() ) def test_parse_result(server): request = jsonrpc_base.Request('my_message', msg_id=1) with pytest.raises(ProtocolError, match='Response is not a dictionary'): request.parse_response([]) with pytest.raises(ProtocolError, match='Response without a result field'): request.parse_response({}) with pytest.raises(ProtocolError) as protoerror: body = { "jsonrpc": "2.0", "error": { "code": -32601, "message": "Method not found" }, "id": "1" } request.parse_response(body) assert protoerror.value.args[0] == -32601 assert protoerror.value.args[1] == 'Method not found' def test_send_message(server): empty_server = Server() with pytest.raises(NotImplementedError): empty_server.send_message(jsonrpc_base.Request('my_method', msg_id=1)) # catch non-json responses with pytest.raises(TransportError) as transport_error: def handler(message): raise MockTransportError("Transport Error") server._handler = handler server.send_message(jsonrpc_base.Request('my_method', msg_id=1)) assert transport_error.value.args[0] == ( "Error calling method 'my_method': Transport Error") assert isinstance(transport_error.value.args[1], MockTransportError) # a notification def handler(message): return 'we dont care about this' server._handler = handler server.send_message(jsonrpc_base.Request('my_notification', msg_id=None)) def test_exception_passthrough(server): with pytest.raises(TransportError) as transport_error: def handler(message): raise MockTransportError("Transport Error") server._handler = handler server.foo() assert transport_error.value.args[0] == ( "Error calling method 'foo': Transport Error") assert isinstance(transport_error.value.args[1], MockTransportError) def test_transport_error_constructor(server): with pytest.raises(TransportError, match='Test Message'): raise TransportError('Test Message') def test_forbid_private_methods(server): """Test that we can't call private class methods""" with pytest.raises(AttributeError): server._foo() # nested private method call with pytest.raises(AttributeError): server.foo.bar._baz() def test_method_call(server): """mixing *args and **kwargs is forbidden by the spec""" with pytest.raises( ProtocolError, match='JSON-RPC spec forbids mixing arguments and keyword ' 'arguments'): server.testmethod(1, 2, a=1, b=2) def test_method_nesting(server): """Test that we correctly nest namespaces""" def handler(message): return { "jsonrpc": "2.0", "result": True if message.params[0] == message.method else False, "id": 1, } server._handler = handler assert server.nest.testmethod("nest.testmethod") assert server.nest.testmethod.some.other.method( "nest.testmethod.some.other.method") def test_calls(server): # rpc call with positional parameters: def handler1(message): assert message.msg_id == "abcd-1234" assert message.params == [42, 23] return { "jsonrpc": "2.0", "result": 19, "id": 1, } server._handler = handler1 with mock.patch("uuid.uuid4", return_value="abcd-1234"): assert server.subtract(42, 23) == 19 # rpc call with named parameters def handler2(message): assert message.params == {'y': 23, 'x': 42} return { "jsonrpc": "2.0", "result": 19, "id": 1, } server._handler = handler2 assert server.subtract(x=42, y=23) == 19 # rpc call with a mapping type def handler3(message): assert message.params == [{'foo': 'bar'}] return { "jsonrpc": "2.0", "result": None, "id": 1, } server._handler = handler3 server.foobar({'foo': 'bar'}) # rpc call with direct dict params def handler3(message): assert message.params == {'foo': 'bar'} return { "jsonrpc": "2.0", "result": None, "id": 1, } server._handler = handler3 server.foobar(**{'foo': 'bar'}) def test_notification(server): # Verify that we ignore the server response def handler(message): return { "jsonrpc": "2.0", "result": 19, "id": 3, } server._handler = handler assert server.subtract(42, 23, _notification=True) is None def test_receive_server_requests(server): def event_handler(*args, **kwargs): return args, kwargs server.on_server_event = event_handler server.namespace.on_server_event = event_handler response = server.receive_request(jsonrpc_base.Request( 'on_server_event', msg_id=1)) args, kwargs = response.result assert len(args) == 0 assert len(kwargs) == 0 # Test with a zero message ID response = server.receive_request(jsonrpc_base.Request( 'on_server_event', msg_id=0)) args, kwargs = response.result assert len(args) == 0 assert len(kwargs) == 0 response = server.receive_request(jsonrpc_base.Request( 'namespace.on_server_event', msg_id=1)) args, kwargs = response.result assert len(args) == 0 assert len(kwargs) == 0 response = server.receive_request(jsonrpc_base.Request( 'on_server_event', params=['foo', 'bar'], msg_id=1)) args, kwargs = response.result assert args == ('foo', 'bar') assert len(kwargs) == 0 response = server.receive_request(jsonrpc_base.Request( 'on_server_event', params={'foo': 'bar'}, msg_id=1)) args, kwargs = response.result assert len(args) == 0 assert kwargs == {'foo': 'bar'} with pytest.raises(ProtocolError): response = server.receive_request(jsonrpc_base.Request( 'on_server_event', params="string_params", msg_id=1)) response = server.receive_request(jsonrpc_base.Request( 'missing_event', params={'foo': 'bar'}, msg_id=1)) assert response.error['code'] == -32601 assert response.error['message'] == 'Method not found' response = server.receive_request(jsonrpc_base.Request( 'on_server_event')) assert response is None def bad_handler(): raise Exception("Bad Server Handler") server.on_bad_handler = bad_handler # receive_request will normally print traceback when an exception is caught # This isn't necessary for the test response = server.receive_request(jsonrpc_base.Request( 'on_bad_handler', msg_id=1)) assert response.error['code'] == -32000 assert response.error['message'] == 'Server Error: Bad Server Handler' async def async_event_handler(*args, **kwargs): return args, kwargs server.on_async_server_event = async_event_handler response = server.receive_request(jsonrpc_base.Request( 'on_async_server_event', msg_id=1)) assert response.error['code'] == -32000 assert response.error['message'] == ( 'Server Error: Async handlers are not' ' supported in synchronous sever implementations') async def test_async_receive_server_requests(server): def event_handler(*args, **kwargs): return args, kwargs server.on_server_event = event_handler server.namespace.on_server_event = event_handler response = await server.async_receive_request(jsonrpc_base.Request( 'on_server_event', msg_id=1)) args, kwargs = response.result assert len(args) == 0 assert len(kwargs) == 0 async def async_event_handler(*args, **kwargs): return args, kwargs server.on_async_server_event = async_event_handler response = await server.async_receive_request(jsonrpc_base.Request( 'on_async_server_event', msg_id=1)) args, kwargs = response.result assert len(args) == 0 assert len(kwargs) == 0 response = await server.async_receive_request(jsonrpc_base.Request( 'missing_event', params={'foo': 'bar'}, msg_id=1)) assert response.error['code'] == -32601 assert response.error['message'] == 'Method not found' response = await server.async_receive_request(jsonrpc_base.Request( 'on_server_event')) assert response is None async def test_server_responses(server): def handler(message): handler.response = message server._handler = handler def subtract(foo, bar): return foo - bar server.subtract = subtract response = server.receive_request(jsonrpc_base.Request( 'subtract', params={'foo': 5, 'bar': 3}, msg_id=1)) server.send_message(response) assertSameJSON( '''{"jsonrpc": "2.0", "result": 2, "id": 1}''', handler.response ) response = server.receive_request(jsonrpc_base.Request( 'subtract', params=[11, 7], msg_id=1)) server.send_message(response) assertSameJSON( '''{"jsonrpc": "2.0", "result": 4, "id": 1}''', handler.response ) response = server.receive_request(jsonrpc_base.Request( 'missing_method', msg_id=1)) server.send_message(response) assertSameJSON( '''{"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method''' ''' not found"}, "id": 1}''', handler.response ) def bad_handler(self): raise MockTransportError("Transport Error") server._handler = bad_handler def good_method(): return True server.good_method = good_method response = server.receive_request(jsonrpc_base.Request( 'good_method', msg_id=1)) with pytest.raises( TransportError, match="Error responding to server method 'good_method': " "Transport Error"): server.send_message(response) async def async_bad_method(): raise ValueError("Mock server error") server.async_bad_method = async_bad_method response = await server.async_receive_request(jsonrpc_base.Request( 'async_bad_method', msg_id=1)) with pytest.raises( TransportError, match="Error responding to server method 'async_bad_method': " "Transport Error"): server.send_message(response) def test_base_message(server): message = jsonrpc_base.Message() assert message.response_id is None with pytest.raises(NotImplementedError): message.serialize() with pytest.raises(NotImplementedError): message.parse_response(None) with pytest.raises(NotImplementedError): message.transport_error_text with pytest.raises(NotImplementedError): str(message) def test_request(server): with pytest.raises( ProtocolError, match='Request from server does not contain method'): jsonrpc_base.Request.parse({}) with pytest.raises( ProtocolError, match='Parameters must either be a positional list or named dict.' ): jsonrpc_base.Request.parse( {'method': 'test_method', 'params': 'string_params'}) request = jsonrpc_base.Request('test_method', msg_id=1) assert request.response_id == 1 def test_jsonrpc_1_0_call(server): # JSON-RPC 1.0 spec needs "error" to be present (with `null` value) when no # error occured def handler(message): assert message.params == [42, 23] return { "result": 19, "id": 1, "error": None, } server._handler = handler assert server.subtract(42, 23) == 19 emlove-jsonrpc-base-45b9790/tox.ini000066400000000000000000000011421453564060200171700ustar00rootroot00000000000000[tox] envlist = flake8, py37, py38, py39, py310, [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/jsonrpc_base commands = pytest --cov-report term-missing --cov=jsonrpc_base tests.py deps = -r{toxinidir}/requirements-test.txt [testenv:py37] basepython = python3.7 deps = {[testenv]deps} [testenv:py38] basepython = python3.8 deps = {[testenv]deps} [testenv:py39] basepython = python3.9 deps = {[testenv]deps} [testenv:py310] basepython = python3.10 deps = {[testenv]deps} [testenv:flake8] basepython = python commands = flake8 jsonrpc_base tests.py