├── .coveragerc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .mailmap ├── LICENSE.txt ├── MANIFEST.in ├── README.rst ├── jsonrpc_async ├── __init__.py └── jsonrpc.py ├── requirements-test.txt ├── setup.py ├── tests.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = 4 | jsonrpc_async 5 | relative_files = True 6 | 7 | [report] 8 | show_missing = True 9 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: [3.7, 3.8, 3.9, "3.10"] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements-test.txt 24 | - name: Lint with flake8 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 jsonrpc_async tests.py --count --select=E9,F63,F7,F82 --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 jsonrpc_async tests.py 30 | - name: Test with pytest 31 | run: | 32 | pytest --cov-report term-missing --cov=jsonrpc_async --asyncio-mode=auto tests.py 33 | - name: Coveralls 34 | uses: AndreMiras/coveralls-python-action@develop 35 | with: 36 | parallel: true 37 | 38 | coveralls_finish: 39 | needs: test 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Coveralls Finished 43 | uses: AndreMiras/coveralls-python-action@develop 44 | with: 45 | parallel-finished: true 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .mypy_cache 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | # Swap files 63 | *.swp 64 | 65 | # Core dumps 66 | core.* 67 | 68 | # Frame dumps 69 | frame* 70 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Git .mailmap file. This file changes the display of names in git log, and 2 | # other locations. This lets us change how old names and emails are displayed 3 | # without rewriting git history. 4 | # 5 | # See https://blog.developer.atlassian.com/aliasing-authors-in-git/ 6 | 7 | Emily Mills 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Giuseppe Ciotta 2 | Copyright (c) 2016-2020 Emily Mills 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions 7 | are met: 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 3. The name of the author may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR 17 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 18 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 19 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 21 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 22 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 25 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | jsonrpc-async: a compact JSON-RPC client library for asyncio 2 | ======================================================================================================= 3 | 4 | .. image:: https://img.shields.io/pypi/v/jsonrpc-async.svg 5 | :target: https://pypi.python.org/pypi/jsonrpc-async 6 | .. image:: https://github.com/emlove/jsonrpc-async/workflows/tests/badge.svg 7 | :target: https://github.com/emlove/jsonrpc-async/actions 8 | .. image:: https://coveralls.io/repos/emlove/jsonrpc-async/badge.svg 9 | :target: https://coveralls.io/r/emlove/jsonrpc-async 10 | 11 | This is a compact and simple JSON-RPC client implementation for asyncio python code. This code is forked from https://github.com/gciotta/jsonrpc-requests 12 | 13 | Main Features 14 | ------------- 15 | 16 | * Supports nested namespaces (eg. `app.users.getUsers()`) 17 | * 100% test coverage 18 | 19 | Usage 20 | ----- 21 | It is recommended to manage the aiohttp ClientSession object externally and pass it to the Server constructor. `(See the aiohttp documentation.) `_ If not passed to Server, a ClientSession object will be created automatically. 22 | 23 | Execute remote JSON-RPC functions 24 | 25 | .. code-block:: python 26 | 27 | import asyncio 28 | from jsonrpc_async import Server 29 | 30 | async def routine(): 31 | async with Server('http://localhost:8080') as server: 32 | await server.foo(1, 2) 33 | await server.foo(bar=1, baz=2) 34 | await server.foo({'foo': 'bar'}) 35 | await server.foo.bar(baz=1, qux=2) 36 | 37 | asyncio.get_event_loop().run_until_complete(routine()) 38 | 39 | A notification 40 | 41 | .. code-block:: python 42 | 43 | import asyncio 44 | from jsonrpc_async import Server 45 | 46 | async def routine(): 47 | async with Server('http://localhost:8080') as server: 48 | await server.foo(bar=1, _notification=True) 49 | 50 | asyncio.get_event_loop().run_until_complete(routine()) 51 | 52 | Pass through arguments to aiohttp (see also `aiohttp documentation `_) 53 | 54 | .. code-block:: python 55 | 56 | import asyncio 57 | import aiohttp 58 | from jsonrpc_async import Server 59 | 60 | async def routine(): 61 | async with Server( 62 | 'http://localhost:8080', 63 | auth=aiohttp.BasicAuth('user', 'pass'), 64 | headers={'x-test2': 'true'} 65 | ) as server: 66 | await server.foo() 67 | 68 | asyncio.get_event_loop().run_until_complete(routine()) 69 | 70 | Pass through aiohttp exceptions 71 | 72 | .. code-block:: python 73 | 74 | import asyncio 75 | import aiohttp 76 | from jsonrpc_async import Server 77 | 78 | async def routine(): 79 | async with Server('http://unknown-host') as server: 80 | try: 81 | await server.foo() 82 | except TransportError as transport_error: 83 | print(transport_error.args[1]) # this will hold a aiohttp exception instance 84 | 85 | asyncio.get_event_loop().run_until_complete(routine()) 86 | 87 | Tests 88 | ----- 89 | Install the Python tox package and run ``tox``, it'll test this package with various versions of Python. 90 | 91 | Changelog 92 | --------- 93 | 2.1.2 (2023-07-10) 94 | ~~~~~~~~~~~~~~~~~~ 95 | - Add support for `async with` `(#10) `_ `@lieryan `_ 96 | 97 | 2.1.1 (2022-05-03) 98 | ~~~~~~~~~~~~~~~~~~ 99 | - Unpin test dependencies 100 | 101 | 2.1.0 (2021-05-03) 102 | ~~~~~~~~~~~~~~~~~~ 103 | - Bumped jsonrpc-base to version 2.1.0 104 | 105 | 2.0.0 (2021-03-16) 106 | ~~~~~~~~~~~~~~~~~~ 107 | - Bumped jsonrpc-base to version 2.0.0 108 | - BREAKING CHANGE: `Allow single mapping as a positional parameter. `_ 109 | 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})``) 110 | 111 | 1.1.1 (2019-11-12) 112 | ~~~~~~~~~~~~~~~~~~ 113 | - Bumped jsonrpc-base to version 1.0.3 114 | 115 | 1.1.0 (2018-09-04) 116 | ~~~~~~~~~~~~~~~~~~ 117 | - Added support for using a custom json.loads method `(#1) `_ `@tdivis `_ 118 | 119 | 1.0.1 (2018-08-23) 120 | ~~~~~~~~~~~~~~~~~~ 121 | - Bumped jsonrpc-base to version 1.0.2 122 | 123 | 1.0.0 (2018-07-06) 124 | ~~~~~~~~~~~~~~~~~~ 125 | - Bumped minimum aiohttp version to 3.0.0 126 | - Bumped jsonrpc-base to version 1.0.1 127 | 128 | Credits 129 | ------- 130 | `@gciotta `_ for creating the base project `jsonrpc-requests `_. 131 | 132 | `@mbroadst `_ for providing full support for nested method calls, JSON-RPC RFC 133 | compliance and other improvements. 134 | 135 | `@vaab `_ for providing api and tests improvements, better RFC compliance. 136 | -------------------------------------------------------------------------------- /jsonrpc_async/__init__.py: -------------------------------------------------------------------------------- 1 | from jsonrpc_base import ( # noqa: F401, F403 2 | JSONRPCError, TransportError, ProtocolError) 3 | from .jsonrpc import Server # noqa: F401, F403 4 | -------------------------------------------------------------------------------- /jsonrpc_async/jsonrpc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | 4 | import aiohttp 5 | import jsonrpc_base 6 | from jsonrpc_base import TransportError 7 | 8 | 9 | class Server(jsonrpc_base.Server): 10 | """A connection to a HTTP JSON-RPC server, backed by aiohttp""" 11 | 12 | def __init__(self, url, session=None, *, loads=None, **post_kwargs): 13 | super().__init__() 14 | object.__setattr__(self, 'session', session or aiohttp.ClientSession()) 15 | post_kwargs['headers'] = post_kwargs.get('headers', {}) 16 | post_kwargs['headers']['Content-Type'] = post_kwargs['headers'].get( 17 | 'Content-Type', 'application/json') 18 | post_kwargs['headers']['Accept'] = post_kwargs['headers'].get( 19 | 'Accept', 'application/json-rpc') 20 | self._request = functools.partial( 21 | self.session.post, url, **post_kwargs) 22 | 23 | self._json_args = {} 24 | if loads is not None: 25 | self._json_args['loads'] = loads 26 | 27 | async def send_message(self, message): 28 | """Send the HTTP message to the server and return the message response. 29 | 30 | No result is returned if message is a notification. 31 | """ 32 | try: 33 | response = await self._request(data=message.serialize()) 34 | except (aiohttp.ClientError, asyncio.TimeoutError) as exc: 35 | raise TransportError('Transport Error', message, exc) 36 | 37 | if response.status != 200: 38 | raise TransportError( 39 | 'HTTP %d %s' % (response.status, response.reason), message) 40 | 41 | if message.response_id is None: 42 | # Message is notification, so no response is expcted. 43 | return None 44 | 45 | try: 46 | response_data = await response.json(**self._json_args) 47 | except ValueError as value_error: 48 | raise TransportError( 49 | 'Cannot deserialize response body', message, value_error) 50 | 51 | return message.parse_response(response_data) 52 | 53 | async def __aenter__(self): 54 | await self.session.__aenter__() 55 | return self 56 | 57 | async def __aexit__(self, exc_type, exc, tb): 58 | return await self.session.__aexit__(exc_type, exc, tb) 59 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.7.8 2 | coverage>=5.5 3 | coveralls>=3.0.1 4 | jsonrpc-base>=2.1.0 5 | aiohttp>=3.0.0 6 | pytest-aiohttp>=1.0.0 7 | pytest>=6.2.2 8 | pytest-cov>=2.11.1 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | import sys 7 | print("Please install the `setuptools` package in order to install this library", file=sys.stderr) 8 | raise 9 | 10 | setup( 11 | name='jsonrpc-async', 12 | version='2.1.2', 13 | author='Emily Mills', 14 | author_email='emily@emlove.me', 15 | packages=('jsonrpc_async',), 16 | license='BSD', 17 | keywords='json-rpc async asyncio', 18 | url='http://github.com/emlove/jsonrpc-async', 19 | description='''A JSON-RPC client library for asyncio''', 20 | long_description=open('README.rst').read(), 21 | install_requires=[ 22 | 'jsonrpc-base>=2.1.0', 23 | 'aiohttp>=3.0.0', 24 | ], 25 | classifiers=[ 26 | 'Development Status :: 5 - Production/Stable', 27 | 'Intended Audience :: Developers', 28 | 'Topic :: Software Development :: Libraries', 29 | 'License :: OSI Approved :: BSD License', 30 | 'Programming Language :: Python :: 3', 31 | 'Programming Language :: Python :: 3.7', 32 | 'Programming Language :: Python :: 3.8', 33 | 'Programming Language :: Python :: 3.9', 34 | 'Programming Language :: Python :: 3.10', 35 | ], 36 | 37 | ) 38 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from unittest import mock 3 | import json 4 | import pytest 5 | 6 | import aiohttp 7 | import aiohttp.web 8 | import aiohttp.test_utils 9 | 10 | import jsonrpc_base 11 | from jsonrpc_async import Server, ProtocolError, TransportError 12 | 13 | 14 | async def test_send_message_timeout(aiohttp_client): 15 | """Test the catching of the timeout responses.""" 16 | 17 | async def handler(request): 18 | try: 19 | await asyncio.sleep(10) 20 | except asyncio.CancelledError: 21 | # Event loop will be terminated before sleep finishes 22 | pass 23 | return aiohttp.web.Response(text='{}', content_type='application/json') 24 | 25 | def create_app(): 26 | app = aiohttp.web.Application() 27 | app.router.add_route('POST', '/', handler) 28 | return app 29 | 30 | client = await aiohttp_client(create_app()) 31 | server = Server('/', client, timeout=0.2) 32 | 33 | with pytest.raises(TransportError) as transport_error: 34 | await server.send_message(jsonrpc_base.Request( 35 | 'my_method', params=None, msg_id=1)) 36 | 37 | assert isinstance(transport_error.value.args[1], asyncio.TimeoutError) 38 | 39 | 40 | async def test_send_message(aiohttp_client): 41 | """Test the sending of messages.""" 42 | # catch non-json responses 43 | async def handler1(request): 44 | return aiohttp.web.Response( 45 | text='not json', content_type='application/json') 46 | 47 | def create_app(): 48 | app = aiohttp.web.Application() 49 | app.router.add_route('POST', '/', handler1) 50 | return app 51 | 52 | client = await aiohttp_client(create_app()) 53 | server = Server('/', client) 54 | 55 | with pytest.raises(TransportError) as transport_error: 56 | await server.send_message( 57 | jsonrpc_base.Request('my_method', params=None, msg_id=1)) 58 | 59 | assert transport_error.value.args[0] == ( 60 | "Error calling method 'my_method': Cannot deserialize response body") 61 | assert isinstance(transport_error.value.args[1], ValueError) 62 | 63 | # catch non-200 responses 64 | async def handler2(request): 65 | return aiohttp.web.Response( 66 | text='{}', content_type='application/json', status=404) 67 | 68 | def create_app(): 69 | app = aiohttp.web.Application() 70 | app.router.add_route('POST', '/', handler2) 71 | return app 72 | 73 | client = await aiohttp_client(create_app()) 74 | server = Server('/', client) 75 | 76 | with pytest.raises(TransportError) as transport_error: 77 | await server.send_message(jsonrpc_base.Request( 78 | 'my_method', params=None, msg_id=1)) 79 | 80 | assert transport_error.value.args[0] == ( 81 | "Error calling method 'my_method': HTTP 404 Not Found") 82 | 83 | # catch aiohttp own exception 84 | async def callback(*args, **kwargs): 85 | raise aiohttp.ClientOSError('aiohttp exception') 86 | 87 | def create_app(): 88 | app = aiohttp.web.Application() 89 | return app 90 | 91 | client = await aiohttp_client(create_app()) 92 | client.post = callback 93 | server = Server('/', client) 94 | 95 | with pytest.raises(TransportError) as transport_error: 96 | await server.send_message(jsonrpc_base.Request( 97 | 'my_method', params=None, msg_id=1)) 98 | 99 | assert transport_error.value.args[0] == ( 100 | "Error calling method 'my_method': Transport Error") 101 | 102 | 103 | async def test_exception_passthrough(aiohttp_client): 104 | async def callback(*args, **kwargs): 105 | raise aiohttp.ClientOSError('aiohttp exception') 106 | 107 | def create_app(): 108 | app = aiohttp.web.Application() 109 | return app 110 | 111 | client = await aiohttp_client(create_app()) 112 | client.post = callback 113 | server = Server('/', client) 114 | 115 | with pytest.raises(TransportError) as transport_error: 116 | await server.foo() 117 | 118 | assert transport_error.value.args[0] == ( 119 | "Error calling method 'foo': Transport Error") 120 | assert isinstance(transport_error.value.args[1], aiohttp.ClientOSError) 121 | 122 | 123 | async def test_forbid_private_methods(aiohttp_client): 124 | """Test that we can't call private methods (those starting with '_').""" 125 | def create_app(): 126 | app = aiohttp.web.Application() 127 | return app 128 | 129 | client = await aiohttp_client(create_app()) 130 | server = Server('/', client) 131 | 132 | with pytest.raises(AttributeError): 133 | await server._foo() 134 | 135 | # nested private method call 136 | with pytest.raises(AttributeError): 137 | await server.foo.bar._baz() 138 | 139 | 140 | async def test_headers_passthrough(aiohttp_client): 141 | """Test that we correctly send RFC headers and merge them with users.""" 142 | async def handler(request): 143 | return aiohttp.web.Response( 144 | text='{"jsonrpc": "2.0", "result": true, "id": 1}', 145 | content_type='application/json') 146 | 147 | def create_app(): 148 | app = aiohttp.web.Application() 149 | app.router.add_route('POST', '/', handler) 150 | return app 151 | 152 | client = await aiohttp_client(create_app()) 153 | 154 | original_post = client.post 155 | 156 | async def callback(*args, **kwargs): 157 | expected_headers = { 158 | 'Content-Type': 'application/json', 159 | 'Accept': 'application/json-rpc', 160 | 'X-TestCustomHeader': '1' 161 | } 162 | assert set(expected_headers.items()).issubset( 163 | set(kwargs['headers'].items())) 164 | return await original_post(*args, **kwargs) 165 | 166 | client.post = callback 167 | 168 | server = Server('/', client, headers={'X-TestCustomHeader': '1'}) 169 | 170 | await server.foo() 171 | 172 | 173 | async def test_method_call(aiohttp_client): 174 | """Mixing *args and **kwargs is forbidden by the spec.""" 175 | def create_app(): 176 | app = aiohttp.web.Application() 177 | return app 178 | 179 | client = await aiohttp_client(create_app()) 180 | server = Server('/', client) 181 | 182 | with pytest.raises(ProtocolError) as error: 183 | await server.testmethod(1, 2, a=1, b=2) 184 | assert error.value.args[0] == ( 185 | "JSON-RPC spec forbids mixing arguments and keyword arguments") 186 | 187 | 188 | async def test_method_nesting(aiohttp_client): 189 | """Test that we correctly nest namespaces.""" 190 | async def handler(request): 191 | request_message = await request.json() 192 | if (request_message["params"][0] == request_message["method"]): 193 | return aiohttp.web.Response( 194 | text='{"jsonrpc": "2.0", "result": true, "id": 1}', 195 | content_type='application/json') 196 | else: 197 | return aiohttp.web.Response( 198 | text='{"jsonrpc": "2.0", "result": false, "id": 1}', 199 | content_type='application/json') 200 | 201 | def create_app(): 202 | app = aiohttp.web.Application() 203 | app.router.add_route('POST', '/', handler) 204 | return app 205 | 206 | client = await aiohttp_client(create_app()) 207 | server = Server('/', client) 208 | 209 | assert await server.nest.testmethod("nest.testmethod") is True 210 | assert await server.nest.testmethod.some.other.method( 211 | "nest.testmethod.some.other.method") is True 212 | 213 | 214 | async def test_calls(aiohttp_client): 215 | """Test RPC call with positional parameters.""" 216 | async def handler1(request): 217 | request_message = await request.json() 218 | assert request_message["params"] == [42, 23] 219 | return aiohttp.web.Response( 220 | text='{"jsonrpc": "2.0", "result": 19, "id": 1}', 221 | content_type='application/json') 222 | 223 | def create_app(): 224 | app = aiohttp.web.Application() 225 | app.router.add_route('POST', '/', handler1) 226 | return app 227 | 228 | client = await aiohttp_client(create_app()) 229 | server = Server('/', client) 230 | 231 | assert await server.subtract(42, 23) == 19 232 | 233 | async def handler2(request): 234 | request_message = await request.json() 235 | assert request_message["params"] == {'y': 23, 'x': 42} 236 | return aiohttp.web.Response( 237 | text='{"jsonrpc": "2.0", "result": 19, "id": 1}', 238 | content_type='application/json') 239 | 240 | def create_app(): 241 | app = aiohttp.web.Application() 242 | app.router.add_route('POST', '/', handler2) 243 | return app 244 | 245 | client = await aiohttp_client(create_app()) 246 | server = Server('/', client) 247 | 248 | assert await server.subtract(x=42, y=23) == 19 249 | 250 | async def handler3(request): 251 | request_message = await request.json() 252 | assert request_message["params"] == [{'foo': 'bar'}] 253 | return aiohttp.web.Response( 254 | text='{"jsonrpc": "2.0", "result": null}', 255 | content_type='application/json') 256 | 257 | def create_app(): 258 | app = aiohttp.web.Application() 259 | app.router.add_route('POST', '/', handler3) 260 | return app 261 | 262 | client = await aiohttp_client(create_app()) 263 | server = Server('/', client) 264 | 265 | await server.foobar({'foo': 'bar'}) 266 | 267 | 268 | async def test_notification(aiohttp_client): 269 | """Verify that we ignore the server response.""" 270 | async def handler(request): 271 | return aiohttp.web.Response( 272 | text='{"jsonrpc": "2.0", "result": 19, "id": 1}', 273 | content_type='application/json') 274 | 275 | def create_app(): 276 | app = aiohttp.web.Application() 277 | app.router.add_route('POST', '/', handler) 278 | return app 279 | 280 | client = await aiohttp_client(create_app()) 281 | server = Server('/', client) 282 | 283 | assert await server.subtract(42, 23, _notification=True) is None 284 | 285 | 286 | async def test_custom_loads(aiohttp_client): 287 | """Test RPC call with custom load.""" 288 | loads_mock = mock.Mock(wraps=json.loads) 289 | 290 | async def handler(request): 291 | request_message = await request.json() 292 | assert request_message["params"] == [42, 23] 293 | return aiohttp.web.Response( 294 | text='{"jsonrpc": "2.0", "result": 19, "id": 1}', 295 | content_type='application/json') 296 | 297 | def create_app(): 298 | app = aiohttp.web.Application() 299 | app.router.add_route('POST', '/', handler) 300 | return app 301 | 302 | client = await aiohttp_client(create_app()) 303 | server = Server('/', client, loads=loads_mock) 304 | 305 | assert await server.subtract(42, 23) == 19 306 | assert loads_mock.call_count == 1 307 | 308 | 309 | async def test_context_manager(aiohttp_client): 310 | # catch non-json responses 311 | async def handler1(request): 312 | return aiohttp.web.Response( 313 | text='not json', content_type='application/json') 314 | 315 | def create_app(): 316 | app = aiohttp.web.Application() 317 | app.router.add_route('POST', '/', handler1) 318 | return app 319 | 320 | client = await aiohttp_client(create_app()) 321 | async with Server('/', client) as server: 322 | assert isinstance(server, Server) 323 | assert not server.session.session.closed 324 | assert server.session.session.closed 325 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py37, 4 | py38, 5 | py39, 6 | py310, 7 | flake8, 8 | 9 | [testenv] 10 | setenv = 11 | PYTHONPATH = {toxinidir}:{toxinidir}/jsonrpc_async 12 | commands = 13 | pytest --cov=jsonrpc_async --asyncio-mode=auto tests.py 14 | coverage report 15 | deps = 16 | -r{toxinidir}/requirements-test.txt 17 | 18 | [testenv:py37] 19 | basepython = python3.7 20 | deps = 21 | {[testenv]deps} 22 | 23 | [testenv:py38] 24 | basepython = python3.8 25 | deps = 26 | {[testenv]deps} 27 | 28 | [testenv:py39] 29 | basepython = python3.9 30 | deps = 31 | {[testenv]deps} 32 | 33 | [testenv:py310] 34 | basepython = python3.10 35 | deps = 36 | {[testenv]deps} 37 | 38 | [testenv:flake8] 39 | basepython = python 40 | commands = flake8 jsonrpc_async tests.py 41 | --------------------------------------------------------------------------------