Compare commits

..

5 Commits
0.3 ... 0.6

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
c2a0534162 Deploy only from master 2019-02-20 16:44:53 +01:00
Paweł Kierski
1614fd6eb2 Fix end of stream detecting 2019-02-20 16:41:44 +01:00
Paweł Kierski
48e54a8460 Revert "Make galaxy namespace package" 2019-02-20 14:09:34 +01:00
Paweł Kierski
70a1d5cd1f SDK-2521 switch plugin transport to sockets 2019-02-20 11:30:06 +01:00
Romuald Juchnowicz-Bierbasz
853ecf1d3b Make galaxy namespace package 2019-02-19 16:53:42 +01:00
8 changed files with 71 additions and 69 deletions

View File

@@ -20,5 +20,7 @@ deploy_package:
- curl -X POST --silent --show-error --fail - curl -X POST --silent --show-error --fail
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}" "https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
when: manual when: manual
only:
- master
except: except:
- tags - tags

View File

@@ -64,10 +64,12 @@ class Server():
async def run(self): async def run(self):
while self._active: while self._active:
data = await self._reader.readline() try:
if not data: data = await self._reader.readline()
# on windows rederecting a pipe to stdin result on continues if not data:
# not-blocking return of empty line on EOF self._eof()
continue
except:
self._eof() self._eof()
continue continue
data = data.strip() data = data.strip()
@@ -149,7 +151,7 @@ class Server():
@staticmethod @staticmethod
def _parse_request(data): def _parse_request(data):
try: try:
jsonrpc_request = json.loads(data) jsonrpc_request = json.loads(data, encoding="utf-8")
if jsonrpc_request.get("jsonrpc") != "2.0": if jsonrpc_request.get("jsonrpc") != "2.0":
raise InvalidRequest() raise InvalidRequest()
del jsonrpc_request["jsonrpc"] del jsonrpc_request["jsonrpc"]
@@ -163,7 +165,7 @@ class Server():
try: try:
line = self._encoder.encode(data) line = self._encoder.encode(data)
logging.debug("Sending data: %s", line) 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()) asyncio.create_task(self._writer.drain())
except TypeError as error: except TypeError as error:
logging.error(str(error)) logging.error(str(error))
@@ -209,7 +211,7 @@ class NotificationClient():
try: try:
line = self._encoder.encode(data) line = self._encoder.encode(data)
logging.debug("Sending data: %s", line) 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()) asyncio.create_task(self._writer.drain())
except TypeError as error: except TypeError as error:
logging.error("Failed to parse outgoing message: %s", str(error)) logging.error("Failed to parse outgoing message: %s", str(error))

View File

@@ -6,7 +6,6 @@ from enum import Enum
from collections import OrderedDict from collections import OrderedDict
from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.jsonrpc import Server, NotificationClient
from galaxy.api.stream import stdio
from galaxy.api.consts import Feature from galaxy.api.consts import Feature
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
@@ -21,13 +20,14 @@ class JSONEncoder(json.JSONEncoder):
return super().default(o) return super().default(o)
class Plugin(): class Plugin():
def __init__(self, platform): def __init__(self, platform, reader, writer, handshake_token):
self._platform = platform self._platform = platform
self._feature_methods = OrderedDict() self._feature_methods = OrderedDict()
self._active = True self._active = True
self._reader, self._writer = stdio() self._reader, self._writer = reader, writer
self._handshake_token = handshake_token
encoder = JSONEncoder() encoder = JSONEncoder()
self._server = Server(self._reader, self._writer, encoder) self._server = Server(self._reader, self._writer, encoder)
@@ -181,7 +181,8 @@ class Plugin():
def _get_capabilities(self): def _get_capabilities(self):
return { return {
"platform_name": self._platform, "platform_name": self._platform,
"features": self.features "features": self.features,
"token": self._handshake_token
} }
@staticmethod @staticmethod
@@ -307,3 +308,23 @@ class Plugin():
async def get_game_times(self): async def get_game_times(self):
raise NotImplementedError() 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())

View File

@@ -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()

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="galaxy.plugin.api", name="galaxy.plugin.api",
version="0.3", version="0.6",
description="Galaxy python plugin API", description="Galaxy python plugin API",
author='Galaxy team', author='Galaxy team',
author_email='galaxy@gog.com', author_email='galaxy@gog.com',

View File

@@ -1,16 +1,36 @@
from contextlib import ExitStack from contextlib import ExitStack
import logging import logging
from unittest.mock import patch from unittest.mock import patch, MagicMock
import pytest import pytest
from galaxy.api.plugin import Plugin from galaxy.api.plugin import Plugin
from galaxy.api.stream import StdinReader, StdoutWriter
from galaxy.api.consts import Platform from galaxy.api.consts import Platform
from tests.async_mock import AsyncMock from tests.async_mock import AsyncMock
@pytest.fixture() @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""" """Return plugin instance with all feature methods mocked"""
async_methods = ( async_methods = (
"authenticate", "authenticate",
@@ -40,17 +60,7 @@ def plugin():
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
for method in methods: for method in methods:
stack.enter_context(patch.object(Plugin, method)) stack.enter_context(patch.object(Plugin, method))
yield Plugin(Platform.Generic) yield Plugin(Platform.Generic, reader, writer, "token")
@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
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def my_caplog(caplog): def my_caplog(caplog):

View File

@@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform, Feature from galaxy.api.consts import Platform, Feature
def test_base_class(): def test_base_class():
plugin = Plugin(Platform.Generic) plugin = Plugin(Platform.Generic, None, None, None)
assert plugin.features == [] assert plugin.features == []
def test_no_overloads(): def test_no_overloads():
class PluginImpl(Plugin): #pylint: disable=abstract-method class PluginImpl(Plugin): #pylint: disable=abstract-method
pass pass
plugin = PluginImpl(Platform.Generic) plugin = PluginImpl(Platform.Generic, None, None, None)
assert plugin.features == [] assert plugin.features == []
def test_one_method_feature(): def test_one_method_feature():
@@ -17,7 +17,7 @@ def test_one_method_feature():
async def get_owned_games(self): async def get_owned_games(self):
pass pass
plugin = PluginImpl(Platform.Generic) plugin = PluginImpl(Platform.Generic, None, None, None)
assert plugin.features == [Feature.ImportOwnedGames] assert plugin.features == [Feature.ImportOwnedGames]
def test_multiple_methods_feature_all(): 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): async def get_room_history_from_timestamp(self, room_id, timestamp):
pass pass
plugin = PluginImpl(Platform.Generic) plugin = PluginImpl(Platform.Generic, None, None, None)
assert plugin.features == [Feature.Chat] assert plugin.features == [Feature.Chat]
def test_multiple_methods_feature_not_all(): 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): async def send_message(self, room_id, message):
pass pass
plugin = PluginImpl(Platform.Generic) plugin = PluginImpl(Platform.Generic, None, None, None)
assert plugin.features == [] assert plugin.features == []

View File

@@ -4,7 +4,7 @@ import json
from galaxy.api.plugin import Plugin from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform 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 class PluginImpl(Plugin): #pylint: disable=abstract-method
async def get_owned_games(self): async def get_owned_games(self):
pass pass
@@ -14,7 +14,8 @@ def test_get_capabilites(readline, write):
"id": "3", "id": "3",
"method": "get_capabilities" "method": "get_capabilities"
} }
plugin = PluginImpl(Platform.Generic) token = "token"
plugin = PluginImpl(Platform.Generic, reader, writer, token)
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0]) response = json.loads(write.call_args[0][0])
@@ -25,7 +26,8 @@ def test_get_capabilites(readline, write):
"platform_name": "generic", "platform_name": "generic",
"features": [ "features": [
"ImportOwnedGames" "ImportOwnedGames"
] ],
"token": token
} }
} }