pax_global_header00006660000000000000000000000064144530253240014514gustar00rootroot0000000000000052 comment=d90b7efb52756edbad6c449d3150c58136fece7e emlove-jsonrpc-async-d90b7ef/000077500000000000000000000000001445302532400162745ustar00rootroot00000000000000emlove-jsonrpc-async-d90b7ef/.coveragerc000066400000000000000000000001401445302532400204100ustar00rootroot00000000000000[run] branch = True source = jsonrpc_async relative_files = True [report] show_missing = True emlove-jsonrpc-async-d90b7ef/.github/000077500000000000000000000000001445302532400176345ustar00rootroot00000000000000emlove-jsonrpc-async-d90b7ef/.github/workflows/000077500000000000000000000000001445302532400216715ustar00rootroot00000000000000emlove-jsonrpc-async-d90b7ef/.github/workflows/main.yml000066400000000000000000000024041445302532400233400ustar00rootroot00000000000000name: tests on: push: pull_request: 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_async 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_async tests.py - name: Test with pytest run: | pytest --cov-report term-missing --cov=jsonrpc_async --asyncio-mode=auto 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-async-d90b7ef/.gitignore000066400000000000000000000014411445302532400202640ustar00rootroot00000000000000# 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-async-d90b7ef/.mailmap000066400000000000000000000004641445302532400177210ustar00rootroot00000000000000# 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-async-d90b7ef/LICENSE.txt000066400000000000000000000026741445302532400201300ustar00rootroot00000000000000Copyright (c) 2014 Giuseppe Ciotta Copyright (c) 2016-2020 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-async-d90b7ef/MANIFEST.in000066400000000000000000000000241445302532400200260ustar00rootroot00000000000000include *.txt *.rst emlove-jsonrpc-async-d90b7ef/README.rst000066400000000000000000000113211445302532400177610ustar00rootroot00000000000000jsonrpc-async: a compact JSON-RPC client library for asyncio ======================================================================================================= .. image:: https://img.shields.io/pypi/v/jsonrpc-async.svg :target: https://pypi.python.org/pypi/jsonrpc-async .. image:: https://github.com/emlove/jsonrpc-async/workflows/tests/badge.svg :target: https://github.com/emlove/jsonrpc-async/actions .. image:: https://coveralls.io/repos/emlove/jsonrpc-async/badge.svg :target: https://coveralls.io/r/emlove/jsonrpc-async 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 Main Features ------------- * Supports nested namespaces (eg. `app.users.getUsers()`) * 100% test coverage Usage ----- 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. Execute remote JSON-RPC functions .. code-block:: python import asyncio from jsonrpc_async import Server async def routine(): async with Server('http://localhost:8080') as server: await server.foo(1, 2) await server.foo(bar=1, baz=2) await server.foo({'foo': 'bar'}) await server.foo.bar(baz=1, qux=2) asyncio.get_event_loop().run_until_complete(routine()) A notification .. code-block:: python import asyncio from jsonrpc_async import Server async def routine(): async with Server('http://localhost:8080') as server: await server.foo(bar=1, _notification=True) asyncio.get_event_loop().run_until_complete(routine()) Pass through arguments to aiohttp (see also `aiohttp documentation `_) .. code-block:: python import asyncio import aiohttp from jsonrpc_async import Server async def routine(): async with Server( 'http://localhost:8080', auth=aiohttp.BasicAuth('user', 'pass'), headers={'x-test2': 'true'} ) as server: await server.foo() asyncio.get_event_loop().run_until_complete(routine()) Pass through aiohttp exceptions .. code-block:: python import asyncio import aiohttp from jsonrpc_async import Server async def routine(): async with Server('http://unknown-host') as server: try: await server.foo() except TransportError as transport_error: print(transport_error.args[1]) # this will hold a aiohttp exception instance asyncio.get_event_loop().run_until_complete(routine()) Tests ----- Install the Python tox package and run ``tox``, it'll test this package with various versions of Python. Changelog --------- 2.1.2 (2023-07-10) ~~~~~~~~~~~~~~~~~~ - Add support for `async with` `(#10) `_ `@lieryan `_ 2.1.1 (2022-05-03) ~~~~~~~~~~~~~~~~~~ - Unpin test dependencies 2.1.0 (2021-05-03) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 2.1.0 2.0.0 (2021-03-16) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 2.0.0 - 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.1 (2019-11-12) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 1.0.3 1.1.0 (2018-09-04) ~~~~~~~~~~~~~~~~~~ - Added support for using a custom json.loads method `(#1) `_ `@tdivis `_ 1.0.1 (2018-08-23) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 1.0.2 1.0.0 (2018-07-06) ~~~~~~~~~~~~~~~~~~ - Bumped minimum aiohttp version to 3.0.0 - Bumped jsonrpc-base to version 1.0.1 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-async-d90b7ef/jsonrpc_async/000077500000000000000000000000001445302532400211475ustar00rootroot00000000000000emlove-jsonrpc-async-d90b7ef/jsonrpc_async/__init__.py000066400000000000000000000002201445302532400232520ustar00rootroot00000000000000from jsonrpc_base import ( # noqa: F401, F403 JSONRPCError, TransportError, ProtocolError) from .jsonrpc import Server # noqa: F401, F403 emlove-jsonrpc-async-d90b7ef/jsonrpc_async/jsonrpc.py000066400000000000000000000040401445302532400231750ustar00rootroot00000000000000import asyncio import functools import aiohttp import jsonrpc_base from jsonrpc_base import TransportError class Server(jsonrpc_base.Server): """A connection to a HTTP JSON-RPC server, backed by aiohttp""" def __init__(self, url, session=None, *, loads=None, **post_kwargs): super().__init__() object.__setattr__(self, 'session', session or aiohttp.ClientSession()) post_kwargs['headers'] = post_kwargs.get('headers', {}) post_kwargs['headers']['Content-Type'] = post_kwargs['headers'].get( 'Content-Type', 'application/json') post_kwargs['headers']['Accept'] = post_kwargs['headers'].get( 'Accept', 'application/json-rpc') self._request = functools.partial( self.session.post, url, **post_kwargs) self._json_args = {} if loads is not None: self._json_args['loads'] = loads async def send_message(self, message): """Send the HTTP message to the server and return the message response. No result is returned if message is a notification. """ try: response = await self._request(data=message.serialize()) except (aiohttp.ClientError, asyncio.TimeoutError) as exc: raise TransportError('Transport Error', message, exc) if response.status != 200: raise TransportError( 'HTTP %d %s' % (response.status, response.reason), message) if message.response_id is None: # Message is notification, so no response is expcted. return None try: response_data = await response.json(**self._json_args) except ValueError as value_error: raise TransportError( 'Cannot deserialize response body', message, value_error) return message.parse_response(response_data) async def __aenter__(self): await self.session.__aenter__() return self async def __aexit__(self, exc_type, exc, tb): return await self.session.__aexit__(exc_type, exc, tb) emlove-jsonrpc-async-d90b7ef/requirements-test.txt000066400000000000000000000002071445302532400225340ustar00rootroot00000000000000flake8>=3.7.8 coverage>=5.5 coveralls>=3.0.1 jsonrpc-base>=2.1.0 aiohttp>=3.0.0 pytest-aiohttp>=1.0.0 pytest>=6.2.2 pytest-cov>=2.11.1 emlove-jsonrpc-async-d90b7ef/setup.py000066400000000000000000000022011445302532400200010ustar00rootroot00000000000000from __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-async', version='2.1.2', author='Emily Mills', author_email='emily@emlove.me', packages=('jsonrpc_async',), license='BSD', keywords='json-rpc async asyncio', url='http://github.com/emlove/jsonrpc-async', description='''A JSON-RPC client library for asyncio''', long_description=open('README.rst').read(), install_requires=[ 'jsonrpc-base>=2.1.0', 'aiohttp>=3.0.0', ], 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-async-d90b7ef/tests.py000066400000000000000000000240651445302532400200170ustar00rootroot00000000000000import asyncio from unittest import mock import json import pytest import aiohttp import aiohttp.web import aiohttp.test_utils import jsonrpc_base from jsonrpc_async import Server, ProtocolError, TransportError async def test_send_message_timeout(aiohttp_client): """Test the catching of the timeout responses.""" async def handler(request): try: await asyncio.sleep(10) except asyncio.CancelledError: # Event loop will be terminated before sleep finishes pass return aiohttp.web.Response(text='{}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler) return app client = await aiohttp_client(create_app()) server = Server('/', client, timeout=0.2) with pytest.raises(TransportError) as transport_error: await server.send_message(jsonrpc_base.Request( 'my_method', params=None, msg_id=1)) assert isinstance(transport_error.value.args[1], asyncio.TimeoutError) async def test_send_message(aiohttp_client): """Test the sending of messages.""" # catch non-json responses async def handler1(request): return aiohttp.web.Response( text='not json', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler1) return app client = await aiohttp_client(create_app()) server = Server('/', client) with pytest.raises(TransportError) as transport_error: await server.send_message( jsonrpc_base.Request('my_method', params=None, msg_id=1)) assert transport_error.value.args[0] == ( "Error calling method 'my_method': Cannot deserialize response body") assert isinstance(transport_error.value.args[1], ValueError) # catch non-200 responses async def handler2(request): return aiohttp.web.Response( text='{}', content_type='application/json', status=404) def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler2) return app client = await aiohttp_client(create_app()) server = Server('/', client) with pytest.raises(TransportError) as transport_error: await server.send_message(jsonrpc_base.Request( 'my_method', params=None, msg_id=1)) assert transport_error.value.args[0] == ( "Error calling method 'my_method': HTTP 404 Not Found") # catch aiohttp own exception async def callback(*args, **kwargs): raise aiohttp.ClientOSError('aiohttp exception') def create_app(): app = aiohttp.web.Application() return app client = await aiohttp_client(create_app()) client.post = callback server = Server('/', client) with pytest.raises(TransportError) as transport_error: await server.send_message(jsonrpc_base.Request( 'my_method', params=None, msg_id=1)) assert transport_error.value.args[0] == ( "Error calling method 'my_method': Transport Error") async def test_exception_passthrough(aiohttp_client): async def callback(*args, **kwargs): raise aiohttp.ClientOSError('aiohttp exception') def create_app(): app = aiohttp.web.Application() return app client = await aiohttp_client(create_app()) client.post = callback server = Server('/', client) with pytest.raises(TransportError) as transport_error: await server.foo() assert transport_error.value.args[0] == ( "Error calling method 'foo': Transport Error") assert isinstance(transport_error.value.args[1], aiohttp.ClientOSError) async def test_forbid_private_methods(aiohttp_client): """Test that we can't call private methods (those starting with '_').""" def create_app(): app = aiohttp.web.Application() return app client = await aiohttp_client(create_app()) server = Server('/', client) with pytest.raises(AttributeError): await server._foo() # nested private method call with pytest.raises(AttributeError): await server.foo.bar._baz() async def test_headers_passthrough(aiohttp_client): """Test that we correctly send RFC headers and merge them with users.""" async def handler(request): return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": true, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler) return app client = await aiohttp_client(create_app()) original_post = client.post async def callback(*args, **kwargs): expected_headers = { 'Content-Type': 'application/json', 'Accept': 'application/json-rpc', 'X-TestCustomHeader': '1' } assert set(expected_headers.items()).issubset( set(kwargs['headers'].items())) return await original_post(*args, **kwargs) client.post = callback server = Server('/', client, headers={'X-TestCustomHeader': '1'}) await server.foo() async def test_method_call(aiohttp_client): """Mixing *args and **kwargs is forbidden by the spec.""" def create_app(): app = aiohttp.web.Application() return app client = await aiohttp_client(create_app()) server = Server('/', client) with pytest.raises(ProtocolError) as error: await server.testmethod(1, 2, a=1, b=2) assert error.value.args[0] == ( "JSON-RPC spec forbids mixing arguments and keyword arguments") async def test_method_nesting(aiohttp_client): """Test that we correctly nest namespaces.""" async def handler(request): request_message = await request.json() if (request_message["params"][0] == request_message["method"]): return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": true, "id": 1}', content_type='application/json') else: return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": false, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler) return app client = await aiohttp_client(create_app()) server = Server('/', client) assert await server.nest.testmethod("nest.testmethod") is True assert await server.nest.testmethod.some.other.method( "nest.testmethod.some.other.method") is True async def test_calls(aiohttp_client): """Test RPC call with positional parameters.""" async def handler1(request): request_message = await request.json() assert request_message["params"] == [42, 23] return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": 19, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler1) return app client = await aiohttp_client(create_app()) server = Server('/', client) assert await server.subtract(42, 23) == 19 async def handler2(request): request_message = await request.json() assert request_message["params"] == {'y': 23, 'x': 42} return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": 19, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler2) return app client = await aiohttp_client(create_app()) server = Server('/', client) assert await server.subtract(x=42, y=23) == 19 async def handler3(request): request_message = await request.json() assert request_message["params"] == [{'foo': 'bar'}] return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": null}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler3) return app client = await aiohttp_client(create_app()) server = Server('/', client) await server.foobar({'foo': 'bar'}) async def test_notification(aiohttp_client): """Verify that we ignore the server response.""" async def handler(request): return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": 19, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler) return app client = await aiohttp_client(create_app()) server = Server('/', client) assert await server.subtract(42, 23, _notification=True) is None async def test_custom_loads(aiohttp_client): """Test RPC call with custom load.""" loads_mock = mock.Mock(wraps=json.loads) async def handler(request): request_message = await request.json() assert request_message["params"] == [42, 23] return aiohttp.web.Response( text='{"jsonrpc": "2.0", "result": 19, "id": 1}', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler) return app client = await aiohttp_client(create_app()) server = Server('/', client, loads=loads_mock) assert await server.subtract(42, 23) == 19 assert loads_mock.call_count == 1 async def test_context_manager(aiohttp_client): # catch non-json responses async def handler1(request): return aiohttp.web.Response( text='not json', content_type='application/json') def create_app(): app = aiohttp.web.Application() app.router.add_route('POST', '/', handler1) return app client = await aiohttp_client(create_app()) async with Server('/', client) as server: assert isinstance(server, Server) assert not server.session.session.closed assert server.session.session.closed emlove-jsonrpc-async-d90b7ef/tox.ini000066400000000000000000000011511445302532400176050ustar00rootroot00000000000000[tox] envlist = py37, py38, py39, py310, flake8, [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/jsonrpc_async commands = pytest --cov=jsonrpc_async --asyncio-mode=auto tests.py coverage report 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_async tests.py