From 70a1d5cd1f827d8804f0f55d62e5c0bd154151e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Wed, 20 Feb 2019 11:30:06 +0100 Subject: [PATCH] SDK-2521 switch plugin transport to sockets --- galaxy/api/jsonrpc.py | 6 +++--- galaxy/api/plugin.py | 29 +++++++++++++++++++++++++---- galaxy/api/stream.py | 35 ----------------------------------- setup.py | 2 +- tests/conftest.py | 38 ++++++++++++++++++++++++-------------- tests/test_features.py | 10 +++++----- tests/test_internal.py | 8 +++++--- 7 files changed, 63 insertions(+), 65 deletions(-) delete mode 100644 galaxy/api/stream.py diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index e7449c3..d9df245 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -149,7 +149,7 @@ class Server(): @staticmethod def _parse_request(data): try: - jsonrpc_request = json.loads(data) + jsonrpc_request = json.loads(data, encoding="utf-8") if jsonrpc_request.get("jsonrpc") != "2.0": raise InvalidRequest() del jsonrpc_request["jsonrpc"] @@ -163,7 +163,7 @@ class Server(): try: line = self._encoder.encode(data) logging.debug("Sending data: %s", line) - self._writer.write(line + "\n") + self._writer.write((line + "\n").encode("utf-8")) asyncio.create_task(self._writer.drain()) except TypeError as error: logging.error(str(error)) @@ -209,7 +209,7 @@ class NotificationClient(): try: line = self._encoder.encode(data) logging.debug("Sending data: %s", line) - self._writer.write(line + "\n") + self._writer.write((line + "\n").encode("utf-8")) asyncio.create_task(self._writer.drain()) except TypeError as error: logging.error("Failed to parse outgoing message: %s", str(error)) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 14a2152..fd19f88 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -6,7 +6,6 @@ from enum import Enum from collections import OrderedDict from galaxy.api.jsonrpc import Server, NotificationClient -from galaxy.api.stream import stdio from galaxy.api.consts import Feature class JSONEncoder(json.JSONEncoder): @@ -21,13 +20,14 @@ class JSONEncoder(json.JSONEncoder): return super().default(o) class Plugin(): - def __init__(self, platform): + def __init__(self, platform, reader, writer, handshake_token): self._platform = platform self._feature_methods = OrderedDict() self._active = True - self._reader, self._writer = stdio() + self._reader, self._writer = reader, writer + self._handshake_token = handshake_token encoder = JSONEncoder() self._server = Server(self._reader, self._writer, encoder) @@ -181,7 +181,8 @@ class Plugin(): def _get_capabilities(self): return { "platform_name": self._platform, - "features": self.features + "features": self.features, + "token": self._handshake_token } @staticmethod @@ -307,3 +308,23 @@ class Plugin(): async def get_game_times(self): raise NotImplementedError() + + +def create_and_run_plugin(plugin_class, argv): + if not issubclass(plugin_class, Plugin): + raise TypeError("plugin_class must be subclass of Plugin") + if len(argv) < 3: + raise ValueError("Not enough parameters, required: token, port") + token = argv[1] + try: + port = int(argv[2]) + except ValueError as e: + raise ValueError("Failed to parse port value, {}".format(e)) + if not (1 <= port <= 65535): + raise ValueError("Port value out of range (1, 65535)") + + async def coroutine(): + reader, writer = await asyncio.open_connection("127.0.0.1", port) + plugin = plugin_class(reader, writer, token) + await plugin.run() + asyncio.run(coroutine()) diff --git a/galaxy/api/stream.py b/galaxy/api/stream.py deleted file mode 100644 index 68ca84b..0000000 --- a/galaxy/api/stream.py +++ /dev/null @@ -1,35 +0,0 @@ -import asyncio -import sys - -class StdinReader(): - def __init__(self): - self._stdin = sys.stdin.buffer - - async def readline(self): - # a single call to sys.stdin.readline() is thread-safe - loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, self._stdin.readline) - -class StdoutWriter(): - def __init__(self): - self._buffer = [] - self._stdout = sys.stdout.buffer - - def write(self, data): - self._buffer.append(data) - - async def drain(self): - data, self._buffer = self._buffer, [] - # a single call to sys.stdout.writelines() is thread-safe - def write(data): - sys.stdout.writelines(data) - sys.stdout.flush() - - loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, write, data) - -def stdio(): - # no support for asyncio stdio yet on Windows, see https://bugs.python.org/issue26832 - # use an executor to read from stdio and write to stdout - # note: if nothing ever drains the writer explicitly, no flushing ever takes place! - return StdinReader(), StdoutWriter() diff --git a/setup.py b/setup.py index dd4e048..153fe1c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.3", + version="0.4", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', diff --git a/tests/conftest.py b/tests/conftest.py index a3d9d53..9c120f2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,36 @@ from contextlib import ExitStack import logging -from unittest.mock import patch +from unittest.mock import patch, MagicMock import pytest from galaxy.api.plugin import Plugin -from galaxy.api.stream import StdinReader, StdoutWriter from galaxy.api.consts import Platform from tests.async_mock import AsyncMock @pytest.fixture() -def plugin(): +def reader(): + stream = MagicMock(name="stream_reader") + stream.readline = AsyncMock() + yield stream + +@pytest.fixture() +def writer(): + stream = MagicMock(name="stream_writer") + stream.write = MagicMock() + stream.drain = AsyncMock() + yield stream + +@pytest.fixture() +def readline(reader): + yield reader.readline + +@pytest.fixture() +def write(writer): + yield writer.write + +@pytest.fixture() +def plugin(reader, writer): """Return plugin instance with all feature methods mocked""" async_methods = ( "authenticate", @@ -40,17 +60,7 @@ def plugin(): stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) for method in methods: stack.enter_context(patch.object(Plugin, method)) - yield Plugin(Platform.Generic) - -@pytest.fixture() -def readline(): - with patch.object(StdinReader, "readline", new_callable=AsyncMock) as mock: - yield mock - -@pytest.fixture() -def write(): - with patch.object(StdoutWriter, "write") as mock: - yield mock + yield Plugin(Platform.Generic, reader, writer, "token") @pytest.fixture(autouse=True) def my_caplog(caplog): diff --git a/tests/test_features.py b/tests/test_features.py index 91034f3..d307bb7 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform, Feature def test_base_class(): - plugin = Plugin(Platform.Generic) + plugin = Plugin(Platform.Generic, None, None, None) assert plugin.features == [] def test_no_overloads(): class PluginImpl(Plugin): #pylint: disable=abstract-method pass - plugin = PluginImpl(Platform.Generic) + plugin = PluginImpl(Platform.Generic, None, None, None) assert plugin.features == [] def test_one_method_feature(): @@ -17,7 +17,7 @@ def test_one_method_feature(): async def get_owned_games(self): pass - plugin = PluginImpl(Platform.Generic) + plugin = PluginImpl(Platform.Generic, None, None, None) assert plugin.features == [Feature.ImportOwnedGames] def test_multiple_methods_feature_all(): @@ -33,7 +33,7 @@ def test_multiple_methods_feature_all(): async def get_room_history_from_timestamp(self, room_id, timestamp): pass - plugin = PluginImpl(Platform.Generic) + plugin = PluginImpl(Platform.Generic, None, None, None) assert plugin.features == [Feature.Chat] def test_multiple_methods_feature_not_all(): @@ -41,5 +41,5 @@ def test_multiple_methods_feature_not_all(): async def send_message(self, room_id, message): pass - plugin = PluginImpl(Platform.Generic) + plugin = PluginImpl(Platform.Generic, None, None, None) assert plugin.features == [] diff --git a/tests/test_internal.py b/tests/test_internal.py index 0f935e5..3837c0e 100644 --- a/tests/test_internal.py +++ b/tests/test_internal.py @@ -4,7 +4,7 @@ import json from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform -def test_get_capabilites(readline, write): +def test_get_capabilites(reader, writer, readline, write): class PluginImpl(Plugin): #pylint: disable=abstract-method async def get_owned_games(self): pass @@ -14,7 +14,8 @@ def test_get_capabilites(readline, write): "id": "3", "method": "get_capabilities" } - plugin = PluginImpl(Platform.Generic) + token = "token" + plugin = PluginImpl(Platform.Generic, reader, writer, token) readline.side_effect = [json.dumps(request), ""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) @@ -25,7 +26,8 @@ def test_get_capabilites(readline, write): "platform_name": "generic", "features": [ "ImportOwnedGames" - ] + ], + "token": token } }