pax_global_header00006660000000000000000000000064145515352260014522gustar00rootroot0000000000000052 comment=e49753c2d91857fb578c8f7170e81eb1a02f4692 emlove-jsonrpc-websocket-e49753c/000077500000000000000000000000001455153522600170165ustar00rootroot00000000000000emlove-jsonrpc-websocket-e49753c/.coveragerc000066400000000000000000000001261455153522600211360ustar00rootroot00000000000000[run] source = jsonrpc_websocket relative_files = True [report] show_missing = True emlove-jsonrpc-websocket-e49753c/.github/000077500000000000000000000000001455153522600203565ustar00rootroot00000000000000emlove-jsonrpc-websocket-e49753c/.github/workflows/000077500000000000000000000000001455153522600224135ustar00rootroot00000000000000emlove-jsonrpc-websocket-e49753c/.github/workflows/main.yml000066400000000000000000000025371455153522600240710ustar00rootroot00000000000000name: 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 if [[ "${{ matrix.python-version }}" == "3.7" ]] ; then pip install asynctest==0.13.0 ; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 jsonrpc_websocket 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_websocket tests.py - name: Test with pytest run: | pytest --cov-report term-missing --cov=jsonrpc_websocket 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-websocket-e49753c/.gitignore000066400000000000000000000014411455153522600210060ustar00rootroot00000000000000# 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-websocket-e49753c/.mailmap000066400000000000000000000004641455153522600204430ustar00rootroot00000000000000# 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-websocket-e49753c/LICENSE.txt000066400000000000000000000026671455153522600206540ustar00rootroot00000000000000Copyright (c) 2014 Giuseppe Ciotta Copyright (c) 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-websocket-e49753c/MANIFEST.in000066400000000000000000000000241455153522600205500ustar00rootroot00000000000000include *.txt *.rst emlove-jsonrpc-websocket-e49753c/README.rst000066400000000000000000000155631455153522600205170ustar00rootroot00000000000000jsonrpc-websocket: a compact JSON-RPC websocket client library for asyncio ======================================================================================================= .. image:: https://img.shields.io/pypi/v/jsonrpc-websocket.svg :target: https://pypi.python.org/pypi/jsonrpc-websocket .. image:: https://github.com/emlove/jsonrpc-websocket/workflows/tests/badge.svg :target: https://github.com/emlove/jsonrpc-websocket/actions .. image:: https://coveralls.io/repos/emlove/jsonrpc-websocket/badge.svg?branch=main :target: https://coveralls.io/github/emlove/jsonrpc-websocket?branch=main This is a compact and simple JSON-RPC websocket 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, and will be closed when the websocket connection is closed. If you pass in an external ClientSession, it is your responsibility to close it when you are finished. Execute remote JSON-RPC functions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import asyncio from jsonrpc_websocket import Server async def routine(): server = Server('ws://localhost:9090') try: await server.ws_connect() 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) finally: await server.close() asyncio.get_event_loop().run_until_complete(routine()) A notification ~~~~~~~~~~~~~~ .. code-block:: python import asyncio from jsonrpc_websocket import Server async def routine(): server = Server('ws://localhost:9090') try: await server.ws_connect() await server.foo(bar=1, _notification=True) finally: await server.close() asyncio.get_event_loop().run_until_complete(routine()) Handle requests from server to client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import asyncio from jsonrpc_websocket import Server def client_method(arg1, arg2): return arg1 + arg2 async def routine(): server = Server('ws://localhost:9090') # client_method is called when server requests method 'namespace.client_method' server.namespace.client_method = client_method try: await server.ws_connect() finally: await server.close() 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_websocket import Server async def routine(): server = Server( 'ws://localhost:9090', auth=aiohttp.BasicAuth('user', 'pass'), headers={'x-test2': 'true'}) try: await server.ws_connect() await server.foo() finally: await server.close() asyncio.get_event_loop().run_until_complete(routine()) Pass through aiohttp exceptions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python import asyncio import aiohttp from jsonrpc_websocket import Server async def routine(): server = Server('ws://unknown-host') try: await server.ws_connect() await server.foo() except TransportError as transport_error: print(transport_error.args[1]) # this will hold a aiohttp exception instance finally: await server.close() 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 --------- 3.1.5 (2024-01-16) ~~~~~~~~~~~~~~~~~~ - Add explicit dependency to async-timeout `(#13) `_ `@miettal `_ 3.1.4 (2022-05-23) ~~~~~~~~~~~~~~~~~~ - Only reconnect session when the session is managed internally - Remove deprecated with timeout syntax 3.1.3 (2022-05-23) ~~~~~~~~~~~~~~~~~~ - Fix unclosed client session bug `(#12) `_ `@Arjentix `_ 3.1.2 (2022-05-03) ~~~~~~~~~~~~~~~~~~ - Unpin test dependencies 3.1.1 (2021-11-21) ~~~~~~~~~~~~~~~~~~ - Fixed compatibility with async_timeout 4.0 3.1.0 (2021-05-03) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 2.1.0 3.0.0 (2021-03-17) ~~~~~~~~~~~~~~~~~~ - 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})``) 2.0.0 (2020-12-22) ~~~~~~~~~~~~~~~~~~ - Remove session as a reserved attribute on Server 1.2.1 (2020-09-11) ~~~~~~~~~~~~~~~~~~ - Fix loop not closing after client closes 1.2.0 (2020-08-24) ~~~~~~~~~~~~~~~~~~ - Support for async server request handlers 1.1.0 (2020-02-17) ~~~~~~~~~~~~~~~~~~ - Support servers that send JSON-RPC requests as binary messages encoded with UTF-8 `(#5) `_ `@shiaky `_ 1.0.2 (2019-11-12) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 1.0.3 1.0.1 (2018-08-23) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 1.0.2 1.0.0 (2018-07-06) ~~~~~~~~~~~~~~~~~~ - Bumped jsonrpc-base to version 1.0.1 0.6 (2018-03-11) ~~~~~~~~~~~~~~~~ - Minimum required version of aiohttp is now 3.0. - Support for Python 3.4 is now dropped. 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-websocket-e49753c/jsonrpc_websocket/000077500000000000000000000000001455153522600225425ustar00rootroot00000000000000emlove-jsonrpc-websocket-e49753c/jsonrpc_websocket/__init__.py000066400000000000000000000001001455153522600246420ustar00rootroot00000000000000from .jsonrpc import Server, TransportError # noqa: F401, F403 emlove-jsonrpc-websocket-e49753c/jsonrpc_websocket/jsonrpc.py000066400000000000000000000131651455153522600246000ustar00rootroot00000000000000import asyncio import json import aiohttp from aiohttp import ClientError from aiohttp.http_exceptions import HttpProcessingError import async_timeout 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, **connect_kwargs): super().__init__() self._session = session or aiohttp.ClientSession() # True if we made our own session self._internal_session = session is None self._client = None self._connect_kwargs = connect_kwargs self._url = url self._connect_kwargs['headers'] = self._connect_kwargs.get( 'headers', {}) self._connect_kwargs['headers']['Content-Type'] = ( self._connect_kwargs['headers'].get( 'Content-Type', 'application/json')) self._connect_kwargs['headers']['Accept'] = ( self._connect_kwargs['headers'].get( 'Accept', 'application/json-rpc')) self._timeout = self._connect_kwargs.get('timeout') self._pending_messages = {} 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. """ if self._client is None: raise TransportError('Client is not connected.', message) try: await self._client.send_str(message.serialize()) if message.response_id: pending_message = PendingMessage() self._pending_messages[message.response_id] = pending_message response = await pending_message.wait(self._timeout) del self._pending_messages[message.response_id] else: response = None return message.parse_response(response) except (ClientError, HttpProcessingError, asyncio.TimeoutError) as exc: raise TransportError('Transport Error', message, exc) async def ws_connect(self): """Connect to the websocket server.""" if self.connected: raise TransportError('Connection already open.') try: if self._internal_session and self._session.closed: self._session = aiohttp.ClientSession() self._client = await self._session.ws_connect( self._url, **self._connect_kwargs) except (ClientError, HttpProcessingError, asyncio.TimeoutError) as exc: raise TransportError('Error connecting to server', None, exc) return self._session.loop.create_task(self._ws_loop()) async def _ws_loop(self): """Listen for messages from the websocket server.""" msg = None try: async for msg in self._client: if msg.type == aiohttp.WSMsgType.ERROR: break elif msg.type == aiohttp.WSMsgType.BINARY: try: # If we get a binary message, try and decode it as a # UTF-8 JSON string, in case the server is sending # binary websocket messages. If it doens't decode we'll # ignore it since we weren't expecting binary messages # anyway data = json.loads(msg.data.decode()) except ValueError: continue elif msg.type == aiohttp.WSMsgType.TEXT: try: data = msg.json() except ValueError as exc: raise TransportError('Error Parsing JSON', None, exc) else: # This is tested with test_message_ping_ignored, but # cpython's optimizations prevent coveragepy from detecting # that it's run # https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered continue # pragma: no cover if 'method' in data: request = jsonrpc_base.Request.parse(data) response = await self.async_receive_request(request) if response: await self.send_message(response) else: self._pending_messages[data['id']].response = data except (ClientError, HttpProcessingError, asyncio.TimeoutError) as exc: raise TransportError('Transport Error', None, exc) finally: await self.close() if msg and msg.type == aiohttp.WSMsgType.ERROR: raise TransportError( 'Websocket error detected. Connection closed.') async def close(self): """Close the connection to the websocket server.""" if self.connected: await self._client.close() self._client = None if self._internal_session: await self._session.close() @property def connected(self): """Websocket server is connected.""" return self._client is not None class PendingMessage(object): """Wait for response of pending message.""" def __init__(self): self._event = asyncio.Event() self._response = None async def wait(self, timeout=None): async with async_timeout.timeout(timeout): await self._event.wait() return self._response @property def response(self): return self._response @response.setter def response(self, value): self._response = value self._event.set() emlove-jsonrpc-websocket-e49753c/requirements-test.txt000066400000000000000000000002521455153522600232560ustar00rootroot00000000000000flake8>=3.7.8 coverage>=5.5 coveralls>=3.0.1 jsonrpc-base>=2.1.0 aiohttp>=3.0.0 pytest-aiohttp>=0.3.0 pytest>=6.2.2 pytest-cov>=2.11.1 pytest-asyncio>=0.14.0 tox>=3.23.0 emlove-jsonrpc-websocket-e49753c/setup.py000066400000000000000000000023661455153522600205370ustar00rootroot00000000000000from __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-websocket', version='3.1.5', author='Emily Love Mills', author_email='emily@emlove.me', packages=('jsonrpc_websocket',), license='BSD', keywords='json-rpc async asyncio websocket', url='http://github.com/emlove/jsonrpc-websocket', description='''A JSON-RPC websocket client library for asyncio''', long_description_content_type='text/x-rst', long_description=open('README.rst').read(), install_requires=[ 'jsonrpc-base>=2.1.0', 'aiohttp>=3.0.0', 'async-timeout>=4.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-websocket-e49753c/tests.py000066400000000000000000000273701455153522600205430ustar00rootroot00000000000000import asyncio import json from unittest import mock import sys import aiohttp from aiohttp import ClientWebSocketResponse import aiohttp.web import pytest import pytest_asyncio import jsonrpc_base import jsonrpc_websocket.jsonrpc from jsonrpc_websocket import Server, TransportError if sys.version_info[:2] < (3, 8): from asynctest import patch else: from unittest.mock import patch pytestmark = pytest.mark.asyncio class JsonTestClient(): def __init__(self, loop=None): self.test_server = None self.loop = loop self.connect_side_effect = None async def ws_connect(self, *args, **kwargs): if self.connect_side_effect: self.connect_side_effect() self.test_server = JsonTestServer(self.loop) return self.test_server async def close(self): self._test_server = None @property def handler(self): return self.test_server.send_handler @handler.setter def handler(self, value): self.test_server.send_handler = value def receive(self, data): self.test_server.test_receive(data) def receive_binary(self, data): self.test_server.test_binary(data) @property def closed(self): return self.test_server is None class JsonTestServer(ClientWebSocketResponse): def __init__(self, loop=None): self.loop = loop self.send_handler = None self.receive_queue = asyncio.Queue() self._closed = False self.receive_side_effect = None async def send_str(self, data): self.send_handler(self, data) def test_receive(self, data): self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, data, '')) def test_binary(self, data=bytes()): self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, data, '')) def test_error(self): self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.ERROR, 0, '')) def test_close(self): self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.CLOSED, None, None)) def test_ping(self): self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.PING, 0, '')) async def receive(self): value = await self.receive_queue.get() if self.receive_side_effect: self.receive_side_effect() return (value) async def close(self): if not self._closed: self._closed = True self.receive_queue.put_nowait( aiohttp.WSMessage(aiohttp.WSMsgType.CLOSING, None, None)) def assertSameJSON(json1, json2): """Tells whether two json strings, once decoded, are the same dictionary""" assert json.loads(json1) == json.loads(json2) @pytest_asyncio.fixture async def client(event_loop): """Generate a mock json server.""" return JsonTestClient(event_loop) @pytest_asyncio.fixture async def server(client): """Generate a mock json server.""" server = Server('/xmlrpc', session=client, timeout=0.2) client.run_loop_future = await server.ws_connect() yield server if server.connected: client.test_server.test_close() await client.run_loop_future def test_pending_message_response(): pending_message = jsonrpc_websocket.jsonrpc.PendingMessage() pending_message.response = 10 assert pending_message.response == 10 async def test_internal_session(client): with patch('jsonrpc_websocket.jsonrpc.aiohttp.ClientSession', return_value=client) as client_class: server = Server('/xmlrpc', timeout=0.2) client_class.assert_called_once() await server.close() await server.ws_connect() assert client_class.call_count == 2 async def test_send_message(server): # catch timeout responses with pytest.raises(TransportError) as transport_error: def handler(server, data): try: sleep_coroutine = asyncio.sleep(10) wait_coroutine = asyncio.wait(sleep_coroutine) except asyncio.CancelledError: # event loop will be terminated before sleep finishes pass # Prevent warning about non-awaited coroutines sleep_coroutine.close() wait_coroutine.close() server._session.handler = handler 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_client_closed(server): assert server._session.run_loop_future.done() is False await server.close() assert server._session.run_loop_future.done() is False await server._session.run_loop_future assert server._session.run_loop_future.done() is True with pytest.raises(TransportError, match='Client is not connected.'): def handler(server, data): pass server._session.handler = handler await server.send_message( jsonrpc_base.Request('my_method', params=None, msg_id=1)) async def test_double_connect(server): with pytest.raises(TransportError, match='Connection already open.'): await server.ws_connect() async def test_ws_error(server): server._session.test_server.test_error() with pytest.raises( TransportError, match='Websocket error detected. Connection closed.'): await server._session.run_loop_future async def test_binary(server): server._session.test_server.test_binary() async def test_message_not_json(server): with pytest.raises(TransportError) as transport_error: server._session.receive('not json') await server._session.run_loop_future assert isinstance(transport_error.value.args[1], ValueError) async def test_message_binary_not_utf8(server): # If we get a binary message, we should try to decode it as JSON, but # if it's not valid we should just ignore it, and an exception should # not be thrown server._session.receive_binary(bytes((0xE0, 0x80, 0x80))) server._session.test_server.test_close() await server._session.run_loop_future async def test_message_binary_not_json(server): # If we get a binary message, we should try to decode it as JSON, but # if it's not valid we should just ignore it, and an exception should # not be thrown server._session.receive_binary('not json'.encode()) server._session.test_server.test_close() await server._session.run_loop_future async def test_message_ping_ignored(server): server._session.test_server.test_ping() server._session.test_server.test_close() await server._session.run_loop_future async def test_connection_timeout(server): def bad_connect(): raise aiohttp.ClientError("Test Error") server._session.connect_side_effect = bad_connect await server.close() with pytest.raises(TransportError) as transport_error: await server.ws_connect() assert isinstance(transport_error.value.args[1], aiohttp.ClientError) async def test_server_request(server): def test_method(): return 1 server.test_method = test_method def handler(server, data): response = json.loads(data) assert response["result"] == 1 server._session.handler = handler server._session.receive( '{"jsonrpc": "2.0", "method": "test_method", "id": 1}') server._session.test_server.test_close() await server._session.run_loop_future async def test_server_async_request(server): async def test_method_async(): return 2 server.test_method_async = test_method_async def handler(server, data): response = json.loads(data) assert response["result"] == 2 server._session.handler = handler server._session.receive( '{"jsonrpc": "2.0", "method": "test_method_async", "id": 1}') server._session.test_server.test_close() await server._session.run_loop_future async def test_server_request_binary(server): # Test that if the server sends a binary websocket message, that's a # UTF-8 encoded JSON request we process it def test_method_binary(): return 1 server.test_method_binary = test_method_binary def handler(server, data): response = json.loads(data) assert response["result"] == 1 server._session.handler = handler server._session.receive_binary( '{"jsonrpc": "2.0", "method": "test_method_binary", "id": 1}'.encode()) server._session.test_server.test_close() await server._session.run_loop_future async def test_server_notification(server): def test_notification(): pass server.test_notification = test_notification server._session.receive( '{"jsonrpc": "2.0", "method": "test_notification"}') server._session.test_server.test_close() await server._session.run_loop_future async def test_server_response_error(server): def test_error(): return 1 server.test_error = test_error def receive_side_effect(): raise aiohttp.ClientError("Test Error") server._session.test_server.receive_side_effect = receive_side_effect server._session.receive( '{"jsonrpc": "2.0", "method": "test_error", "id": 1}') server._session.test_server.test_close() with pytest.raises(TransportError) as transport_error: await server._session.run_loop_future assert isinstance(transport_error.value.args[1], aiohttp.ClientError) async def test_calls(server): # rpc call with positional parameters: def handler1(server, data): request = json.loads(data) assert request["params"] == [42, 23] server.test_receive( '{"jsonrpc": "2.0", "result": 19, "id": "abcd-1234"}') server._session.handler = handler1 with mock.patch("uuid.uuid4", return_value="abcd-1234"): assert (await server.subtract(42, 23)) == 19 def handler2(server, data): request = json.loads(data) assert request["params"] == {'y': 23, 'x': 42} server.test_receive( '{"jsonrpc": "2.0", "result": 19, "id": "abcd-1234"}') server._session.handler = handler2 with mock.patch("uuid.uuid4", return_value="abcd-1234"): assert (await server.subtract(x=42, y=23)) == 19 def handler3(server, data): request = json.loads(data) assert request["params"] == [{'foo': 'bar'}] server._session.handler = handler3 await server.foobar({'foo': 'bar'}, _notification=True) def handler3(server, data): request = json.loads(data) assert request["params"] == {'foo': 'bar'} server._session.handler = handler3 await server.foobar(**{'foo': 'bar'}, _notification=True) async def test_simultaneous_calls(event_loop, server): # Test that calls can be delivered simultaneously, and can return out # of order def handler(server, data): pass server._session.handler = handler with mock.patch("uuid.uuid4", return_value="abcd-1234"): task1 = event_loop.create_task(server.call1()) with mock.patch("uuid.uuid4", return_value="efgh-5678"): task2 = event_loop.create_task(server.call2()) assert task1.done() is False assert task2.done() is False server._session.receive( '{"jsonrpc": "2.0", "result": 2, "id": "efgh-5678"}') await task2 assert task1.done() is False assert task2.done() server._session.receive( '{"jsonrpc": "2.0", "result": 1, "id": "abcd-1234"}') await task1 assert task1.done() assert task2.done() assert 1 == task1.result() assert 2 == task2.result() async def test_notification(server): # Verify that we ignore the server response def handler(server, data): pass server._session.handler = handler assert (await server.subtract(42, 23, _notification=True)) is None emlove-jsonrpc-websocket-e49753c/tox.ini000066400000000000000000000012211455153522600203250ustar00rootroot00000000000000[tox] envlist = py37, py38, py39, py310, flake8, [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/jsonrpc_websocket commands = pytest --cov-report term-missing --cov=jsonrpc_websocket tests.py {posargs} deps = -r{toxinidir}/requirements-test.txt [testenv:py37] basepython = python3.7 deps = {[testenv]deps} asynctest==0.13.0 [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_websocket tests.py