From 11a6416702f63771ec87e3ba552836edc7d65d46 Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Mon, 11 Feb 2019 10:30:41 +0100 Subject: [PATCH 001/147] Initial commit --- README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8f0c6b --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# galaxy-plugin-api + From 3e9276e41925e9ec9876de1e9ca4976ec4c94a92 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 11:05:46 +0100 Subject: [PATCH 002/147] SDK-2520: Move files from desktop-galaxy-client --- .gitignore | 2 + galaxy/__init__.py | 0 galaxy/api/__init__.py | 1 + galaxy/api/consts.py | 33 ++++ galaxy/api/jsonrpc.py | 202 +++++++++++++++++++++ galaxy/api/plugin.py | 308 ++++++++++++++++++++++++++++++++ galaxy/api/stream.py | 35 ++++ galaxy/api/types.py | 154 ++++++++++++++++ requirements.txt | 1 + setup.py | 10 ++ tests/__init__.py | 0 tests/async_mock.py | 6 + tests/conftest.py | 58 ++++++ tests/test_achievements.py | 85 +++++++++ tests/test_authenticate.py | 86 +++++++++ tests/test_chat.py | 336 +++++++++++++++++++++++++++++++++++ tests/test_features.py | 45 +++++ tests/test_game_times.py | 85 +++++++++ tests/test_install_game.py | 16 ++ tests/test_internal.py | 66 +++++++ tests/test_launch_game.py | 16 ++ tests/test_local_games.py | 84 +++++++++ tests/test_owned_games.py | 152 ++++++++++++++++ tests/test_uninstall_game.py | 16 ++ tests/test_users.py | 220 +++++++++++++++++++++++ 25 files changed, 2017 insertions(+) create mode 100644 .gitignore create mode 100644 galaxy/__init__.py create mode 100644 galaxy/api/__init__.py create mode 100644 galaxy/api/consts.py create mode 100644 galaxy/api/jsonrpc.py create mode 100644 galaxy/api/plugin.py create mode 100644 galaxy/api/stream.py create mode 100644 galaxy/api/types.py create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/async_mock.py create mode 100644 tests/conftest.py create mode 100644 tests/test_achievements.py create mode 100644 tests/test_authenticate.py create mode 100644 tests/test_chat.py create mode 100644 tests/test_features.py create mode 100644 tests/test_game_times.py create mode 100644 tests/test_install_game.py create mode 100644 tests/test_internal.py create mode 100644 tests/test_launch_game.py create mode 100644 tests/test_local_games.py create mode 100644 tests/test_owned_games.py create mode 100644 tests/test_uninstall_game.py create mode 100644 tests/test_users.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a87a247 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# pytest +__pycache__/ diff --git a/galaxy/__init__.py b/galaxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/galaxy/api/__init__.py b/galaxy/api/__init__.py new file mode 100644 index 0000000..e876727 --- /dev/null +++ b/galaxy/api/__init__.py @@ -0,0 +1 @@ +from galaxy.api.plugin import Plugin diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py new file mode 100644 index 0000000..056ba1f --- /dev/null +++ b/galaxy/api/consts.py @@ -0,0 +1,33 @@ +from enum import Enum + +class Platform(Enum): + Unknown = "unknown" + Gog = "gog" + Steam = "steam" + Psn = "psn" + XBoxOne = "xboxone" + Generic = "generic" + Origin = "origin" + Uplay = "uplay" + Battlenet = "battlenet" + +class Feature(Enum): + ImportInstalledGames = "ImportInstalledGames" + ImportOwnedGames = "ImportOwnedGames" + LaunchGame = "LaunchGame" + InstallGame = "InstallGame" + UninstallGame = "UninstallGame" + ImportAchievements = "ImportAchievements" + ImportGameTime = "ImportGameTime" + Chat = "Chat" + ImportUsers = "ImportUsers" + VerifyGame = "VerifyGame" + +class LocalGameState(Enum): + Installed = "Installed" + Running = "Running" + +class PresenceState(Enum): + Online = "online" + Offline = "offline" + Away = "away" diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py new file mode 100644 index 0000000..f1cfe41 --- /dev/null +++ b/galaxy/api/jsonrpc.py @@ -0,0 +1,202 @@ +import asyncio +from collections import namedtuple +import logging +import json + +class JsonRpcError(Exception): + def __init__(self, code, message, data=None): + self.code = code + self.message = message + self.data = data + super().__init__() + +class ParseError(JsonRpcError): + def __init__(self): + super().__init__(-32700, "Parse error") + +class InvalidRequest(JsonRpcError): + def __init__(self): + super().__init__(-32600, "Invalid Request") + +class MethodNotFound(JsonRpcError): + def __init__(self): + super().__init__(-32601, "Method not found") + +class InvalidParams(JsonRpcError): + def __init__(self): + super().__init__(-32601, "Invalid params") + +class ApplicationError(JsonRpcError): + def __init__(self, data): + super().__init__(-32003, "Custom error", data) + +Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) +Method = namedtuple("Method", ["callback", "internal"]) + +class Server(): + def __init__(self, reader, writer, encoder=json.JSONEncoder()): + self._active = True + self._reader = reader + self._writer = writer + self._encoder = encoder + self._methods = {} + self._notifications = {} + self._eof_listeners = [] + + def register_method(self, name, callback, internal): + self._methods[name] = Method(callback, internal) + + def register_notification(self, name, callback, internal): + self._notifications[name] = Method(callback, internal) + + def register_eof(self, callback): + self._eof_listeners.append(callback) + + async def run(self): + while self._active: + data = await self._reader.readline() + if not data: + # on windows rederecting a pipe to stdin result on continues + # not-blocking return of empty line on EOF + self._eof() + continue + data = data.strip() + logging.debug("Received data: %s", data) + self._handle_input(data) + + def stop(self): + self._active = False + + def _eof(self): + logging.info("Received EOF") + self.stop() + for listener in self._eof_listeners: + listener() + + def _handle_input(self, data): + try: + request = self._parse_request(data) + except JsonRpcError as error: + self._send_error(None, error) + return + + logging.debug("Parsed input: %s", request) + + if request.id is not None: + self._handle_request(request) + else: + self._handle_notification(request) + + def _handle_notification(self, request): + logging.debug("Handling notification %s", request) + method = self._notifications.get(request.method) + if not method: + logging.error("Received uknown notification: %s", request.method) + + callback, internal = method + if internal: + # internal requests are handled immediately + callback(**request.params) + else: + try: + asyncio.create_task(callback(**request.params)) + except Exception as error: #pylint: disable=broad-except + logging.error( + "Unexpected exception raised in notification handler: %s", + repr(error) + ) + + def _handle_request(self, request): + logging.debug("Handling request %s", request) + method = self._methods.get(request.method) + + if not method: + logging.error("Received uknown request: %s", request.method) + self._send_error(request.id, MethodNotFound()) + return + + callback, internal = method + if internal: + # internal requests are handled immediately + response = callback(request.params) + self._send_response(request.id, response) + else: + async def handle(): + try: + result = await callback(request.params) + self._send_response(request.id, result) + except TypeError: + self._send_error(request.id, InvalidParams()) + except NotImplementedError: + self._send_error(request.id, MethodNotFound()) + except JsonRpcError as error: + self._send_error(request.id, error) + except Exception as error: #pylint: disable=broad-except + logging.error("Unexpected exception raised in plugin handler: %s", repr(error)) + + asyncio.create_task(handle()) + + @staticmethod + def _parse_request(data): + try: + jsonrpc_request = json.loads(data) + if jsonrpc_request.get("jsonrpc") != "2.0": + raise InvalidRequest() + del jsonrpc_request["jsonrpc"] + return Request(**jsonrpc_request) + except json.JSONDecodeError: + raise ParseError() + except TypeError: + raise InvalidRequest() + + def _send(self, data): + try: + line = self._encoder.encode(data) + logging.debug("Sending data: %s", line) + self._writer.write(line + "\n") + asyncio.create_task(self._writer.drain()) + except TypeError as error: + logging.error(str(error)) + + def _send_response(self, request_id, result): + response = { + "jsonrpc": "2.0", + "id": request_id, + "result": result + } + self._send(response) + + def _send_error(self, request_id, error): + response = { + "jsonrpc": "2.0", + "id": request_id, + "error": { + "code": error.code, + "message": error.message, + "data": error.data + } + } + self._send(response) + +class NotificationClient(): + def __init__(self, writer, encoder=json.JSONEncoder()): + self._writer = writer + self._encoder = encoder + self._methods = {} + + def notify(self, method, params): + notification = { + "jsonrpc": "2.0", + "method": method, + "params": params + } + self._send(notification) + + def _send(self, data): + try: + line = self._encoder.encode(data) + logging.debug("Sending data: %s", line) + self._writer.write(line + "\n") + 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 new file mode 100644 index 0000000..f2b8bbb --- /dev/null +++ b/galaxy/api/plugin.py @@ -0,0 +1,308 @@ +import asyncio +import json +import logging +import dataclasses +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): + def default(self, o): # pylint: disable=method-hidden + if dataclasses.is_dataclass(o): + # filter None values + def dict_factory(elements): + return {k: v for k, v in elements if v is not None} + return dataclasses.asdict(o, dict_factory=dict_factory) + if isinstance(o, Enum): + return o.value + return super().default(o) + +class Plugin(): + def __init__(self, platform): + self._platform = platform + + self._feature_methods = OrderedDict() + self._active = True + + self._reader, self._writer = stdio() + + encoder = JSONEncoder() + self._server = Server(self._reader, self._writer, encoder) + self._notification_client = NotificationClient(self._writer, encoder) + + def eof_handler(): + self._active = False + self._server.register_eof(eof_handler) + + # internal + self._register_method("shutdown", self._shutdown, internal=True) + self._register_method("get_capabilities", self._get_capabilities, internal=True) + self._register_method("ping", self._ping, internal=True) + + # implemented by developer + self._register_method("init_authentication", self.authenticate) + self._register_method("pass_login_credentials", self.pass_login_credentials) + self._register_method( + "import_owned_games", + self.get_owned_games, + result_name="owned_games", + feature=Feature.ImportOwnedGames + ) + self._register_method( + "import_unlocked_achievements", + self.get_unlocked_achievements, + result_name="unlocked_achievements", + feature=Feature.ImportAchievements + ) + self._register_method( + "import_local_games", + self.get_local_games, + result_name="local_games", + feature=Feature.ImportInstalledGames + ) + self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame) + self._register_notification("install_game", self.install_game, feature=Feature.InstallGame) + self._register_notification( + "uninstall_game", + self.uninstall_game, + feature=Feature.UninstallGame + ) + self._register_method( + "import_friends", + self.get_friends, + result_name="user_info_list", + feature=Feature.ImportUsers + ) + self._register_method( + "import_user_infos", + self.get_users, + result_name="user_info_list", + feature=Feature.ImportUsers + ) + self._register_method( + "send_message", + self.send_message, + feature=Feature.Chat + ) + self._register_method( + "mark_as_read", + self.mark_as_read, + feature=Feature.Chat + ) + self._register_method( + "import_rooms", + self.get_rooms, + result_name="rooms", + feature=Feature.Chat + ) + self._register_method( + "import_room_history_from_message", + self.get_room_history_from_message, + result_name="messages", + feature=Feature.Chat + ) + self._register_method( + "import_room_history_from_timestamp", + self.get_room_history_from_timestamp, + result_name="messages", + feature=Feature.Chat + ) + + self._register_method( + "import_game_times", + self.get_game_times, + result_name="game_times", + feature=Feature.ImportGameTime + ) + + @property + def features(self): + features = [] + if self.__class__ != Plugin: + for feature, handlers in self._feature_methods.items(): + if self._implements(handlers): + features.append(feature) + + return features + + def _implements(self, handlers): + for handler in handlers: + if handler.__name__ not in self.__class__.__dict__: + return False + return True + + def _register_method(self, name, handler, result_name=None, internal=False, feature=None): + if internal: + def method(params): + result = handler(**params) + if result_name: + result = { + result_name: result + } + return result + self._server.register_method(name, method, True) + else: + async def method(params): + result = await handler(**params) + if result_name: + result = { + result_name: result + } + return result + self._server.register_method(name, method, False) + + if feature is not None: + self._feature_methods.setdefault(feature, []).append(handler) + + def _register_notification(self, name, handler, internal=False, feature=None): + self._server.register_notification(name, handler, internal) + + if feature is not None: + self._feature_methods.setdefault(feature, []).append(handler) + + async def run(self): + """Plugin main coorutine""" + async def pass_control(): + while self._active: + logging.debug("Passing control to plugin") + self.tick() + await asyncio.sleep(1) + + await asyncio.gather(pass_control(), self._server.run()) + + def _shutdown(self): + logging.info("Shuting down") + self._server.stop() + self._active = False + self.shutdown() + + def _get_capabilities(self): + return { + "platform_name": self._platform, + "features": self.features + } + + @staticmethod + def _ping(): + pass + + # notifications + def store_credentials(self, credentials): + """Notify client to store plugin credentials. + They will be pass to next authencicate calls. + """ + self._notification_client.notify("store_credentials", credentials) + + def add_game(self, game): + params = {"owned_game" : game} + self._notification_client.notify("owned_game_added", params) + + def remove_game(self, game_id): + params = {"game_id" : game_id} + self._notification_client.notify("owned_game_removed", params) + + def update_game(self, game): + params = {"owned_game" : game} + self._notification_client.notify("owned_game_updated", params) + + def unlock_achievement(self, achievement): + self._notification_client.notify("achievement_unlocked", achievement) + + def update_local_game_status(self, local_game): + params = {"local_game" : local_game} + self._notification_client.notify("local_game_status_changed", params) + + def add_friend(self, user): + params = {"user_info" : user} + self._notification_client.notify("friend_added", params) + + def remove_friend(self, user_id): + params = {"user_id" : user_id} + self._notification_client.notify("friend_removed", params) + + def update_friend(self, user): + params = {"user_info" : user} + self._notification_client.notify("friend_updated", params) + + def update_room(self, room_id, unread_message_count=None, new_messages=None): + params = {"room_id": room_id} + if unread_message_count is not None: + params["unread_message_count"] = unread_message_count + if new_messages is not None: + params["messages"] = new_messages + self._notification_client.notify("chat_room_updated", params) + + def update_game_time(self, game_time): + params = {"game_time" : game_time} + self._notification_client.notify("game_time_updated", params) + + # handlers + def tick(self): + """This method is called periodicaly. + Override it to implement periodical tasks like refreshing cache. + This method should not be blocking - any longer actions should be + handled by asycio tasks. + """ + + def shutdown(self): + """This method is called on plugin shutdown. + Override it to implement tear down. + """ + + # methods + async def authenticate(self, stored_credentials=None): + """Overide this method to handle plugin authentication. + The method should return one of: + - galaxy.api.types.AuthenticationSuccess - on successful authencication + - galaxy.api.types.NextStep - when more authentication steps are required + Or raise galaxy.api.types.LoginError on authentication failure. + """ + raise NotImplementedError() + + async def pass_login_credentials(self, step, credentials): + raise NotImplementedError() + + async def get_owned_games(self): + raise NotImplementedError() + + async def get_unlocked_achievements(self, game_id): + raise NotImplementedError() + + async def get_local_games(self): + raise NotImplementedError() + + async def launch_game(self, game_id): + raise NotImplementedError() + + async def install_game(self, game_id): + raise NotImplementedError() + + async def uninstall_game(self, game_id): + raise NotImplementedError() + + async def get_friends(self): + raise NotImplementedError() + + async def get_users(self, user_id_list): + raise NotImplementedError() + + async def send_message(self, room_id, message): + raise NotImplementedError() + + async def mark_as_read(self, room_id, last_message_id): + raise NotImplementedError() + + async def get_rooms(self): + raise NotImplementedError() + + async def get_room_history_from_message(self, room_id, message_id): + raise NotImplementedError() + + async def get_room_history_from_timestamp(self, room_id, from_timestamp): + raise NotImplementedError() + + async def get_game_times(self): + raise NotImplementedError() diff --git a/galaxy/api/stream.py b/galaxy/api/stream.py new file mode 100644 index 0000000..68ca84b --- /dev/null +++ b/galaxy/api/stream.py @@ -0,0 +1,35 @@ +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/galaxy/api/types.py b/galaxy/api/types.py new file mode 100644 index 0000000..3633edf --- /dev/null +++ b/galaxy/api/types.py @@ -0,0 +1,154 @@ +from dataclasses import dataclass +from typing import List + +from galaxy.api.jsonrpc import ApplicationError +from galaxy.api.consts import LocalGameState, PresenceState + +@dataclass +class AuthenticationSuccess(): + user_id: str + user_name: str + +@dataclass +class NextStep(): + next_step: str + auth_params: dict + +class LoginError(ApplicationError): + def __init__(self, current_step, reason): + data = { + "current_step": current_step, + "reason": reason + } + super().__init__(data) + +@dataclass +class LicenseInfo(): + license_type: str + owner: str = None + +@dataclass +class Dlc(): + dlc_id: str + dlc_title: str + license_info: LicenseInfo + +@dataclass +class Game(): + game_id: str + game_title: str + dlcs: List[Dlc] + license_info: LicenseInfo + +class GetGamesError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Achievement(): + achievement_id: str + unlock_time: int + +class GetAchievementsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class LocalGame(): + game_id: str + local_game_state: LocalGameState + +class GetLocalGamesError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Presence(): + presence_state: PresenceState + game_id: str = None + presence_status: str = None + +@dataclass +class UserInfo(): + user_id: str + is_friend: bool + user_name: str + avatar_url: str + presence: Presence + +class GetFriendsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class GetUsersError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class SendMessageError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +class MarkAsReadError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Room(): + room_id: str + unread_message_count: int + last_message_id: str + +class GetRoomsError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class Message(): + message_id: str + sender_id: str + sent_time: int + message_text: str + +class GetRoomHistoryError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) + +@dataclass +class GameTime(): + game_id: str + time_played: int + last_played_time: int + +class GetGameTimeError(ApplicationError): + def __init__(self, reason): + data = { + "reason": reason + } + super().__init__(data) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8fac6cf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +pytest==4.2.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2969b1e --- /dev/null +++ b/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup, find_packages + +setup( + name="galaxy.python.api", + version="0.1", + description="Galaxy python plugin API", + author='Galaxy team', + author_email='galaxy@gog.com', + packages=find_packages() +) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/async_mock.py b/tests/async_mock.py new file mode 100644 index 0000000..ecf4b30 --- /dev/null +++ b/tests/async_mock.py @@ -0,0 +1,6 @@ +from unittest.mock import MagicMock + +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + # pylint: disable=useless-super-delegation + return super(AsyncMock, self).__call__(*args, **kwargs) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..589b5f1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,58 @@ +from contextlib import ExitStack +import logging +from unittest.mock import patch + +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(): + """Return plugin instance with all feature methods mocked""" + async_methods = ( + "authenticate", + "pass_login_credentials", + "get_owned_games", + "get_unlocked_achievements", + "get_local_games", + "launch_game", + "install_game", + "uninstall_game", + "get_friends", + "get_users", + "send_message", + "mark_as_read", + "get_rooms", + "get_room_history_from_message", + "get_room_history_from_timestamp", + "get_game_times" + ) + + methods = ( + "shutdown", + "tick" + ) + + with ExitStack() as stack: + for method in async_methods: + 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 + +@pytest.fixture(autouse=True) +def my_caplog(caplog): + caplog.set_level(logging.DEBUG) diff --git a/tests/test_achievements.py b/tests/test_achievements.py new file mode 100644 index 0000000..72d4693 --- /dev/null +++ b/tests/test_achievements.py @@ -0,0 +1,85 @@ +import asyncio +import json + +from galaxy.api.types import Achievement, GetAchievementsError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_unlocked_achievements", + "params": { + "game_id": "14" + } + } + readline.side_effect = [json.dumps(request), ""] + plugin.get_unlocked_achievements.return_value = [ + Achievement("lvl10", 1548421241), + Achievement("lvl20", 1548422395) + ] + asyncio.run(plugin.run()) + plugin.get_unlocked_achievements.assert_called_with(game_id="14") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "unlocked_achievements": [ + { + "achievement_id": "lvl10", + "unlock_time": 1548421241 + }, + { + "achievement_id": "lvl20", + "unlock_time": 1548422395 + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_unlocked_achievements", + "params": { + "game_id": "14" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason") + asyncio.run(plugin.run()) + plugin.get_unlocked_achievements.assert_called() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_unlock_achievement(plugin, write): + achievement = Achievement("lvl20", 1548422395) + + async def couritine(): + plugin.unlock_achievement(achievement) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "achievement_unlocked", + "params": { + "achievement_id": "lvl20", + "unlock_time": 1548422395 + } + } diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py new file mode 100644 index 0000000..24006e8 --- /dev/null +++ b/tests/test_authenticate.py @@ -0,0 +1,86 @@ +import asyncio +import json + +from galaxy.api.types import AuthenticationSuccess, LoginError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "user_id": "132", + "user_name": "Zenek" + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.side_effect = LoginError("step", "reason") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "current_step": "step", + "reason": "reason" + } + } + } + +def test_stored_credentials(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "init_authentication", + "params": { + "stored_credentials": { + "token": "ABC" + } + } + } + readline.side_effect = [json.dumps(request), ""] + plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + asyncio.run(plugin.run()) + plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) + write.assert_called() + +def test_store_credentials(plugin, write): + credentials = { + "token": "ABC" + } + + async def couritine(): + plugin.store_credentials(credentials) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "store_credentials", + "params": credentials + } diff --git a/tests/test_chat.py b/tests/test_chat.py new file mode 100644 index 0000000..474955e --- /dev/null +++ b/tests/test_chat.py @@ -0,0 +1,336 @@ +import asyncio +import json + +from galaxy.api.types import ( + SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError +) + +def test_send_message_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "send_message", + "params": { + "room_id": "14", + "message": "Hello!" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.send_message.return_value = None + asyncio.run(plugin.run()) + plugin.send_message.assert_called_with(room_id="14", message="Hello!") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": None + } + +def test_send_message_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "6", + "method": "send_message", + "params": { + "room_id": "15", + "message": "Bye" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.send_message.side_effect = SendMessageError("reason") + asyncio.run(plugin.run()) + plugin.send_message.assert_called_with(room_id="15", message="Bye") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "6", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_mark_as_read_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "mark_as_read", + "params": { + "room_id": "14", + "last_message_id": "67" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.mark_as_read.return_value = None + asyncio.run(plugin.run()) + plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": None + } + +def test_mark_as_read_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "4", + "method": "mark_as_read", + "params": { + "room_id": "18", + "last_message_id": "7" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.mark_as_read.side_effect = MarkAsReadError("reason") + asyncio.run(plugin.run()) + plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_rooms_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "2", + "method": "import_rooms" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_rooms.return_value = [ + Room("13", 0, None), + Room("15", 34, "8") + ] + asyncio.run(plugin.run()) + plugin.get_rooms.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "2", + "result": { + "rooms": [ + { + "room_id": "13", + "unread_message_count": 0, + }, + { + "room_id": "15", + "unread_message_count": 34, + "last_message_id": "8" + } + ] + } + } + +def test_get_rooms_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "9", + "method": "import_rooms" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_rooms.side_effect = GetRoomsError("reason") + asyncio.run(plugin.run()) + plugin.get_rooms.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "9", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_room_history_from_message_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "2", + "method": "import_room_history_from_message", + "params": { + "room_id": "34", + "message_id": "66" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_message.return_value = [ + Message("13", "149", 1549454837, "Hello"), + Message("14", "812", 1549454899, "Hi") + ] + asyncio.run(plugin.run()) + plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "2", + "result": { + "messages": [ + { + "message_id": "13", + "sender_id": "149", + "sent_time": 1549454837, + "message_text": "Hello" + }, + { + "message_id": "14", + "sender_id": "812", + "sent_time": 1549454899, + "message_text": "Hi" + } + ] + } + } + +def test_get_room_history_from_message_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "import_room_history_from_message", + "params": { + "room_id": "33", + "message_id": "88" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason") + asyncio.run(plugin.run()) + plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_get_room_history_from_timestamp_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "import_room_history_from_timestamp", + "params": { + "room_id": "12", + "from_timestamp": 1549454835 + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_timestamp.return_value = [ + Message("12", "155", 1549454836, "Bye") + ] + asyncio.run(plugin.run()) + plugin.get_room_history_from_timestamp.assert_called_with( + room_id="12", + from_timestamp=1549454835 + ) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": { + "messages": [ + { + "message_id": "12", + "sender_id": "155", + "sent_time": 1549454836, + "message_text": "Bye" + } + ] + } + } + +def test_get_room_history_from_timestamp_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_room_history_from_timestamp", + "params": { + "room_id": "10", + "from_timestamp": 1549454800 + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason") + asyncio.run(plugin.run()) + plugin.get_room_history_from_timestamp.assert_called_with( + room_id="10", + from_timestamp=1549454800 + ) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_update_room(plugin, write): + messages = [ + Message("10", "898", 1549454832, "Hi") + ] + + async def couritine(): + plugin.update_room("14", 15, messages) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "chat_room_updated", + "params": { + "room_id": "14", + "unread_message_count": 15, + "messages": [ + { + "message_id": "10", + "sender_id": "898", + "sent_time": 1549454832, + "message_text": "Hi" + } + ] + } + } diff --git a/tests/test_features.py b/tests/test_features.py new file mode 100644 index 0000000..91034f3 --- /dev/null +++ b/tests/test_features.py @@ -0,0 +1,45 @@ +from galaxy.api.plugin import Plugin +from galaxy.api.consts import Platform, Feature + +def test_base_class(): + plugin = Plugin(Platform.Generic) + assert plugin.features == [] + +def test_no_overloads(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [] + +def test_one_method_feature(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def get_owned_games(self): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [Feature.ImportOwnedGames] + +def test_multiple_methods_feature_all(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def send_message(self, room_id, message): + pass + async def mark_as_read(self, room_id, last_message_id): + pass + async def get_rooms(self): + pass + async def get_room_history_from_message(self, room_id, message_id): + pass + async def get_room_history_from_timestamp(self, room_id, timestamp): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [Feature.Chat] + +def test_multiple_methods_feature_not_all(): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def send_message(self, room_id, message): + pass + + plugin = PluginImpl(Platform.Generic) + assert plugin.features == [] diff --git a/tests/test_game_times.py b/tests/test_game_times.py new file mode 100644 index 0000000..95559ce --- /dev/null +++ b/tests/test_game_times.py @@ -0,0 +1,85 @@ +import asyncio +import json + +from galaxy.api.types import GameTime, GetGameTimeError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_game_times" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_game_times.return_value = [ + GameTime("3", 60, 1549550504), + GameTime("5", 10, 1549550502) + ] + asyncio.run(plugin.run()) + plugin.get_game_times.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "game_times": [ + { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + }, + { + "game_id": "5", + "time_played": 10, + "last_played_time": 1549550502 + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_game_times" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_game_times.side_effect = GetGameTimeError("reason") + asyncio.run(plugin.run()) + plugin.get_game_times.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_update_game(plugin, write): + game_time = GameTime("3", 60, 1549550504) + + async def couritine(): + plugin.update_game_time(game_time) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_updated", + "params": { + "game_time": { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + } + } + } diff --git a/tests/test_install_game.py b/tests/test_install_game.py new file mode 100644 index 0000000..ca9c4d0 --- /dev/null +++ b/tests/test_install_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") diff --git a/tests/test_internal.py b/tests/test_internal.py new file mode 100644 index 0000000..0f935e5 --- /dev/null +++ b/tests/test_internal.py @@ -0,0 +1,66 @@ +import asyncio +import json + +from galaxy.api.plugin import Plugin +from galaxy.api.consts import Platform + +def test_get_capabilites(readline, write): + class PluginImpl(Plugin): #pylint: disable=abstract-method + async def get_owned_games(self): + pass + + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "get_capabilities" + } + plugin = PluginImpl(Platform.Generic) + readline.side_effect = [json.dumps(request), ""] + asyncio.run(plugin.run()) + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "platform_name": "generic", + "features": [ + "ImportOwnedGames" + ] + } + } + +def test_shutdown(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "5", + "method": "shutdown" + } + readline.side_effect = [json.dumps(request)] + asyncio.run(plugin.run()) + plugin.shutdown.assert_called_with() + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "5", + "result": None + } + +def test_ping(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "7", + "method": "ping" + } + readline.side_effect = [json.dumps(request), ""] + asyncio.run(plugin.run()) + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "7", + "result": None + } + +def test_tick(plugin, readline): + readline.side_effect = [""] + asyncio.run(plugin.run()) + plugin.tick.assert_called_with() diff --git a/tests/test_launch_game.py b/tests/test_launch_game.py new file mode 100644 index 0000000..fa654e9 --- /dev/null +++ b/tests/test_launch_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "launch_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.launch_game.assert_called_with(game_id="3") diff --git a/tests/test_local_games.py b/tests/test_local_games.py new file mode 100644 index 0000000..4e1e28e --- /dev/null +++ b/tests/test_local_games.py @@ -0,0 +1,84 @@ +import asyncio +import json + +from galaxy.api.types import GetLocalGamesError, LocalGame +from galaxy.api.consts import LocalGameState + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_local_games" + } + + readline.side_effect = [json.dumps(request), ""] + + plugin.get_local_games.return_value = [ + LocalGame("1", "Running"), + LocalGame("2", "Installed") + ] + asyncio.run(plugin.run()) + plugin.get_local_games.assert_called_with() + + response = json.loads(write.call_args[0][0]) + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "local_games" : [ + { + "game_id": "1", + "local_game_state": "Running" + }, + { + "game_id": "2", + "local_game_state": "Installed" + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_local_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_local_games.side_effect = GetLocalGamesError("reason") + asyncio.run(plugin.run()) + plugin.get_local_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_local_game_state_update(plugin, write): + game = LocalGame("1", LocalGameState.Running) + + async def couritine(): + plugin.update_local_game_status(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "local_game_status_changed", + "params": { + "local_game": { + "game_id": "1", + "local_game_state": "Running" + } + } + } diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py new file mode 100644 index 0000000..0d852e1 --- /dev/null +++ b/tests/test_owned_games.py @@ -0,0 +1,152 @@ +import asyncio +import json + +from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError + +def test_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_owned_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = [ + Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)), + Game( + "5", + "Witcher 3", + [ + Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)), + Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)), + ], + LicenseInfo("SinglePurchase", None)) + ] + asyncio.run(plugin.run()) + plugin.get_owned_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "owned_games": [ + { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + }, + { + "game_id": "5", + "game_title": "Witcher 3", + "dlcs": [ + { + "dlc_id": "7", + "dlc_title": "Hearts of Stone", + "license_info": { + "license_type": "SinglePurchase" + } + }, + { + "dlc_id": "8", + "dlc_title": "Temerian Armor Set", + "license_info": { + "license_type": "FreeToPlay" + } + } + ], + "license_info": { + "license_type": "SinglePurchase" + } + } + ] + } + } + +def test_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_owned_games" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.side_effect = GetGamesError("reason") + asyncio.run(plugin.run()) + plugin.get_owned_games.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_add_game(plugin, write): + game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + + async def couritine(): + plugin.add_game(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_added", + "params": { + "owned_game": { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + } + } + } + +def test_remove_game(plugin, write): + async def couritine(): + plugin.remove_game("5") + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_removed", + "params": { + "game_id": "5" + } + } + +def test_update_game(plugin, write): + game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + + async def couritine(): + plugin.update_game(game) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "owned_game_updated", + "params": { + "owned_game": { + "game_id": "3", + "game_title": "Doom", + "license_info": { + "license_type": "SinglePurchase" + } + } + } + } diff --git a/tests/test_uninstall_game.py b/tests/test_uninstall_game.py new file mode 100644 index 0000000..2e7c4ef --- /dev/null +++ b/tests/test_uninstall_game.py @@ -0,0 +1,16 @@ +import asyncio +import json + +def test_success(plugin, readline): + request = { + "jsonrpc": "2.0", + "method": "uninstall_game", + "params": { + "game_id": "3" + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.uninstall_game.assert_called_with(game_id="3") diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..50c4858 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,220 @@ +import asyncio +import json + +from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError +from galaxy.api.consts import PresenceState + +def test_get_friends_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.return_value = [ + UserInfo( + "3", + True, + "Jan", + "http://avatar1.png", + Presence( + PresenceState.Online, + "123", + "Main menu" + ) + ), + UserInfo( + "5", + True, + "Ola", + "http://avatar2.png", + Presence(PresenceState.Offline) + ) + ] + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "user_info_list": [ + { + "user_id": "3", + "is_friend": True, + "user_name": "Jan", + "avatar_url": "http://avatar1.png", + "presence": { + "presence_state": "online", + "game_id": "123", + "presence_status": "Main menu" + } + }, + { + "user_id": "5", + "is_friend": True, + "user_name": "Ola", + "avatar_url": "http://avatar2.png", + "presence": { + "presence_state": "offline" + } + } + ] + } + } + +def test_get_friends_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.side_effect = GetFriendsError("reason") + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } + +def test_add_friend(plugin, write): + friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline)) + + async def couritine(): + plugin.add_friend(friend) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_added", + "params": { + "user_info": { + "user_id": "7", + "is_friend": True, + "user_name": "Kuba", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + } + } + +def test_remove_friend(plugin, write): + async def couritine(): + plugin.remove_friend("5") + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_removed", + "params": { + "user_id": "5" + } + } + +def test_update_friend(plugin, write): + friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline)) + + async def couritine(): + plugin.update_friend(friend) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_updated", + "params": { + "user_info": { + "user_id": "9", + "is_friend": True, + "user_name": "Anna", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + } + } + +def test_get_users_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "8", + "method": "import_user_infos", + "params": { + "user_id_list": ["13"] + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_users.return_value = [ + UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) + ] + asyncio.run(plugin.run()) + plugin.get_users.assert_called_with(user_id_list=["13"]) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "8", + "result": { + "user_info_list": [ + { + "user_id": "5", + "is_friend": False, + "user_name": "Ula", + "avatar_url": "http://avatar.png", + "presence": { + "presence_state": "offline" + } + } + ] + } + } + +def test_get_users_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "12", + "method": "import_user_infos", + "params": { + "user_id_list": ["10", "11", "12"] + } + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_users.side_effect = GetUsersError("reason") + asyncio.run(plugin.run()) + plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "12", + "error": { + "code": -32003, + "message": "Custom error", + "data": { + "reason": "reason" + } + } + } From 68025644ffd78292ac42d10c41585f513b884984 Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Mon, 11 Feb 2019 12:02:02 +0100 Subject: [PATCH 003/147] SDK-2520: Configure CI --- .gitlab-ci.yml | 12 ++++++++++++ galaxy/api/__init__.py | 1 - pytest.ini | 2 ++ requirements.txt | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .gitlab-ci.yml create mode 100644 pytest.ini diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a814279 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,12 @@ +image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest + +stages: + - test + +test_package: + stage: test + script: + - pip install -r requirements.txt + - pytest + except: + - tags \ No newline at end of file diff --git a/galaxy/api/__init__.py b/galaxy/api/__init__.py index e876727..e69de29 100644 --- a/galaxy/api/__init__.py +++ b/galaxy/api/__init__.py @@ -1 +0,0 @@ -from galaxy.api.plugin import Plugin diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3d6dc59 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --flakes \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8fac6cf..9dee50a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ pytest==4.2.0 +pytest-flakes==4.0.0 From b7b759d483e5bdb661db85548f05ecee0e98cae8 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 14:30:08 +0100 Subject: [PATCH 004/147] SDK-2520: Configure deployment --- .gitlab-ci.yml | 13 ++++++++++++- setup.py | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a814279..23cc699 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,6 +2,7 @@ image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest stages: - test + - deploy test_package: stage: test @@ -9,4 +10,14 @@ test_package: - pip install -r requirements.txt - pytest except: - - tags \ No newline at end of file + - tags + +deploy_package: + stage: deploy + script: + - export VERSION=$(python setup.py --version) + - python setup.py sdist --formats=gztar upload -r gog-pypi + - 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}" + when: manual diff --git a/setup.py b/setup.py index 2969b1e..765b3e7 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup( - name="galaxy.python.api", + name="galaxy.plugin.api", version="0.1", description="Galaxy python plugin API", author='Galaxy team', From 94b8c8d1a0b0c305f15a1b067e348f396fc273ed Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 14:50:28 +0100 Subject: [PATCH 005/147] SDK-2520: Fix multiline command --- .gitlab-ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 23cc699..ea564bd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,6 +18,5 @@ deploy_package: - export VERSION=$(python setup.py --version) - python setup.py sdist --formats=gztar upload -r gog-pypi - 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 From 0b9b2dc8d3540ef04436fe9cf499a32842aa7033 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 15:09:23 +0100 Subject: [PATCH 006/147] SDK-2520: Add docs --- README.md | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8f0c6b..6c73bf9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,33 @@ -# galaxy-plugin-api +# Galaxy python plugin API +## Usage + +Implement plugin: + +```python +import asyncio +from galaxy.api.plugin import Plugin + +class PluginExample(Plugin): + # implement methods + async def authenticate(self, stored_credentials=None): + pass + +# run plugin event loop +if __name__ == "__main__": + asyncio.run(MockPlugin().run()) +``` + +Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale. + +## Development + +Install required packages: +```bash +pip install -r requirments.txt +``` + +Run tests: +```bash +pytest +``` \ No newline at end of file From 20143e3b4f40e6cb3fc01408baf7f6ec1b3e3923 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 15:11:18 +0100 Subject: [PATCH 007/147] SDK-2520: Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c73bf9..130296d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale. Install required packages: ```bash -pip install -r requirments.txt +pip install -r requirements.txt ``` Run tests: From 00fe3dd5534008524d73cbe73abd9882158b4f62 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Feb 2019 15:12:57 +0100 Subject: [PATCH 008/147] SDK-2520: Skipt deployment for tags --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ea564bd..8b8d47c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,3 +20,5 @@ deploy_package: - 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}" when: manual + except: + - tags \ No newline at end of file From 0d52b3dda66533b57f29fa8139c0d8b0a9009bd5 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 12 Feb 2019 17:55:28 +0100 Subject: [PATCH 009/147] SDK-2526: Refactor authentication --- galaxy/api/plugin.py | 10 ++-------- galaxy/api/types.py | 7 +------ tests/conftest.py | 1 - tests/test_authenticate.py | 6 +++--- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index f2b8bbb..273ff69 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -44,7 +44,6 @@ class Plugin(): # implemented by developer self._register_method("init_authentication", self.authenticate) - self._register_method("pass_login_credentials", self.pass_login_credentials) self._register_method( "import_owned_games", self.get_owned_games, @@ -255,16 +254,11 @@ class Plugin(): # methods async def authenticate(self, stored_credentials=None): """Overide this method to handle plugin authentication. - The method should return one of: - - galaxy.api.types.AuthenticationSuccess - on successful authencication - - galaxy.api.types.NextStep - when more authentication steps are required - Or raise galaxy.api.types.LoginError on authentication failure. + The method should return galaxy.api.types.Authentication + or raise galaxy.api.types.LoginError on authentication failure. """ raise NotImplementedError() - async def pass_login_credentials(self, step, credentials): - raise NotImplementedError() - async def get_owned_games(self): raise NotImplementedError() diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 3633edf..899f682 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -5,15 +5,10 @@ from galaxy.api.jsonrpc import ApplicationError from galaxy.api.consts import LocalGameState, PresenceState @dataclass -class AuthenticationSuccess(): +class Authentication(): user_id: str user_name: str -@dataclass -class NextStep(): - next_step: str - auth_params: dict - class LoginError(ApplicationError): def __init__(self, current_step, reason): data = { diff --git a/tests/conftest.py b/tests/conftest.py index 589b5f1..a3d9d53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,6 @@ def plugin(): """Return plugin instance with all feature methods mocked""" async_methods = ( "authenticate", - "pass_login_credentials", "get_owned_games", "get_unlocked_achievements", "get_local_games", diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 24006e8..c12266d 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -1,7 +1,7 @@ import asyncio import json -from galaxy.api.types import AuthenticationSuccess, LoginError +from galaxy.api.types import Authentication, LoginError def test_success(plugin, readline, write): request = { @@ -11,7 +11,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + plugin.authenticate.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -63,7 +63,7 @@ def test_stored_credentials(plugin, readline, write): } } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek") + plugin.authenticate.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) write.assert_called() From 3e34edf5e7cad5d4a170af83e674658f23ead4e6 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 13 Feb 2019 08:23:07 +0100 Subject: [PATCH 010/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 765b3e7..8e88cf4 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.1", + version="0.2", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 4cec6c09b2e572b868477a65831f0facbc26aa8e Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Wed, 13 Feb 2019 10:22:36 +0100 Subject: [PATCH 011/147] SDK-2524: Fix achievement notification --- galaxy/api/plugin.py | 8 ++++++-- tests/test_achievements.py | 9 ++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 273ff69..ce09698 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -207,8 +207,12 @@ class Plugin(): params = {"owned_game" : game} self._notification_client.notify("owned_game_updated", params) - def unlock_achievement(self, achievement): - self._notification_client.notify("achievement_unlocked", achievement) + def unlock_achievement(self, game_id, achievement): + params = { + "game_id": game_id, + "achievement": achievement + } + self._notification_client.notify("achievement_unlocked", params) def update_local_game_status(self, local_game): params = {"local_game" : local_game} diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 72d4693..eb5d441 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -70,7 +70,7 @@ def test_unlock_achievement(plugin, write): achievement = Achievement("lvl20", 1548422395) async def couritine(): - plugin.unlock_achievement(achievement) + plugin.unlock_achievement("14", achievement) asyncio.run(couritine()) response = json.loads(write.call_args[0][0]) @@ -79,7 +79,10 @@ def test_unlock_achievement(plugin, write): "jsonrpc": "2.0", "method": "achievement_unlocked", "params": { - "achievement_id": "lvl20", - "unlock_time": 1548422395 + "game_id": "14", + "achievement": { + "achievement_id": "lvl20", + "unlock_time": 1548422395 + } } } From d78c08ae4bc79657a8ad426256960928c80b0351 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Wed, 13 Feb 2019 10:40:08 +0100 Subject: [PATCH 012/147] add authentication lost notification --- galaxy/api/plugin.py | 3 +++ tests/test_authenticate.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index ce09698..14a2152 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -242,6 +242,9 @@ class Plugin(): params = {"game_time" : game_time} self._notification_client.notify("game_time_updated", params) + def lost_authentication(self): + self._notification_client.notify("authentication_lost", None) + # handlers def tick(self): """This method is called periodicaly. diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index c12266d..72ad82a 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -84,3 +84,17 @@ def test_store_credentials(plugin, write): "method": "store_credentials", "params": credentials } + +def test_lost_authentication(plugin, readline, write): + + async def couritine(): + plugin.lost_authentication() + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "authentication_lost", + "params": None + } From c6d5c55dfdc44dc7904fe9b7e41044ebf1dbc933 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 13 Feb 2019 12:53:25 +0100 Subject: [PATCH 013/147] SDK-2525: Refactor errors --- galaxy/api/errors.py | 73 +++++++++++++++++++++++++++++++++++ galaxy/api/jsonrpc.py | 21 ++++++++-- galaxy/api/types.py | 79 -------------------------------------- tests/test_achievements.py | 12 +++--- tests/test_authenticate.py | 37 +++++++++++++----- tests/test_chat.py | 75 +++++++++++++++++++----------------- tests/test_game_times.py | 12 +++--- tests/test_local_games.py | 23 +++++++---- tests/test_owned_games.py | 12 +++--- tests/test_users.py | 21 ++++------ 10 files changed, 195 insertions(+), 170 deletions(-) create mode 100644 galaxy/api/errors.py diff --git a/galaxy/api/errors.py b/galaxy/api/errors.py new file mode 100644 index 0000000..d918fc7 --- /dev/null +++ b/galaxy/api/errors.py @@ -0,0 +1,73 @@ +from galaxy.api.jsonrpc import ApplicationError + +class UnknownError(ApplicationError): + def __init__(self, data=None): + super().__init__(0, "Unknown error", data) + +class InvalidCredentials(ApplicationError): + def __init__(self, data=None): + super().__init__(100, "Invalid credentials", data) + +class NetworkError(ApplicationError): + def __init__(self, data=None): + super().__init__(101, "Network error", data) + +class LoggedInElsewhere(ApplicationError): + def __init__(self, data=None): + super().__init__(102, "Logged in elsewhere", data) + +class ProtocolError(ApplicationError): + def __init__(self, data=None): + super().__init__(103, "Protocol error", data) + +class BackendNotAvailable(ApplicationError): + def __init__(self, data=None): + super().__init__(104, "Backend not available", data) + +class BackendTimeout(ApplicationError): + def __init__(self, data=None): + super().__init__(105, "Backend timed out", data) + +class BackendError(ApplicationError): + def __init__(self, data=None): + super().__init__(106, "Backend error", data) + +class TemporaryBlocked(ApplicationError): + def __init__(self, data=None): + super().__init__(107, "Temporary blocked", data) + +class Banned(ApplicationError): + def __init__(self, data=None): + super().__init__(108, "Banned", data) + +class AccessDenied(ApplicationError): + def __init__(self, data=None): + super().__init__(109, "Access denied", data) + +class ParentalControlBlock(ApplicationError): + def __init__(self, data=None): + super().__init__(110, "Parental control block", data) + +class DeviceBlocked(ApplicationError): + def __init__(self, data=None): + super().__init__(111, "Device blocked", data) + +class RegionBlocked(ApplicationError): + def __init__(self, data=None): + super().__init__(112, "Region blocked", data) + +class FailedParsingManifest(ApplicationError): + def __init__(self, data=None): + super().__init__(200, "Failed parsing manifest", data) + +class TooManyMessagesSent(ApplicationError): + def __init__(self, data=None): + super().__init__(300, "Too many messages sent", data) + +class IncoherentLastMessage(ApplicationError): + def __init__(self, data=None): + super().__init__(400, "Different last message id on backend", data) + +class MessageNotFound(ApplicationError): + def __init__(self, data=None): + super().__init__(500, "Message not found", data) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index f1cfe41..e7449c3 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -26,9 +26,19 @@ class InvalidParams(JsonRpcError): def __init__(self): super().__init__(-32601, "Invalid params") +class Timeout(JsonRpcError): + def __init__(self): + super().__init__(-32000, "Method timed out") + +class Aborted(JsonRpcError): + def __init__(self): + super().__init__(-32001, "Method aborted") + class ApplicationError(JsonRpcError): - def __init__(self, data): - super().__init__(-32003, "Custom error", data) + def __init__(self, code, message, data): + if code >= -32768 and code <= -32000: + raise ValueError("The error code in reserved range") + super().__init__(code, message, data) Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) Method = namedtuple("Method", ["callback", "internal"]) @@ -172,10 +182,13 @@ class Server(): "id": request_id, "error": { "code": error.code, - "message": error.message, - "data": error.data + "message": error.message } } + + if error.data is not None: + response["error"]["data"] = error.data + self._send(response) class NotificationClient(): diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 899f682..0b73ba8 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -1,7 +1,6 @@ from dataclasses import dataclass from typing import List -from galaxy.api.jsonrpc import ApplicationError from galaxy.api.consts import LocalGameState, PresenceState @dataclass @@ -9,14 +8,6 @@ class Authentication(): user_id: str user_name: str -class LoginError(ApplicationError): - def __init__(self, current_step, reason): - data = { - "current_step": current_step, - "reason": reason - } - super().__init__(data) - @dataclass class LicenseInfo(): license_type: str @@ -35,37 +26,16 @@ class Game(): dlcs: List[Dlc] license_info: LicenseInfo -class GetGamesError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class Achievement(): achievement_id: str unlock_time: int -class GetAchievementsError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class LocalGame(): game_id: str local_game_state: LocalGameState -class GetLocalGamesError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class Presence(): presence_state: PresenceState @@ -80,47 +50,12 @@ class UserInfo(): avatar_url: str presence: Presence -class GetFriendsError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - -class GetUsersError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - -class SendMessageError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - -class MarkAsReadError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class Room(): room_id: str unread_message_count: int last_message_id: str -class GetRoomsError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class Message(): message_id: str @@ -128,22 +63,8 @@ class Message(): sent_time: int message_text: str -class GetRoomHistoryError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) - @dataclass class GameTime(): game_id: str time_played: int last_played_time: int - -class GetGameTimeError(ApplicationError): - def __init__(self, reason): - data = { - "reason": reason - } - super().__init__(data) \ No newline at end of file diff --git a/tests/test_achievements.py b/tests/test_achievements.py index eb5d441..cf5b2d7 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -1,7 +1,8 @@ import asyncio import json -from galaxy.api.types import Achievement, GetAchievementsError +from galaxy.api.types import Achievement +from galaxy.api.errors import UnknownError def test_success(plugin, readline, write): request = { @@ -49,7 +50,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason") + plugin.get_unlocked_achievements.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called() response = json.loads(write.call_args[0][0]) @@ -58,11 +59,8 @@ def test_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error" } } diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 72ad82a..6a8ca26 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -1,7 +1,14 @@ import asyncio import json -from galaxy.api.types import Authentication, LoginError +import pytest + +from galaxy.api.types import Authentication +from galaxy.api.errors import ( + UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError, + BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied, + ParentalControlBlock, DeviceBlocked, RegionBlocked +) def test_success(plugin, readline, write): request = { @@ -25,7 +32,23 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +@pytest.mark.parametrize("error,code,message", [ + pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"), + pytest.param(NetworkError, 101, "Network error", id="network_error"), + pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"), + pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"), + pytest.param(BackendNotAvailable, 104, "Backend not available", id="backend_not_available"), + pytest.param(BackendTimeout, 105, "Backend timed out", id="backend_timeout"), + pytest.param(BackendError, 106, "Backend error", id="backend_error"), + pytest.param(TemporaryBlocked, 107, "Temporary blocked", id="temporary_blocked"), + pytest.param(Banned, 108, "Banned", id="banned"), + pytest.param(AccessDenied, 109, "Access denied", id="access_denied"), + pytest.param(ParentalControlBlock, 110, "Parental control block", id="parental_control_clock"), + pytest.param(DeviceBlocked, 111, "Device blocked", id="device_blocked"), + pytest.param(RegionBlocked, 112, "Region blocked", id="region_blocked") +]) +def test_failure(plugin, readline, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", @@ -33,7 +56,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.side_effect = LoginError("step", "reason") + plugin.authenticate.side_effect = error() asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -42,12 +65,8 @@ def test_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "current_step": "step", - "reason": "reason" - } + "code": code, + "message": message } } diff --git a/tests/test_chat.py b/tests/test_chat.py index 474955e..9493458 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -1,9 +1,10 @@ import asyncio import json -from galaxy.api.types import ( - SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError -) +import pytest + +from galaxy.api.types import Room, Message +from galaxy.api.errors import UnknownError, TooManyMessagesSent, IncoherentLastMessage, MessageNotFound def test_send_message_success(plugin, readline, write): request = { @@ -28,7 +29,11 @@ def test_send_message_success(plugin, readline, write): "result": None } -def test_send_message_failure(plugin, readline, write): +@pytest.mark.parametrize("error,code,message", [ + pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages") +]) +def test_send_message_failure(plugin, readline, write, error, code, message): request = { "jsonrpc": "2.0", "id": "6", @@ -40,7 +45,7 @@ def test_send_message_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.send_message.side_effect = SendMessageError("reason") + plugin.send_message.side_effect = error() asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="15", message="Bye") response = json.loads(write.call_args[0][0]) @@ -49,11 +54,8 @@ def test_send_message_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "6", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": code, + "message": message } } @@ -80,7 +82,16 @@ def test_mark_as_read_success(plugin, readline, write): "result": None } -def test_mark_as_read_failure(plugin, readline, write): +@pytest.mark.parametrize("error,code,message", [ + pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param( + IncoherentLastMessage, + 400, + "Different last message id on backend", + id="incoherent_last_message" + ) +]) +def test_mark_as_read_failure(plugin, readline, write, error, code, message): request = { "jsonrpc": "2.0", "id": "4", @@ -92,7 +103,7 @@ def test_mark_as_read_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.mark_as_read.side_effect = MarkAsReadError("reason") + plugin.mark_as_read.side_effect = error() asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") response = json.loads(write.call_args[0][0]) @@ -101,11 +112,8 @@ def test_mark_as_read_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "4", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": code, + "message": message } } @@ -151,7 +159,7 @@ def test_get_rooms_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_rooms.side_effect = GetRoomsError("reason") + plugin.get_rooms.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_rooms.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -160,11 +168,8 @@ def test_get_rooms_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "9", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error" } } @@ -209,7 +214,11 @@ def test_get_room_history_from_message_success(plugin, readline, write): } } -def test_get_room_history_from_message_failure(plugin, readline, write): +@pytest.mark.parametrize("error,code,message", [ + pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found") +]) +def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message): request = { "jsonrpc": "2.0", "id": "7", @@ -221,7 +230,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason") + plugin.get_room_history_from_message.side_effect = error() asyncio.run(plugin.run()) plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") response = json.loads(write.call_args[0][0]) @@ -230,11 +239,8 @@ def test_get_room_history_from_message_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "7", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": code, + "message": message } } @@ -287,7 +293,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason") + plugin.get_room_history_from_timestamp.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_room_history_from_timestamp.assert_called_with( room_id="10", @@ -299,11 +305,8 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error" } } diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 95559ce..3c5690a 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -1,7 +1,8 @@ import asyncio import json -from galaxy.api.types import GameTime, GetGameTimeError +from galaxy.api.types import GameTime +from galaxy.api.errors import UnknownError def test_success(plugin, readline, write): request = { @@ -46,7 +47,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_game_times.side_effect = GetGameTimeError("reason") + plugin.get_game_times.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -55,11 +56,8 @@ def test_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error", } } diff --git a/tests/test_local_games.py b/tests/test_local_games.py index 4e1e28e..a243b33 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -1,8 +1,11 @@ import asyncio import json -from galaxy.api.types import GetLocalGamesError, LocalGame +import pytest + +from galaxy.api.types import LocalGame from galaxy.api.consts import LocalGameState +from galaxy.api.errors import UnknownError, FailedParsingManifest def test_success(plugin, readline, write): request = { @@ -38,7 +41,14 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +@pytest.mark.parametrize( + "error,code,message", + [ + pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing") + ], +) +def test_failure(plugin, readline, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", @@ -46,7 +56,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_local_games.side_effect = GetLocalGamesError("reason") + plugin.get_local_games.side_effect = error() asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -55,11 +65,8 @@ def test_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": code, + "message": message } } diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index 0d852e1..d0c8db9 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -1,7 +1,8 @@ import asyncio import json -from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError +from galaxy.api.types import Game, Dlc, LicenseInfo +from galaxy.api.errors import UnknownError def test_success(plugin, readline, write): request = { @@ -73,7 +74,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_owned_games.side_effect = GetGamesError("reason") + plugin.get_owned_games.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -82,11 +83,8 @@ def test_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error" } } diff --git a/tests/test_users.py b/tests/test_users.py index 50c4858..f4bf775 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -1,7 +1,8 @@ import asyncio import json -from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError +from galaxy.api.types import UserInfo, Presence +from galaxy.api.errors import UnknownError from galaxy.api.consts import PresenceState def test_get_friends_success(plugin, readline, write): @@ -73,7 +74,7 @@ def test_get_friends_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.side_effect = GetFriendsError("reason") + plugin.get_friends.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_friends.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -82,11 +83,8 @@ def test_get_friends_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "3", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error", } } @@ -202,7 +200,7 @@ def test_get_users_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_users.side_effect = GetUsersError("reason") + plugin.get_users.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) response = json.loads(write.call_args[0][0]) @@ -211,10 +209,7 @@ def test_get_users_failure(plugin, readline, write): "jsonrpc": "2.0", "id": "12", "error": { - "code": -32003, - "message": "Custom error", - "data": { - "reason": "reason" - } + "code": 0, + "message": "Unknown error" } } From 9f3df6aee3a0ffb0484f0617126762b4e90d16c4 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 15 Feb 2019 10:16:26 +0100 Subject: [PATCH 014/147] SDK-2525: Add AuthenticationRequired error, change codes --- galaxy/api/errors.py | 40 +++++++++++++++++++++----------------- tests/test_authenticate.py | 18 ++++++++--------- tests/test_chat.py | 17 +++++++++++++++- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/galaxy/api/errors.py b/galaxy/api/errors.py index d918fc7..24800c8 100644 --- a/galaxy/api/errors.py +++ b/galaxy/api/errors.py @@ -4,6 +4,22 @@ class UnknownError(ApplicationError): def __init__(self, data=None): super().__init__(0, "Unknown error", data) +class AuthenticationRequired(ApplicationError): + def __init__(self, data=None): + super().__init__(1, "Authentication required", data) + +class BackendNotAvailable(ApplicationError): + def __init__(self, data=None): + super().__init__(2, "Backend not available", data) + +class BackendTimeout(ApplicationError): + def __init__(self, data=None): + super().__init__(3, "Backend timed out", data) + +class BackendError(ApplicationError): + def __init__(self, data=None): + super().__init__(4, "Backend error", data) + class InvalidCredentials(ApplicationError): def __init__(self, data=None): super().__init__(100, "Invalid credentials", data) @@ -20,41 +36,29 @@ class ProtocolError(ApplicationError): def __init__(self, data=None): super().__init__(103, "Protocol error", data) -class BackendNotAvailable(ApplicationError): - def __init__(self, data=None): - super().__init__(104, "Backend not available", data) - -class BackendTimeout(ApplicationError): - def __init__(self, data=None): - super().__init__(105, "Backend timed out", data) - -class BackendError(ApplicationError): - def __init__(self, data=None): - super().__init__(106, "Backend error", data) - class TemporaryBlocked(ApplicationError): def __init__(self, data=None): - super().__init__(107, "Temporary blocked", data) + super().__init__(104, "Temporary blocked", data) class Banned(ApplicationError): def __init__(self, data=None): - super().__init__(108, "Banned", data) + super().__init__(105, "Banned", data) class AccessDenied(ApplicationError): def __init__(self, data=None): - super().__init__(109, "Access denied", data) + super().__init__(106, "Access denied", data) class ParentalControlBlock(ApplicationError): def __init__(self, data=None): - super().__init__(110, "Parental control block", data) + super().__init__(107, "Parental control block", data) class DeviceBlocked(ApplicationError): def __init__(self, data=None): - super().__init__(111, "Device blocked", data) + super().__init__(108, "Device blocked", data) class RegionBlocked(ApplicationError): def __init__(self, data=None): - super().__init__(112, "Region blocked", data) + super().__init__(109, "Region blocked", data) class FailedParsingManifest(ApplicationError): def __init__(self, data=None): diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 6a8ca26..c11abf2 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -34,19 +34,19 @@ def test_success(plugin, readline, write): @pytest.mark.parametrize("error,code,message", [ pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"), + pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"), + pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"), pytest.param(NetworkError, 101, "Network error", id="network_error"), pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"), pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"), - pytest.param(BackendNotAvailable, 104, "Backend not available", id="backend_not_available"), - pytest.param(BackendTimeout, 105, "Backend timed out", id="backend_timeout"), - pytest.param(BackendError, 106, "Backend error", id="backend_error"), - pytest.param(TemporaryBlocked, 107, "Temporary blocked", id="temporary_blocked"), - pytest.param(Banned, 108, "Banned", id="banned"), - pytest.param(AccessDenied, 109, "Access denied", id="access_denied"), - pytest.param(ParentalControlBlock, 110, "Parental control block", id="parental_control_clock"), - pytest.param(DeviceBlocked, 111, "Device blocked", id="device_blocked"), - pytest.param(RegionBlocked, 112, "Region blocked", id="region_blocked") + pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"), + pytest.param(Banned, 105, "Banned", id="banned"), + pytest.param(AccessDenied, 106, "Access denied", id="access_denied"), + pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"), + pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"), + pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked") ]) def test_failure(plugin, readline, write, error, code, message): request = { diff --git a/tests/test_chat.py b/tests/test_chat.py index 9493458..a9fbd13 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -4,7 +4,10 @@ import json import pytest from galaxy.api.types import Room, Message -from galaxy.api.errors import UnknownError, TooManyMessagesSent, IncoherentLastMessage, MessageNotFound +from galaxy.api.errors import ( + UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError, + TooManyMessagesSent, IncoherentLastMessage, MessageNotFound +) def test_send_message_success(plugin, readline, write): request = { @@ -31,6 +34,10 @@ def test_send_message_success(plugin, readline, write): @pytest.mark.parametrize("error,code,message", [ pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"), + pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"), + pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"), + pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages") ]) def test_send_message_failure(plugin, readline, write, error, code, message): @@ -84,6 +91,10 @@ def test_mark_as_read_success(plugin, readline, write): @pytest.mark.parametrize("error,code,message", [ pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"), + pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"), + pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"), + pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param( IncoherentLastMessage, 400, @@ -216,6 +227,10 @@ def test_get_room_history_from_message_success(plugin, readline, write): @pytest.mark.parametrize("error,code,message", [ pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"), + pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"), + pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"), + pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"), + pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found") ]) def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message): From f025d9f93c90ffd879d406a2f61bb856b5bdc144 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 15 Feb 2019 10:16:38 +0100 Subject: [PATCH 015/147] SDK-2525: Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8e88cf4..dd4e048 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.2", + version="0.3", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 853ecf1d3b24ecd327855c33be9685d1457ecb02 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 19 Feb 2019 16:53:10 +0100 Subject: [PATCH 016/147] Make galaxy namespace package --- galaxy/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 galaxy/__init__.py diff --git a/galaxy/__init__.py b/galaxy/__init__.py deleted file mode 100644 index e69de29..0000000 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 017/147] 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 } } From 48e54a8460c15fcee3368abcec080ec88797f76e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Wed, 20 Feb 2019 14:09:34 +0100 Subject: [PATCH 018/147] Revert "Make galaxy namespace package" --- galaxy/__init__.py | 0 setup.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 galaxy/__init__.py diff --git a/galaxy/__init__.py b/galaxy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index 153fe1c..057c8b5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.4", + version="0.5", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 1614fd6eb2e6f74bd67fcad3e2e9a15c8a41dd2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Wed, 20 Feb 2019 16:41:44 +0100 Subject: [PATCH 019/147] Fix end of stream detecting --- galaxy/api/jsonrpc.py | 10 ++++++---- setup.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index d9df245..6e431da 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -64,10 +64,12 @@ class Server(): async def run(self): while self._active: - data = await self._reader.readline() - if not data: - # on windows rederecting a pipe to stdin result on continues - # not-blocking return of empty line on EOF + try: + data = await self._reader.readline() + if not data: + self._eof() + continue + except: self._eof() continue data = data.strip() diff --git a/setup.py b/setup.py index 057c8b5..073b615 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.5", + version="0.6", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From c2a053416294fe6d1fe7bd4cb123d3fe57c9b02a Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 20 Feb 2019 16:44:53 +0100 Subject: [PATCH 020/147] Deploy only from master --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8b8d47c..5e346af 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,5 +20,7 @@ deploy_package: - 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}" when: manual + only: + - master except: - tags \ No newline at end of file From d69e1aaa0839a9005ee288710531246733e6056e Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 21 Feb 2019 15:11:49 +0100 Subject: [PATCH 021/147] SDK-2538: Add LicenseType enum --- galaxy/api/consts.py | 6 ++++++ galaxy/api/types.py | 4 ++-- tests/test_owned_games.py | 13 +++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index 056ba1f..c6969fd 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -23,6 +23,12 @@ class Feature(Enum): ImportUsers = "ImportUsers" VerifyGame = "VerifyGame" +class LicenseType(Enum): + SinglePurchase = "SinglePurchase" + FreeToPlay = "FreeToPlay" + OtherUserLicense = "OtherUserLicense" + Unknown = "Unknown" + class LocalGameState(Enum): Installed = "Installed" Running = "Running" diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 0b73ba8..0ed08a3 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import List -from galaxy.api.consts import LocalGameState, PresenceState +from galaxy.api.consts import LicenseType, LocalGameState, PresenceState @dataclass class Authentication(): @@ -10,7 +10,7 @@ class Authentication(): @dataclass class LicenseInfo(): - license_type: str + license_type: LicenseType owner: str = None @dataclass diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index d0c8db9..ac5cc7c 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -2,6 +2,7 @@ import asyncio import json from galaxy.api.types import Game, Dlc, LicenseInfo +from galaxy.api.consts import LicenseType from galaxy.api.errors import UnknownError def test_success(plugin, readline, write): @@ -13,15 +14,15 @@ def test_success(plugin, readline, write): readline.side_effect = [json.dumps(request), ""] plugin.get_owned_games.return_value = [ - Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)), + Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)), Game( "5", "Witcher 3", [ - Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)), - Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)), + Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)), + Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)), ], - LicenseInfo("SinglePurchase", None)) + LicenseInfo(LicenseType.SinglePurchase, None)) ] asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() @@ -89,7 +90,7 @@ def test_failure(plugin, readline, write): } def test_add_game(plugin, write): - game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)) async def couritine(): plugin.add_game(game) @@ -127,7 +128,7 @@ def test_remove_game(plugin, write): } def test_update_game(plugin, write): - game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) + game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)) async def couritine(): plugin.update_game(game) From bc7d1c291487656601926b405af6bb3c818f6cd7 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 21 Feb 2019 15:17:38 +0100 Subject: [PATCH 022/147] SDK-2538: Use Optional --- galaxy/api/types.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 0ed08a3..ec92f54 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List +from typing import List, Optional from galaxy.api.consts import LicenseType, LocalGameState, PresenceState @@ -11,7 +11,7 @@ class Authentication(): @dataclass class LicenseInfo(): license_type: LicenseType - owner: str = None + owner: Optional[str] = None @dataclass class Dlc(): @@ -39,8 +39,8 @@ class LocalGame(): @dataclass class Presence(): presence_state: PresenceState - game_id: str = None - presence_status: str = None + game_id: Optional[str] = None + presence_status: Optional[str] = None @dataclass class UserInfo(): From 6c0389834b0a76e9405d4829031c4ac6ed08810b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 21 Feb 2019 15:29:39 +0100 Subject: [PATCH 023/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 073b615..87f73ee 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.6", + version="0.7", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From a114c9721c6aa0762df194223404433afc8ac43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Fri, 22 Feb 2019 11:26:17 +0100 Subject: [PATCH 024/147] Add Unknown for all enums --- galaxy/api/consts.py | 11 +++++++---- setup.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index c6969fd..d359a1e 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -12,6 +12,7 @@ class Platform(Enum): Battlenet = "battlenet" class Feature(Enum): + Unknown = "Unknown" ImportInstalledGames = "ImportInstalledGames" ImportOwnedGames = "ImportOwnedGames" LaunchGame = "LaunchGame" @@ -24,16 +25,18 @@ class Feature(Enum): VerifyGame = "VerifyGame" class LicenseType(Enum): - SinglePurchase = "SinglePurchase" - FreeToPlay = "FreeToPlay" - OtherUserLicense = "OtherUserLicense" - Unknown = "Unknown" + Unknown = "Unknown" + SinglePurchase = "SinglePurchase" + FreeToPlay = "FreeToPlay" + OtherUserLicense = "OtherUserLicense" class LocalGameState(Enum): + Unknown = "Unknown" Installed = "Installed" Running = "Running" class PresenceState(Enum): + Unknown = "Unknown" Online = "online" Offline = "offline" Away = "away" diff --git a/setup.py b/setup.py index 87f73ee..f6c1dc2 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.7", + version="0.8", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From d6e6efc63334974fda12a917945c8d736687180a Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Thu, 28 Feb 2019 10:31:12 +0100 Subject: [PATCH 025/147] SDK-2571: Refactor logging --- galaxy/api/jsonrpc.py | 4 ++-- galaxy/api/plugin.py | 42 +++++++++++++++++++++++++++++++++--------- setup.py | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 6e431da..9dadb48 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -143,8 +143,8 @@ class Server(): self._send_error(request.id, MethodNotFound()) except JsonRpcError as error: self._send_error(request.id, error) - except Exception as error: #pylint: disable=broad-except - logging.error("Unexpected exception raised in plugin handler: %s", repr(error)) + except Exception: #pylint: disable=broad-except + logging.exception("Unexpected exception raised in plugin handler") asyncio.create_task(handle()) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index fd19f88..a7e35af 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -1,9 +1,11 @@ import asyncio import json import logging +import logging.handlers import dataclasses from enum import Enum from collections import OrderedDict +import sys from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.consts import Feature @@ -21,6 +23,7 @@ class JSONEncoder(json.JSONEncoder): class Plugin(): def __init__(self, platform, reader, writer, handshake_token): + logging.info("Creating plugin for platform %s", platform.value) self._platform = platform self._feature_methods = OrderedDict() @@ -167,7 +170,10 @@ class Plugin(): async def pass_control(): while self._active: logging.debug("Passing control to plugin") - self.tick() + try: + self.tick() + except Exception: + logging.exception("Unexpected exception raised in plugin tick") await asyncio.sleep(1) await asyncio.gather(pass_control(), self._server.run()) @@ -309,22 +315,40 @@ 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") + root = logging.getLogger() + root.setLevel(logging.DEBUG) + if len(argv) >= 4: + handler = logging.handlers.RotatingFileHandler(argv[3], "a", 10000000, 10) + root.addHandler(handler) + if len(argv) < 3: - raise ValueError("Not enough parameters, required: token, port") + logging.critical("Not enough parameters, required: token, port") + sys.exit(1) + token = argv[1] + try: port = int(argv[2]) - except ValueError as e: - raise ValueError("Failed to parse port value, {}".format(e)) + except ValueError: + logging.critical("Failed to parse port value: %s", argv[2]) + sys.exit(2) + if not (1 <= port <= 65535): - raise ValueError("Port value out of range (1, 65535)") + logging.critical("Port value out of range (1, 65535)") + sys.exit(3) + + if not issubclass(plugin_class, Plugin): + logging.critical("plugin_class must be subclass of Plugin") + sys.exit(4) 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()) + + try: + asyncio.run(coroutine()) + except Exception: + logging.exception("Error while running plugin") + sys.exit(5) diff --git a/setup.py b/setup.py index f6c1dc2..3510b19 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.8", + version="0.9", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From e244d3bb44ec323be78920d7b3a33434e3c0741b Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Thu, 28 Feb 2019 15:20:03 +0100 Subject: [PATCH 026/147] SDK-2577: Add UnknownBackendResponse --- galaxy/api/errors.py | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/galaxy/api/errors.py b/galaxy/api/errors.py index 24800c8..1099a16 100644 --- a/galaxy/api/errors.py +++ b/galaxy/api/errors.py @@ -20,6 +20,10 @@ class BackendError(ApplicationError): def __init__(self, data=None): super().__init__(4, "Backend error", data) +class UnknownBackendResponse(ApplicationError): + def __init__(self, data=None): + super().__init__(4, "Backend responded in uknown way", data) + class InvalidCredentials(ApplicationError): def __init__(self, data=None): super().__init__(100, "Invalid credentials", data) diff --git a/setup.py b/setup.py index 3510b19..e79381f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.9", + version="0.10", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 43556a047086d56e3764c3ce222fa6914cbfaae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Fri, 1 Mar 2019 14:10:48 +0100 Subject: [PATCH 027/147] SDK-2586 Return "None" instead of "Unknown" state for local game for Origin --- galaxy/api/consts.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index d359a1e..b1c7933 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -31,7 +31,7 @@ class LicenseType(Enum): OtherUserLicense = "OtherUserLicense" class LocalGameState(Enum): - Unknown = "Unknown" + None_ = "None" Installed = "Installed" Running = "Running" diff --git a/setup.py b/setup.py index e79381f..16e4fb9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.10", + version="0.11", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 300ade5d432432ea880452e58185011b443a7baa Mon Sep 17 00:00:00 2001 From: Pawel Kierski Date: Mon, 4 Mar 2019 11:52:08 +0100 Subject: [PATCH 028/147] Fix handling unknown notification --- galaxy/api/jsonrpc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 9dadb48..90989c5 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -104,6 +104,7 @@ class Server(): method = self._notifications.get(request.method) if not method: logging.error("Received uknown notification: %s", request.method) + return callback, internal = method if internal: From 059a1ea3431cea762da02f01d03ffc5adf8702a9 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Tue, 5 Mar 2019 09:36:44 +0100 Subject: [PATCH 029/147] update logging facility in plugin API --- galaxy/api/plugin.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index a7e35af..536f342 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -315,12 +315,23 @@ class Plugin(): async def get_game_times(self): raise NotImplementedError() -def create_and_run_plugin(plugin_class, argv): +def _prepare_logging(logger_file): root = logging.getLogger() root.setLevel(logging.DEBUG) - if len(argv) >= 4: - handler = logging.handlers.RotatingFileHandler(argv[3], "a", 10000000, 10) - root.addHandler(handler) + handler = logging.handlers.RotatingFileHandler( + logger_file, + mode="a", + maxBytes=10000000, + backupCount=10, + encoding="utf-8" + ) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + root.addHandler(handler) + +def create_and_run_plugin(plugin_class, argv): + logger_file = argv[3] if len(argv) >= 4 else "output.log" + _prepare_logging(logger_file) if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") From 788d2550e6449087a9c1795d13ced05a82ea18fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Tue, 5 Mar 2019 12:18:33 +0100 Subject: [PATCH 030/147] SDK-2552 optional achievement id or name --- galaxy/api/types.py | 7 ++++++- setup.py | 2 +- tests/test_achievements.py | 23 +++++++++++++++++++---- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/galaxy/api/types.py b/galaxy/api/types.py index ec92f54..20827da 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -28,8 +28,13 @@ class Game(): @dataclass class Achievement(): - achievement_id: str unlock_time: int + achievement_id: Optional[str] = None + achievement_name: Optional[str] = None + + def __post_init__(self): + assert self.achievement_id or self.achievement_name, \ + "One of achievement_id or achievement_name is required" @dataclass class LocalGame(): diff --git a/setup.py b/setup.py index 16e4fb9..6604796 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.11", + version="0.12", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', diff --git a/tests/test_achievements.py b/tests/test_achievements.py index cf5b2d7..1c8c9e2 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -1,9 +1,18 @@ import asyncio import json +from pytest import raises from galaxy.api.types import Achievement from galaxy.api.errors import UnknownError +def test_initialization_no_unlock_time(): + with raises(Exception): + Achievement(achievement_id="lvl30", achievement_name="Got level 30") + +def test_initialization_no_id_nor_name(): + with raises(AssertionError): + Achievement(unlock_time=1234567890) + def test_success(plugin, readline, write): request = { "jsonrpc": "2.0", @@ -15,8 +24,9 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] plugin.get_unlocked_achievements.return_value = [ - Achievement("lvl10", 1548421241), - Achievement("lvl20", 1548422395) + Achievement(achievement_id="lvl10", unlock_time=1548421241), + Achievement(achievement_name="Got level 20", unlock_time=1548422395), + Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633) ] asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called_with(game_id="14") @@ -32,8 +42,13 @@ def test_success(plugin, readline, write): "unlock_time": 1548421241 }, { - "achievement_id": "lvl20", + "achievement_name": "Got level 20", "unlock_time": 1548422395 + }, + { + "achievement_id": "lvl30", + "achievement_name": "Got level 30", + "unlock_time": 1548495633 } ] } @@ -65,7 +80,7 @@ def test_failure(plugin, readline, write): } def test_unlock_achievement(plugin, write): - achievement = Achievement("lvl20", 1548422395) + achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395) async def couritine(): plugin.unlock_achievement("14", achievement) From 67e7a4c0b2ca38be9acd3c53cfedebfe603c7206 Mon Sep 17 00:00:00 2001 From: Pawel Kierski Date: Tue, 5 Mar 2019 11:28:23 +0100 Subject: [PATCH 031/147] Don't create log file if not specified --- galaxy/api/plugin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 536f342..853d860 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -318,19 +318,22 @@ class Plugin(): def _prepare_logging(logger_file): root = logging.getLogger() root.setLevel(logging.DEBUG) - handler = logging.handlers.RotatingFileHandler( - logger_file, - mode="a", - maxBytes=10000000, - backupCount=10, - encoding="utf-8" - ) + if logger_file: + handler = logging.handlers.RotatingFileHandler( + logger_file, + mode="a", + maxBytes=10000000, + backupCount=10, + encoding="utf-8" + ) + else: + handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) root.addHandler(handler) def create_and_run_plugin(plugin_class, argv): - logger_file = argv[3] if len(argv) >= 4 else "output.log" + logger_file = argv[3] if len(argv) >= 4 else None _prepare_logging(logger_file) if len(argv) < 3: From 88e25a93bec44a850c9b4eb5035a90199f3514ce Mon Sep 17 00:00:00 2001 From: Pawel Kierski Date: Wed, 6 Mar 2019 11:15:33 +0100 Subject: [PATCH 032/147] Ensure log folder exists --- galaxy/api/plugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 853d860..cef489e 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -6,6 +6,7 @@ import dataclasses from enum import Enum from collections import OrderedDict import sys +import os from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.consts import Feature @@ -319,6 +320,8 @@ def _prepare_logging(logger_file): root = logging.getLogger() root.setLevel(logging.DEBUG) if logger_file: + # ensure destination folder exists + os.makedirs(os.path.dirname(os.path.abspath(logger_file)), exist_ok=True) handler = logging.handlers.RotatingFileHandler( logger_file, mode="a", From 6885cdc4391d844a68fb906cf273c531cd773244 Mon Sep 17 00:00:00 2001 From: Pawel Kierski Date: Wed, 6 Mar 2019 14:13:45 +0100 Subject: [PATCH 033/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6604796..845e3d9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.12", + version="0.13", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From bda867473cd787ec9b9b80b60f44bbc6193f3720 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 7 Mar 2019 13:18:26 +0100 Subject: [PATCH 034/147] SDK-2627: Add version param to Plugin --- galaxy/api/plugin.py | 5 +++-- tests/conftest.py | 2 +- tests/test_features.py | 10 +++++----- tests/test_internal.py | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index cef489e..1fb4bb4 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -23,9 +23,10 @@ class JSONEncoder(json.JSONEncoder): return super().default(o) class Plugin(): - def __init__(self, platform, reader, writer, handshake_token): - logging.info("Creating plugin for platform %s", platform.value) + def __init__(self, platform, version, reader, writer, handshake_token): + logging.info("Creating plugin for platform %s, version %s", platform.value, version) self._platform = platform + self._version = version self._feature_methods = OrderedDict() self._active = True diff --git a/tests/conftest.py b/tests/conftest.py index 9c120f2..6b9cc51 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,7 +60,7 @@ def plugin(reader, writer): 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, reader, writer, "token") + yield Plugin(Platform.Generic, "0.1", reader, writer, "token") @pytest.fixture(autouse=True) def my_caplog(caplog): diff --git a/tests/test_features.py b/tests/test_features.py index d307bb7..7f11c17 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, None, None, None) + plugin = Plugin(Platform.Generic, "0.1", None, None, None) assert plugin.features == [] def test_no_overloads(): class PluginImpl(Plugin): #pylint: disable=abstract-method pass - plugin = PluginImpl(Platform.Generic, None, None, None) + plugin = PluginImpl(Platform.Generic, "0.1", 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, None, None, None) + plugin = PluginImpl(Platform.Generic, "0.1", 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, None, None, None) + plugin = PluginImpl(Platform.Generic, "0.1", 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, None, None, None) + plugin = PluginImpl(Platform.Generic, "0.1", None, None, None) assert plugin.features == [] diff --git a/tests/test_internal.py b/tests/test_internal.py index 3837c0e..44f8ee7 100644 --- a/tests/test_internal.py +++ b/tests/test_internal.py @@ -15,7 +15,7 @@ def test_get_capabilites(reader, writer, readline, write): "method": "get_capabilities" } token = "token" - plugin = PluginImpl(Platform.Generic, reader, writer, token) + plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token) readline.side_effect = [json.dumps(request), ""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) From 52273e2f8cbc02c84a778a42780faaa8a0699dcd Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 7 Mar 2019 13:19:37 +0100 Subject: [PATCH 035/147] Increment veresion, add changelog --- README.md | 7 ++++++- setup.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 130296d..30b70c7 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,9 @@ pip install -r requirements.txt Run tests: ```bash pytest -``` \ No newline at end of file +``` + +## Changelog + +### 0.14 +* Added required version parameter to Plugin constructor. \ No newline at end of file diff --git a/setup.py b/setup.py index 845e3d9..6e0d847 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.13", + version="0.14", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 73bc9aa8ecbbe5aec2d0c6c421f9fb966f277c60 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 8 Mar 2019 10:16:57 +0100 Subject: [PATCH 036/147] SDK-2623: Call shutdown on socket close --- galaxy/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 1fb4bb4..a4e5f77 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -39,7 +39,7 @@ class Plugin(): self._notification_client = NotificationClient(self._writer, encoder) def eof_handler(): - self._active = False + self._shutdown() self._server.register_eof(eof_handler) # internal From ca8d0dfaf4a4ec430782e7aca10d9469366dd8f1 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 8 Mar 2019 10:19:43 +0100 Subject: [PATCH 037/147] Increment version, add changelog --- README.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 30b70c7..1012890 100644 --- a/README.md +++ b/README.md @@ -34,5 +34,7 @@ pytest ## Changelog +### 0.15 +* `shutdown()` is called on socket disconnection. ### 0.14 * Added required version parameter to Plugin constructor. \ No newline at end of file diff --git a/setup.py b/setup.py index 6e0d847..f3024ae 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.14", + version="0.15", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From f09171672f13140976adb7c2a43560da2a5408d9 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Mar 2019 11:07:31 +0100 Subject: [PATCH 038/147] SDK-2636: Do not log sensitive data --- galaxy/api/jsonrpc.py | 98 ++++++++++++++++++++++++++++++++----------- galaxy/api/plugin.py | 14 +++---- 2 files changed, 81 insertions(+), 31 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 90989c5..a00f72d 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -1,5 +1,6 @@ import asyncio from collections import namedtuple +from collections.abc import Iterable import logging import json @@ -41,7 +42,21 @@ class ApplicationError(JsonRpcError): super().__init__(code, message, data) Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) -Method = namedtuple("Method", ["callback", "internal"]) +Method = namedtuple("Method", ["callback", "internal", "sensitive_params"]) + +def anonymise_sensitive_params(params, sensitive_params): + anomized_data = "****" + if not sensitive_params: + return params + + if isinstance(sensitive_params, Iterable): + anomized_params = params.copy() + for key in anomized_params.keys(): + if key in sensitive_params: + anomized_params[key] = anomized_data + return anomized_params + + return anomized_data class Server(): def __init__(self, reader, writer, encoder=json.JSONEncoder()): @@ -53,11 +68,27 @@ class Server(): self._notifications = {} self._eof_listeners = [] - def register_method(self, name, callback, internal): - self._methods[name] = Method(callback, internal) + def register_method(self, name, callback, internal, sensitive_params=False): + """ + Register method + :param name: + :param callback: + :param internal: if True the callback will be processed immediately (synchronously) + :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params + are considered sensitive, if True - all params are considered sensitive + """ + self._methods[name] = Method(callback, internal, sensitive_params) - def register_notification(self, name, callback, internal): - self._notifications[name] = Method(callback, internal) + def register_notification(self, name, callback, internal, sensitive_params=False): + """ + Register notification + :param name: + :param callback: + :param internal: if True the callback will be processed immediately (synchronously) + :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params + are considered sensitive, if True - all params are considered sensitive + """ + self._notifications[name] = Method(callback, internal, sensitive_params) def register_eof(self, callback): self._eof_listeners.append(callback) @@ -73,7 +104,7 @@ class Server(): self._eof() continue data = data.strip() - logging.debug("Received data: %s", data) + logging.debug("Received %d bytes of data", len(data)) self._handle_input(data) def stop(self): @@ -92,43 +123,39 @@ class Server(): self._send_error(None, error) return - logging.debug("Parsed input: %s", request) - if request.id is not None: self._handle_request(request) else: self._handle_notification(request) def _handle_notification(self, request): - logging.debug("Handling notification %s", request) method = self._notifications.get(request.method) if not method: - logging.error("Received uknown notification: %s", request.method) + logging.error("Received unknown notification: %s", request.method) return - callback, internal = method + callback, internal, sensitive_params = method + self._log_request(request, sensitive_params) + if internal: # internal requests are handled immediately callback(**request.params) else: try: asyncio.create_task(callback(**request.params)) - except Exception as error: #pylint: disable=broad-except - logging.error( - "Unexpected exception raised in notification handler: %s", - repr(error) - ) + except Exception: + logging.exception("Unexpected exception raised in notification handler") def _handle_request(self, request): - logging.debug("Handling request %s", request) method = self._methods.get(request.method) - if not method: - logging.error("Received uknown request: %s", request.method) + logging.error("Received unknown request: %s", request.method) self._send_error(request.id, MethodNotFound()) return - callback, internal = method + callback, internal, sensitive_params = method + self._log_request(request, sensitive_params) + if internal: # internal requests are handled immediately response = callback(request.params) @@ -166,7 +193,8 @@ class Server(): try: line = self._encoder.encode(data) logging.debug("Sending data: %s", line) - self._writer.write((line + "\n").encode("utf-8")) + data = (line + "\n").encode("utf-8") + self._writer.write(data) asyncio.create_task(self._writer.drain()) except TypeError as error: logging.error(str(error)) @@ -194,25 +222,47 @@ class Server(): self._send(response) + @staticmethod + def _log_request(request, sensitive_params): + params = anonymise_sensitive_params(request.params, sensitive_params) + if request.id is not None: + logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params) + else: + logging.info("Handling notification: method=%s, params=%s", request.method, params) + class NotificationClient(): def __init__(self, writer, encoder=json.JSONEncoder()): self._writer = writer self._encoder = encoder self._methods = {} - def notify(self, method, params): + def notify(self, method, params, sensitive_params=False): + """ + Send notification + :param method: + :param params: + :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params + are considered sensitive, if True - all params are considered sensitive + """ notification = { "jsonrpc": "2.0", "method": method, "params": params } + self._log(method, params, sensitive_params) self._send(notification) def _send(self, data): try: line = self._encoder.encode(data) - logging.debug("Sending data: %s", line) - self._writer.write((line + "\n").encode("utf-8")) + data = (line + "\n").encode("utf-8") + logging.debug("Sending %d byte of data", len(data)) + self._writer.write(data) asyncio.create_task(self._writer.drain()) except TypeError as error: logging.error("Failed to parse outgoing message: %s", str(error)) + + @staticmethod + def _log(method, params, sensitive_params): + params = anonymise_sensitive_params(params, sensitive_params) + logging.info("Sending notification: method=%s, params=%s", method, params) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index a4e5f77..c917fc4 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -48,7 +48,7 @@ class Plugin(): self._register_method("ping", self._ping, internal=True) # implemented by developer - self._register_method("init_authentication", self.authenticate) + self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"]) self._register_method( "import_owned_games", self.get_owned_games, @@ -138,7 +138,7 @@ class Plugin(): return False return True - def _register_method(self, name, handler, result_name=None, internal=False, feature=None): + def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None): if internal: def method(params): result = handler(**params) @@ -147,7 +147,7 @@ class Plugin(): result_name: result } return result - self._server.register_method(name, method, True) + self._server.register_method(name, method, True, sensitive_params) else: async def method(params): result = await handler(**params) @@ -156,13 +156,13 @@ class Plugin(): result_name: result } return result - self._server.register_method(name, method, False) + self._server.register_method(name, method, False, sensitive_params) if feature is not None: self._feature_methods.setdefault(feature, []).append(handler) - def _register_notification(self, name, handler, internal=False, feature=None): - self._server.register_notification(name, handler, internal) + def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None): + self._server.register_notification(name, handler, internal, sensitive_params) if feature is not None: self._feature_methods.setdefault(feature, []).append(handler) @@ -202,7 +202,7 @@ class Plugin(): """Notify client to store plugin credentials. They will be pass to next authencicate calls. """ - self._notification_client.notify("store_credentials", credentials) + self._notification_client.notify("store_credentials", credentials, sensitive_params=True) def add_game(self, game): params = {"owned_game" : game} From f9eaeaf7269fd285613ade31de6ca3f2df615d8c Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Mar 2019 11:08:28 +0100 Subject: [PATCH 039/147] Increment version, add changelog --- README.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1012890..4bd7533 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ pytest ## Changelog +### 0.16 +* Do not log sensitive data. ### 0.15 * `shutdown()` is called on socket disconnection. ### 0.14 diff --git a/setup.py b/setup.py index f3024ae..8330390 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.15", + version="0.16", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 9a06428fc01622a23596b11b7aa5dc58851c93c8 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 11 Mar 2019 12:10:35 +0100 Subject: [PATCH 040/147] Remove log --- galaxy/api/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index c917fc4..53aed38 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -171,7 +171,6 @@ class Plugin(): """Plugin main coorutine""" async def pass_control(): while self._active: - logging.debug("Passing control to plugin") try: self.tick() except Exception: From ca778e2cdb95b4b00283896ab208f5fa21c0c7ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Mon, 11 Mar 2019 13:47:12 +0100 Subject: [PATCH 041/147] SDK-2647 Serializing local game state as integer --- README.md | 1 + galaxy/api/consts.py | 10 +++++----- tests/test_local_games.py | 15 ++++++++++----- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4bd7533..011ba0a 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ pytest ### 0.16 * Do not log sensitive data. +* Return `LocalGameState` as int (possible combination of flags). ### 0.15 * `shutdown()` is called on socket disconnection. ### 0.14 diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index b1c7933..14cfa59 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -1,4 +1,4 @@ -from enum import Enum +from enum import Enum, Flag class Platform(Enum): Unknown = "unknown" @@ -30,10 +30,10 @@ class LicenseType(Enum): FreeToPlay = "FreeToPlay" OtherUserLicense = "OtherUserLicense" -class LocalGameState(Enum): - None_ = "None" - Installed = "Installed" - Running = "Running" +class LocalGameState(Flag): + None_ = 0 + Installed = 1 + Running = 2 class PresenceState(Enum): Unknown = "Unknown" diff --git a/tests/test_local_games.py b/tests/test_local_games.py index a243b33..c97e8d2 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -17,8 +17,9 @@ def test_success(plugin, readline, write): readline.side_effect = [json.dumps(request), ""] plugin.get_local_games.return_value = [ - LocalGame("1", "Running"), - LocalGame("2", "Installed") + LocalGame("1", LocalGameState.Running), + LocalGame("2", LocalGameState.Installed), + LocalGame("3", LocalGameState.Installed | LocalGameState.Running) ] asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() @@ -31,11 +32,15 @@ def test_success(plugin, readline, write): "local_games" : [ { "game_id": "1", - "local_game_state": "Running" + "local_game_state": LocalGameState.Running.value }, { "game_id": "2", - "local_game_state": "Installed" + "local_game_state": LocalGameState.Installed.value + }, + { + "game_id": "3", + "local_game_state": (LocalGameState.Installed | LocalGameState.Running).value } ] } @@ -85,7 +90,7 @@ def test_local_game_state_update(plugin, write): "params": { "local_game": { "game_id": "1", - "local_game_state": "Running" + "local_game_state": LocalGameState.Running.value } } } From 833e6999d72b29ad482d36a712a59def1edb160a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Kierski?= Date: Tue, 12 Mar 2019 15:38:22 +0100 Subject: [PATCH 042/147] Return JSON-RPC reponse on generic Exception --- galaxy/api/errors.py | 6 ++---- galaxy/api/jsonrpc.py | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/galaxy/api/errors.py b/galaxy/api/errors.py index 1099a16..3e1921d 100644 --- a/galaxy/api/errors.py +++ b/galaxy/api/errors.py @@ -1,8 +1,6 @@ -from galaxy.api.jsonrpc import ApplicationError +from galaxy.api.jsonrpc import ApplicationError, UnknownError -class UnknownError(ApplicationError): - def __init__(self, data=None): - super().__init__(0, "Unknown error", data) +UnknownError = UnknownError class AuthenticationRequired(ApplicationError): def __init__(self, data=None): diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index a00f72d..033c04c 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -41,6 +41,10 @@ class ApplicationError(JsonRpcError): raise ValueError("The error code in reserved range") super().__init__(code, message, data) +class UnknownError(ApplicationError): + def __init__(self, data=None): + super().__init__(0, "Unknown error", data) + Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) Method = namedtuple("Method", ["callback", "internal", "sensitive_params"]) @@ -171,8 +175,9 @@ class Server(): self._send_error(request.id, MethodNotFound()) except JsonRpcError as error: self._send_error(request.id, error) - except Exception: #pylint: disable=broad-except + except Exception as e: #pylint: disable=broad-except logging.exception("Unexpected exception raised in plugin handler") + self._send_error(request.id, UnknownError(str(e))) asyncio.create_task(handle()) From e06e40f845d811f6c3c57b3e10f27f02452e0177 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Tue, 12 Mar 2019 15:53:42 +0100 Subject: [PATCH 043/147] Fix duplicated error code --- galaxy/api/jsonrpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 033c04c..cc9d6e4 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -25,7 +25,7 @@ class MethodNotFound(JsonRpcError): class InvalidParams(JsonRpcError): def __init__(self): - super().__init__(-32601, "Invalid params") + super().__init__(-32602, "Invalid params") class Timeout(JsonRpcError): def __init__(self): From d73d048ff77a52c97f708aab138517498c8ee29e Mon Sep 17 00:00:00 2001 From: Pawel Kierski Date: Tue, 12 Mar 2019 16:10:23 +0100 Subject: [PATCH 044/147] Increment version to 0.17 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8330390..1e6e15e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.16", + version="0.17", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 958d9bc0e675c7b77351cce14cd4707bb98c4af9 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Mon, 25 Mar 2019 11:46:30 +0100 Subject: [PATCH 045/147] Fix send_message message param name --- galaxy/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 53aed38..eaf8678 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -298,7 +298,7 @@ class Plugin(): async def get_users(self, user_id_list): raise NotImplementedError() - async def send_message(self, room_id, message): + async def send_message(self, room_id, message_text): raise NotImplementedError() async def mark_as_read(self, room_id, last_message_id): From 00ed52384af6866eda40fdcf84f3d58d0ff344a0 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 27 Mar 2019 15:14:29 +0100 Subject: [PATCH 046/147] Exclude tests from package --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1e6e15e..85c0a91 100644 --- a/setup.py +++ b/setup.py @@ -6,5 +6,5 @@ setup( description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', - packages=find_packages() + packages=find_packages(exclude=["tests"]) ) From e09e443064d3994dad8e069f90a2967e7c4f60cd Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 27 Mar 2019 15:15:25 +0100 Subject: [PATCH 047/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 85c0a91..d090c73 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.17", + version="0.18", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 9b333978271b89d0af7ce51c4015586a424c68a6 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 12 Mar 2019 16:53:57 +0100 Subject: [PATCH 048/147] Add NextStep and pass_login_credentials --- galaxy/api/plugin.py | 4 ++++ galaxy/api/types.py | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index eaf8678..9370b4d 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -49,6 +49,7 @@ class Plugin(): # implemented by developer self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"]) + self._register_method("pass_login_credentials", self.pass_login_credentials) self._register_method( "import_owned_games", self.get_owned_games, @@ -274,6 +275,9 @@ class Plugin(): """ raise NotImplementedError() + async def pass_login_credentials(self, step, credentials, cookies): + raise NotImplementedError() + async def get_owned_games(self): raise NotImplementedError() diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 20827da..ea835e0 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Optional +from typing import List, Dict, Optional from galaxy.api.consts import LicenseType, LocalGameState, PresenceState @@ -8,6 +8,11 @@ class Authentication(): user_id: str user_name: str +@dataclass +class NextStep(): + next_step: str + auth_params: Dict[str, str] + @dataclass class LicenseInfo(): license_type: LicenseType From d759b4aa85693ff027881d46895320dbe23bfd40 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Thu, 28 Mar 2019 14:37:30 +0100 Subject: [PATCH 049/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d090c73..4b1a849 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.18", + version="0.19", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 4d62b8ccb80673648e8a8df9ae9843cfe3de5349 Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Fri, 5 Apr 2019 14:24:14 +0200 Subject: [PATCH 050/147] SDK-2743: Remove logging setup --- galaxy/api/plugin.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index 9370b4d..d6ee78d 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -6,7 +6,6 @@ import dataclasses from enum import Enum from collections import OrderedDict import sys -import os from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.consts import Feature @@ -320,29 +319,7 @@ class Plugin(): async def get_game_times(self): raise NotImplementedError() -def _prepare_logging(logger_file): - root = logging.getLogger() - root.setLevel(logging.DEBUG) - if logger_file: - # ensure destination folder exists - os.makedirs(os.path.dirname(os.path.abspath(logger_file)), exist_ok=True) - handler = logging.handlers.RotatingFileHandler( - logger_file, - mode="a", - maxBytes=10000000, - backupCount=10, - encoding="utf-8" - ) - else: - handler = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - root.addHandler(handler) - def create_and_run_plugin(plugin_class, argv): - logger_file = argv[3] if len(argv) >= 4 else None - _prepare_logging(logger_file) - if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") sys.exit(1) From a4b08f81055369f0fe5b1e5f9ba767edddee3426 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Sat, 6 Apr 2019 14:32:31 +0200 Subject: [PATCH 051/147] Add Cookies to NextStep --- galaxy/api/types.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/galaxy/api/types.py b/galaxy/api/types.py index ea835e0..be310d4 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -8,10 +8,18 @@ class Authentication(): user_id: str user_name: str +@dataclass +class Cookie(): + name: str + value: str + domain: Optional[str] = None + path: Optional[str] = None + @dataclass class NextStep(): next_step: str auth_params: Dict[str, str] + cookies: Optional[List[Cookie]] = None @dataclass class LicenseInfo(): From 23ef34bed51060aa1f45cfeaa3cfec292811d441 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Sat, 6 Apr 2019 15:00:41 +0200 Subject: [PATCH 052/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4b1a849..a8a5aab 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.19", + version="0.20", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 3071c2e7714587e90799843692d66d7d0624336b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 12 Apr 2019 13:51:41 +0200 Subject: [PATCH 053/147] SDK-2760: Add Epic platform --- galaxy/api/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index 14cfa59..4e595b2 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -10,6 +10,7 @@ class Platform(Enum): Origin = "origin" Uplay = "uplay" Battlenet = "battlenet" + Epic = "epic" class Feature(Enum): Unknown = "Unknown" From 403736612adad2226a748c9b2c8ce1785f181f2b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 12 Apr 2019 13:52:09 +0200 Subject: [PATCH 054/147] Increment version, add changelog --- README.md | 2 ++ setup.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 011ba0a..825e160 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ pytest ## Changelog +### 0.21 +* Add `Epic` platform. ### 0.16 * Do not log sensitive data. * Return `LocalGameState` as int (possible combination of flags). diff --git a/setup.py b/setup.py index a8a5aab..e4d7748 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.20", + version="0.21", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 25b850d8bb09994cf472517e56c799cebc9c8e11 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:21:31 +0200 Subject: [PATCH 055/147] SDK-2760: Dlc list optional --- galaxy/api/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy/api/types.py b/galaxy/api/types.py index be310d4..574d427 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -36,7 +36,7 @@ class Dlc(): class Game(): game_id: str game_title: str - dlcs: List[Dlc] + dlcs: Optional[List[Dlc]] license_info: LicenseInfo @dataclass From f2e2e41d0436bdb8a0a0115c5ca6e46f0a37b90c Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:24:32 +0200 Subject: [PATCH 056/147] SDK-2760: Add tools module --- galaxy/api/tools.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 galaxy/api/tools.py diff --git a/galaxy/api/tools.py b/galaxy/api/tools.py new file mode 100644 index 0000000..3996d25 --- /dev/null +++ b/galaxy/api/tools.py @@ -0,0 +1,20 @@ +import io +import os +import zipfile +from glob import glob + +def zip_folder(folder): + files = glob(os.path.join(folder, "**"), recursive=True) + files = [file.replace(folder + os.sep, "") for file in files] + files = [file for file in files if file] + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf: + for file in files: + zipf.write(os.path.join(folder, file), arcname=file) + return zip_buffer + +def zip_folder_to_file(folder, filename): + zip_content = zip_folder(folder).getbuffer() + with open(filename, "wb") as archive: + archive.write(zip_content) From 7c4f3fba5b100646f4f9fadad758e1f14697cb0f Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:26:37 +0200 Subject: [PATCH 057/147] SDK-2760: Add mock module --- galaxy/api/mock.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 galaxy/api/mock.py diff --git a/galaxy/api/mock.py b/galaxy/api/mock.py new file mode 100644 index 0000000..f475ebd --- /dev/null +++ b/galaxy/api/mock.py @@ -0,0 +1,5 @@ +from unittest.mock import MagicMock + +class AsyncMock(MagicMock): + async def __call__(self, *args, **kwargs): + return super(AsyncMock, self).__call__(*args, **kwargs) From c591efc4935af6167bda53902729b0d3130f489d Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:27:07 +0200 Subject: [PATCH 058/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e4d7748..15bef57 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.21", + version="0.22", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 1430fe39d7af08330c51105e1ab247551c45c933 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:33:05 +0200 Subject: [PATCH 059/147] SDK-2760: Make galaxy namespace package --- galaxy/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 galaxy/__init__.py diff --git a/galaxy/__init__.py b/galaxy/__init__.py deleted file mode 100644 index e69de29..0000000 From 4adef2dace58d340107383b38ccbff875f521b80 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 10:38:45 +0200 Subject: [PATCH 060/147] SDK-2760: Move modules --- galaxy/{api => }/tools.py | 0 galaxy/unittest/__init__.py | 0 galaxy/{api => unittest}/mock.py | 0 tests/async_mock.py | 6 ------ 4 files changed, 6 deletions(-) rename galaxy/{api => }/tools.py (100%) create mode 100644 galaxy/unittest/__init__.py rename galaxy/{api => unittest}/mock.py (100%) delete mode 100644 tests/async_mock.py diff --git a/galaxy/api/tools.py b/galaxy/tools.py similarity index 100% rename from galaxy/api/tools.py rename to galaxy/tools.py diff --git a/galaxy/unittest/__init__.py b/galaxy/unittest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/galaxy/api/mock.py b/galaxy/unittest/mock.py similarity index 100% rename from galaxy/api/mock.py rename to galaxy/unittest/mock.py diff --git a/tests/async_mock.py b/tests/async_mock.py deleted file mode 100644 index ecf4b30..0000000 --- a/tests/async_mock.py +++ /dev/null @@ -1,6 +0,0 @@ -from unittest.mock import MagicMock - -class AsyncMock(MagicMock): - async def __call__(self, *args, **kwargs): - # pylint: disable=useless-super-delegation - return super(AsyncMock, self).__call__(*args, **kwargs) From 92b1d8e4dfc144858cb1e8181978378ca3cc6cc2 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 11:02:56 +0200 Subject: [PATCH 061/147] SDK-2760: Fix paths --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6b9cc51..b55602e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform -from tests.async_mock import AsyncMock +from galaxy.unittest.mock import AsyncMock @pytest.fixture() def reader(): From 30b3533e1d7dc3c06a26a2b14cf9400353aad328 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 14:53:28 +0200 Subject: [PATCH 062/147] Old style namespace package --- galaxy/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 galaxy/__init__.py diff --git a/galaxy/__init__.py b/galaxy/__init__.py new file mode 100644 index 0000000..69e3be5 --- /dev/null +++ b/galaxy/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) From 7b9bcf86a1427491448fd87da4a4ade14f234daa Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 16 Apr 2019 14:53:49 +0200 Subject: [PATCH 063/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 15bef57..c524033 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.22", + version="0.23", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 1fb79eb21a9fc34f5589db724af4053e20a38b15 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Fri, 26 Apr 2019 10:37:53 +0200 Subject: [PATCH 064/147] Add friends features --- galaxy/api/consts.py | 1 + galaxy/api/plugin.py | 10 +-- galaxy/api/types.py | 5 ++ tests/test_friends.py | 90 +++++++++++++++++++++++++ tests/test_users.py | 148 +----------------------------------------- 5 files changed, 100 insertions(+), 154 deletions(-) create mode 100644 tests/test_friends.py diff --git a/galaxy/api/consts.py b/galaxy/api/consts.py index 4e595b2..a4ad274 100644 --- a/galaxy/api/consts.py +++ b/galaxy/api/consts.py @@ -24,6 +24,7 @@ class Feature(Enum): Chat = "Chat" ImportUsers = "ImportUsers" VerifyGame = "VerifyGame" + ImportFriends = "ImportFriends" class LicenseType(Enum): Unknown = "Unknown" diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index d6ee78d..3c51188 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -77,8 +77,8 @@ class Plugin(): self._register_method( "import_friends", self.get_friends, - result_name="user_info_list", - feature=Feature.ImportUsers + result_name="friend_info_list", + feature=Feature.ImportFriends ) self._register_method( "import_user_infos", @@ -227,17 +227,13 @@ class Plugin(): self._notification_client.notify("local_game_status_changed", params) def add_friend(self, user): - params = {"user_info" : user} + params = {"friend_info" : user} self._notification_client.notify("friend_added", params) def remove_friend(self, user_id): params = {"user_id" : user_id} self._notification_client.notify("friend_removed", params) - def update_friend(self, user): - params = {"user_info" : user} - self._notification_client.notify("friend_updated", params) - def update_room(self, room_id, unread_message_count=None, new_messages=None): params = {"room_id": room_id} if unread_message_count is not None: diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 574d427..25dad4e 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -68,6 +68,11 @@ class UserInfo(): avatar_url: str presence: Presence +@dataclass +class FriendInfo(): + user_id: str + user_name: str + @dataclass class Room(): room_id: str diff --git a/tests/test_friends.py b/tests/test_friends.py new file mode 100644 index 0000000..eb90c14 --- /dev/null +++ b/tests/test_friends.py @@ -0,0 +1,90 @@ +import asyncio +import json + +from galaxy.api.types import FriendInfo +from galaxy.api.errors import UnknownError + + +def test_get_friends_success(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.return_value = [ + FriendInfo("3", "Jan"), + FriendInfo("5", "Ola") + ] + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "result": { + "friend_info_list": [ + {"user_id": "3", "user_name": "Jan"}, + {"user_id": "5", "user_name": "Ola"} + ] + } + } + + +def test_get_friends_failure(plugin, readline, write): + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "import_friends" + } + + readline.side_effect = [json.dumps(request), ""] + plugin.get_friends.side_effect = UnknownError() + asyncio.run(plugin.run()) + plugin.get_friends.assert_called_with() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": 0, + "message": "Unknown error", + } + } + + +def test_add_friend(plugin, write): + friend = FriendInfo("7", "Kuba") + + async def couritine(): + plugin.add_friend(friend) + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_added", + "params": { + "friend_info": {"user_id": "7", "user_name": "Kuba"} + } + } + + +def test_remove_friend(plugin, write): + async def couritine(): + plugin.remove_friend("5") + + asyncio.run(couritine()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "friend_removed", + "params": { + "user_id": "5" + } + } diff --git a/tests/test_users.py b/tests/test_users.py index f4bf775..1e82831 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -5,153 +5,6 @@ from galaxy.api.types import UserInfo, Presence from galaxy.api.errors import UnknownError from galaxy.api.consts import PresenceState -def test_get_friends_success(plugin, readline, write): - request = { - "jsonrpc": "2.0", - "id": "3", - "method": "import_friends" - } - - readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.return_value = [ - UserInfo( - "3", - True, - "Jan", - "http://avatar1.png", - Presence( - PresenceState.Online, - "123", - "Main menu" - ) - ), - UserInfo( - "5", - True, - "Ola", - "http://avatar2.png", - Presence(PresenceState.Offline) - ) - ] - asyncio.run(plugin.run()) - plugin.get_friends.assert_called_with() - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "id": "3", - "result": { - "user_info_list": [ - { - "user_id": "3", - "is_friend": True, - "user_name": "Jan", - "avatar_url": "http://avatar1.png", - "presence": { - "presence_state": "online", - "game_id": "123", - "presence_status": "Main menu" - } - }, - { - "user_id": "5", - "is_friend": True, - "user_name": "Ola", - "avatar_url": "http://avatar2.png", - "presence": { - "presence_state": "offline" - } - } - ] - } - } - -def test_get_friends_failure(plugin, readline, write): - request = { - "jsonrpc": "2.0", - "id": "3", - "method": "import_friends" - } - - readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.side_effect = UnknownError() - asyncio.run(plugin.run()) - plugin.get_friends.assert_called_with() - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "id": "3", - "error": { - "code": 0, - "message": "Unknown error", - } - } - -def test_add_friend(plugin, write): - friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline)) - - async def couritine(): - plugin.add_friend(friend) - - asyncio.run(couritine()) - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "method": "friend_added", - "params": { - "user_info": { - "user_id": "7", - "is_friend": True, - "user_name": "Kuba", - "avatar_url": "http://avatar.png", - "presence": { - "presence_state": "offline" - } - } - } - } - -def test_remove_friend(plugin, write): - async def couritine(): - plugin.remove_friend("5") - - asyncio.run(couritine()) - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "method": "friend_removed", - "params": { - "user_id": "5" - } - } - -def test_update_friend(plugin, write): - friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline)) - - async def couritine(): - plugin.update_friend(friend) - - asyncio.run(couritine()) - response = json.loads(write.call_args[0][0]) - - assert response == { - "jsonrpc": "2.0", - "method": "friend_updated", - "params": { - "user_info": { - "user_id": "9", - "is_friend": True, - "user_name": "Anna", - "avatar_url": "http://avatar.png", - "presence": { - "presence_state": "offline" - } - } - } - } def test_get_users_success(plugin, readline, write): request = { @@ -189,6 +42,7 @@ def test_get_users_success(plugin, readline, write): } } + def test_get_users_failure(plugin, readline, write): request = { "jsonrpc": "2.0", From dc9fc2cc5d89f61591196bb741e1db7d6bae46a9 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 29 Apr 2019 14:33:19 +0200 Subject: [PATCH 065/147] SDK-2762: Standarize parameter binding --- galaxy/api/jsonrpc.py | 4 ++-- galaxy/api/plugin.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index cc9d6e4..563754b 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -162,12 +162,12 @@ class Server(): if internal: # internal requests are handled immediately - response = callback(request.params) + response = callback(**request.params) self._send_response(request.id, response) else: async def handle(): try: - result = await callback(request.params) + result = await callback(**request.params) self._send_response(request.id, result) except TypeError: self._send_error(request.id, InvalidParams()) diff --git a/galaxy/api/plugin.py b/galaxy/api/plugin.py index d6ee78d..9c34ac3 100644 --- a/galaxy/api/plugin.py +++ b/galaxy/api/plugin.py @@ -140,8 +140,8 @@ class Plugin(): def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None): if internal: - def method(params): - result = handler(**params) + def method(*args, **kwargs): + result = handler(*args, **kwargs) if result_name: result = { result_name: result @@ -149,8 +149,8 @@ class Plugin(): return result self._server.register_method(name, method, True, sensitive_params) else: - async def method(params): - result = await handler(**params) + async def method(*args, **kwargs): + result = await handler(*args, **kwargs) if result_name: result = { result_name: result From 6e251c6eb942fb3b78dd3c664eb0bcaa790db8a7 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 29 Apr 2019 15:45:03 +0200 Subject: [PATCH 066/147] SDK-2762: Bind method params before calling --- galaxy/api/jsonrpc.py | 29 ++++++++++++++++++++--------- galaxy/unittest/mock.py | 7 +++++++ tests/conftest.py | 4 ++-- tests/test_achievements.py | 4 ++-- tests/test_authenticate.py | 6 +++--- tests/test_chat.py | 20 ++++++++++---------- tests/test_game_times.py | 4 ++-- tests/test_local_games.py | 4 ++-- tests/test_owned_games.py | 4 ++-- tests/test_users.py | 8 ++++---- 10 files changed, 54 insertions(+), 36 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index 563754b..db45f55 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -2,6 +2,7 @@ import asyncio from collections import namedtuple from collections.abc import Iterable import logging +import inspect import json class JsonRpcError(Exception): @@ -46,7 +47,7 @@ class UnknownError(ApplicationError): super().__init__(0, "Unknown error", data) Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None]) -Method = namedtuple("Method", ["callback", "internal", "sensitive_params"]) +Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"]) def anonymise_sensitive_params(params, sensitive_params): anomized_data = "****" @@ -81,7 +82,7 @@ class Server(): :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params are considered sensitive, if True - all params are considered sensitive """ - self._methods[name] = Method(callback, internal, sensitive_params) + self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) def register_notification(self, name, callback, internal, sensitive_params=False): """ @@ -92,7 +93,7 @@ class Server(): :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params are considered sensitive, if True - all params are considered sensitive """ - self._notifications[name] = Method(callback, internal, sensitive_params) + self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) def register_eof(self, callback): self._eof_listeners.append(callback) @@ -138,15 +139,20 @@ class Server(): logging.error("Received unknown notification: %s", request.method) return - callback, internal, sensitive_params = method + callback, signature, internal, sensitive_params = method self._log_request(request, sensitive_params) + try: + bound_args = signature.bind(**request.params) + except TypeError: + self._send_error(request.id, InvalidParams()) + if internal: # internal requests are handled immediately - callback(**request.params) + callback(*bound_args.args, **bound_args.kwargs) else: try: - asyncio.create_task(callback(**request.params)) + asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs)) except Exception: logging.exception("Unexpected exception raised in notification handler") @@ -157,17 +163,22 @@ class Server(): self._send_error(request.id, MethodNotFound()) return - callback, internal, sensitive_params = method + callback, signature, internal, sensitive_params = method self._log_request(request, sensitive_params) + try: + bound_args = signature.bind(**request.params) + except TypeError: + self._send_error(request.id, InvalidParams()) + if internal: # internal requests are handled immediately - response = callback(**request.params) + response = callback(*bound_args.args, **bound_args.kwargs) self._send_response(request.id, response) else: async def handle(): try: - result = await callback(**request.params) + result = await callback(*bound_args.args, **bound_args.kwargs) self._send_response(request.id, result) except TypeError: self._send_error(request.id, InvalidParams()) diff --git a/galaxy/unittest/mock.py b/galaxy/unittest/mock.py index f475ebd..264c3fa 100644 --- a/galaxy/unittest/mock.py +++ b/galaxy/unittest/mock.py @@ -1,5 +1,12 @@ +from asyncio import coroutine from unittest.mock import MagicMock class AsyncMock(MagicMock): async def __call__(self, *args, **kwargs): return super(AsyncMock, self).__call__(*args, **kwargs) + +def coroutine_mock(): + coro = MagicMock(name="CoroutineResult") + corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro)) + corofunc.coro = coro + return corofunc \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index b55602e..fed2e87 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,7 +6,7 @@ import pytest from galaxy.api.plugin import Plugin from galaxy.api.consts import Platform -from galaxy.unittest.mock import AsyncMock +from galaxy.unittest.mock import AsyncMock, coroutine_mock @pytest.fixture() def reader(): @@ -57,7 +57,7 @@ def plugin(reader, writer): with ExitStack() as stack: for method in async_methods: - stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock)) + stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock)) for method in methods: stack.enter_context(patch.object(Plugin, method)) yield Plugin(Platform.Generic, "0.1", reader, writer, "token") diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 1c8c9e2..3926662 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -23,7 +23,7 @@ def test_success(plugin, readline, write): } } readline.side_effect = [json.dumps(request), ""] - plugin.get_unlocked_achievements.return_value = [ + plugin.get_unlocked_achievements.coro.return_value = [ Achievement(achievement_id="lvl10", unlock_time=1548421241), Achievement(achievement_name="Got level 20", unlock_time=1548422395), Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633) @@ -65,7 +65,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_unlocked_achievements.side_effect = UnknownError() + plugin.get_unlocked_achievements.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index c11abf2..4c25ba5 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -18,7 +18,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = Authentication("132", "Zenek") + plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -56,7 +56,7 @@ def test_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.side_effect = error() + plugin.authenticate.coro.side_effect = error() asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -82,7 +82,7 @@ def test_stored_credentials(plugin, readline, write): } } readline.side_effect = [json.dumps(request), ""] - plugin.authenticate.return_value = Authentication("132", "Zenek") + plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) write.assert_called() diff --git a/tests/test_chat.py b/tests/test_chat.py index a9fbd13..97dad89 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -21,7 +21,7 @@ def test_send_message_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.send_message.return_value = None + plugin.send_message.coro.return_value = None asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="14", message="Hello!") response = json.loads(write.call_args[0][0]) @@ -52,7 +52,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.send_message.side_effect = error() + plugin.send_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="15", message="Bye") response = json.loads(write.call_args[0][0]) @@ -78,7 +78,7 @@ def test_mark_as_read_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.mark_as_read.return_value = None + plugin.mark_as_read.coro.return_value = None asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") response = json.loads(write.call_args[0][0]) @@ -114,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.mark_as_read.side_effect = error() + plugin.mark_as_read.coro.side_effect = error() asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") response = json.loads(write.call_args[0][0]) @@ -136,7 +136,7 @@ def test_get_rooms_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_rooms.return_value = [ + plugin.get_rooms.coro.return_value = [ Room("13", 0, None), Room("15", 34, "8") ] @@ -170,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_rooms.side_effect = UnknownError() + plugin.get_rooms.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_rooms.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -196,7 +196,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_message.return_value = [ + plugin.get_room_history_from_message.coro.return_value = [ Message("13", "149", 1549454837, "Hello"), Message("14", "812", 1549454899, "Hi") ] @@ -245,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_message.side_effect = error() + plugin.get_room_history_from_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") response = json.loads(write.call_args[0][0]) @@ -271,7 +271,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_timestamp.return_value = [ + plugin.get_room_history_from_timestamp.coro.return_value = [ Message("12", "155", 1549454836, "Bye") ] asyncio.run(plugin.run()) @@ -308,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_room_history_from_timestamp.side_effect = UnknownError() + plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_room_history_from_timestamp.assert_called_with( room_id="10", diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 3c5690a..38f3853 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -12,7 +12,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_game_times.return_value = [ + plugin.get_game_times.coro.return_value = [ GameTime("3", 60, 1549550504), GameTime("5", 10, 1549550502) ] @@ -47,7 +47,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_game_times.side_effect = UnknownError() + plugin.get_game_times.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_local_games.py b/tests/test_local_games.py index c97e8d2..445e699 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -16,7 +16,7 @@ def test_success(plugin, readline, write): readline.side_effect = [json.dumps(request), ""] - plugin.get_local_games.return_value = [ + plugin.get_local_games.coro.return_value = [ LocalGame("1", LocalGameState.Running), LocalGame("2", LocalGameState.Installed), LocalGame("3", LocalGameState.Installed | LocalGameState.Running) @@ -61,7 +61,7 @@ def test_failure(plugin, readline, write, error, code, message): } readline.side_effect = [json.dumps(request), ""] - plugin.get_local_games.side_effect = error() + plugin.get_local_games.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index ac5cc7c..1202c9e 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -13,7 +13,7 @@ def test_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_owned_games.return_value = [ + plugin.get_owned_games.coro.return_value = [ Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)), Game( "5", @@ -75,7 +75,7 @@ def test_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_owned_games.side_effect = UnknownError() + plugin.get_owned_games.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() response = json.loads(write.call_args[0][0]) diff --git a/tests/test_users.py b/tests/test_users.py index f4bf775..eb661cd 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -13,7 +13,7 @@ def test_get_friends_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.return_value = [ + plugin.get_friends.coro.return_value = [ UserInfo( "3", True, @@ -74,7 +74,7 @@ def test_get_friends_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_friends.side_effect = UnknownError() + plugin.get_friends.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_friends.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -164,7 +164,7 @@ def test_get_users_success(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_users.return_value = [ + plugin.get_users.coro.return_value = [ UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) ] asyncio.run(plugin.run()) @@ -200,7 +200,7 @@ def test_get_users_failure(plugin, readline, write): } readline.side_effect = [json.dumps(request), ""] - plugin.get_users.side_effect = UnknownError() + plugin.get_users.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) response = json.loads(write.call_args[0][0]) From 527fd034bf0d7936a8247a3fc90a755399dc3a99 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 29 Apr 2019 15:45:18 +0200 Subject: [PATCH 067/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c524033..316bc4b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.23", + version="0.24", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 7727098c6f45aec7cc300c706bd9f8ea6a9107f9 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 30 Apr 2019 16:58:54 +0200 Subject: [PATCH 068/147] SDK-2762: Fix error handling --- galaxy/api/jsonrpc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/galaxy/api/jsonrpc.py b/galaxy/api/jsonrpc.py index db45f55..c46b26f 100644 --- a/galaxy/api/jsonrpc.py +++ b/galaxy/api/jsonrpc.py @@ -180,8 +180,6 @@ class Server(): try: result = await callback(*bound_args.args, **bound_args.kwargs) self._send_response(request.id, result) - except TypeError: - self._send_error(request.id, InvalidParams()) except NotImplementedError: self._send_error(request.id, MethodNotFound()) except JsonRpcError as error: From 8ad5ed76b73e951b6fde396f7a926e673bfa4632 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 30 Apr 2019 16:59:13 +0200 Subject: [PATCH 069/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 316bc4b..ad7f6e8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.24", + version="0.25", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From f7f170b9ca9fe0a455aacd71b1860f1901f4cfc0 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Fri, 3 May 2019 15:56:21 +0200 Subject: [PATCH 070/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad7f6e8..7bbc635 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.25", + version="0.26", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 9e1c8cfdddd4d22d4aa94fd9871168c10d991319 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Fri, 3 May 2019 15:55:02 +0200 Subject: [PATCH 071/147] Add JS to NextStep params --- galaxy/api/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/galaxy/api/types.py b/galaxy/api/types.py index 25dad4e..746f3a3 100644 --- a/galaxy/api/types.py +++ b/galaxy/api/types.py @@ -20,6 +20,7 @@ class NextStep(): next_step: str auth_params: Dict[str, str] cookies: Optional[List[Cookie]] = None + js: Optional[Dict[str, List[str]]] = None @dataclass class LicenseInfo(): From 90835ece58539d077b9eeb035a8fe3d99ab504dd Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 13:16:28 +0200 Subject: [PATCH 072/147] Change project layout --- requirements.txt | 1 + setup.py | 3 ++- {galaxy => src/galaxy}/__init__.py | 0 {galaxy => src/galaxy}/api/__init__.py | 0 {galaxy => src/galaxy}/api/consts.py | 0 {galaxy => src/galaxy}/api/errors.py | 0 {galaxy => src/galaxy}/api/jsonrpc.py | 0 {galaxy => src/galaxy}/api/plugin.py | 0 {galaxy => src/galaxy}/api/types.py | 0 {galaxy => src/galaxy}/tools.py | 0 {galaxy => src/galaxy}/unittest/__init__.py | 0 {galaxy => src/galaxy}/unittest/mock.py | 0 12 files changed, 3 insertions(+), 1 deletion(-) rename {galaxy => src/galaxy}/__init__.py (100%) rename {galaxy => src/galaxy}/api/__init__.py (100%) rename {galaxy => src/galaxy}/api/consts.py (100%) rename {galaxy => src/galaxy}/api/errors.py (100%) rename {galaxy => src/galaxy}/api/jsonrpc.py (100%) rename {galaxy => src/galaxy}/api/plugin.py (100%) rename {galaxy => src/galaxy}/api/types.py (100%) rename {galaxy => src/galaxy}/tools.py (100%) rename {galaxy => src/galaxy}/unittest/__init__.py (100%) rename {galaxy => src/galaxy}/unittest/mock.py (100%) diff --git a/requirements.txt b/requirements.txt index 9dee50a..aee0842 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ +-e . pytest==4.2.0 pytest-flakes==4.0.0 diff --git a/setup.py b/setup.py index 7bbc635..2aca14b 100644 --- a/setup.py +++ b/setup.py @@ -6,5 +6,6 @@ setup( description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', - packages=find_packages(exclude=["tests"]) + packages=find_packages("src"), + package_dir={'': 'src'} ) diff --git a/galaxy/__init__.py b/src/galaxy/__init__.py similarity index 100% rename from galaxy/__init__.py rename to src/galaxy/__init__.py diff --git a/galaxy/api/__init__.py b/src/galaxy/api/__init__.py similarity index 100% rename from galaxy/api/__init__.py rename to src/galaxy/api/__init__.py diff --git a/galaxy/api/consts.py b/src/galaxy/api/consts.py similarity index 100% rename from galaxy/api/consts.py rename to src/galaxy/api/consts.py diff --git a/galaxy/api/errors.py b/src/galaxy/api/errors.py similarity index 100% rename from galaxy/api/errors.py rename to src/galaxy/api/errors.py diff --git a/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py similarity index 100% rename from galaxy/api/jsonrpc.py rename to src/galaxy/api/jsonrpc.py diff --git a/galaxy/api/plugin.py b/src/galaxy/api/plugin.py similarity index 100% rename from galaxy/api/plugin.py rename to src/galaxy/api/plugin.py diff --git a/galaxy/api/types.py b/src/galaxy/api/types.py similarity index 100% rename from galaxy/api/types.py rename to src/galaxy/api/types.py diff --git a/galaxy/tools.py b/src/galaxy/tools.py similarity index 100% rename from galaxy/tools.py rename to src/galaxy/tools.py diff --git a/galaxy/unittest/__init__.py b/src/galaxy/unittest/__init__.py similarity index 100% rename from galaxy/unittest/__init__.py rename to src/galaxy/unittest/__init__.py diff --git a/galaxy/unittest/mock.py b/src/galaxy/unittest/mock.py similarity index 100% rename from galaxy/unittest/mock.py rename to src/galaxy/unittest/mock.py From 234a21d085e56c49c1b7a098ca34534c969c24a5 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 13:36:32 +0200 Subject: [PATCH 073/147] Add HttpClient --- requirements.txt | 3 +++ setup.py | 6 +++++- src/galaxy/http.py | 41 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/galaxy/http.py diff --git a/requirements.txt b/requirements.txt index aee0842..49e755d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ -e . pytest==4.2.0 pytest-flakes==4.0.0 +# because of pip bug https://github.com/pypa/pip/issues/4780 +aiohttp==3.5.4 +certifi==2019.3.9 diff --git a/setup.py b/setup.py index 2aca14b..d50ec98 100644 --- a/setup.py +++ b/setup.py @@ -7,5 +7,9 @@ setup( author='Galaxy team', author_email='galaxy@gog.com', packages=find_packages("src"), - package_dir={'': 'src'} + package_dir={'': 'src'}, + install_requires=[ + "aiohttp==3.5.4", + "certifi==2019.3.9" + ] ) diff --git a/src/galaxy/http.py b/src/galaxy/http.py new file mode 100644 index 0000000..2f984e6 --- /dev/null +++ b/src/galaxy/http.py @@ -0,0 +1,41 @@ +import asyncio +import ssl +from http import HTTPStatus + +import aiohttp +import certifi + +from galaxy.api.errors import ( + AccessDenied, AuthenticationRequired, + BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownError +) + +class AuthenticatedHttpClient: + def __init__(self, limit=20): # TODO timeout? + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_verify_locations(certifi.where()) + connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context) + self._session = aiohttp.ClientSession(connector=connector) + + async def close(self): + await self._session.close() + + async def request(self, method, *args, **kwargs): + try: + response = await self._session.request(method, *args, **kwargs) + except asyncio.TimeoutError: + raise BackendTimeout() + except aiohttp.ClientConnectionError: + raise NetworkError() + if response.status == HTTPStatus.UNAUTHORIZED: + raise AuthenticationRequired() + if response.status == HTTPStatus.FORBIDDEN: + raise AccessDenied() + if response.status == HTTPStatus.SERVICE_UNAVAILABLE: + raise BackendNotAvailable() + if response.status >= 500: + raise BackendError() + if response.status >= 400: + raise UnknownError() + + return response From 1fd959a665c3dd160287ce80d45d2b082d128141 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 13:50:46 +0200 Subject: [PATCH 074/147] Handle ServerDisconnectedError --- setup.py | 2 +- src/galaxy/http.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d50ec98..6ec387e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.26", + version="0.27", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 2f984e6..7973e64 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -27,6 +27,8 @@ class AuthenticatedHttpClient: raise BackendTimeout() except aiohttp.ClientConnectionError: raise NetworkError() + except aiohttp.ServerDisconnectedError: + raise BackendNotAvailable() if response.status == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() if response.status == HTTPStatus.FORBIDDEN: From c349a3df8e6e5117f2aa55650e989799126de0e6 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 13:56:53 +0200 Subject: [PATCH 075/147] Add timeout --- src/galaxy/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 7973e64..1e6ec82 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -11,10 +11,10 @@ from galaxy.api.errors import ( ) class AuthenticatedHttpClient: - def __init__(self, limit=20): # TODO timeout? + def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60)): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_verify_locations(certifi.where()) - connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context) + connector = aiohttp.TCPConnector(limit=limit, timeout=timeout, ssl=ssl_context) self._session = aiohttp.ClientSession(connector=connector) async def close(self): From 0608ade6d3bdb3ef1c744960c44ed4f922510080 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 14:14:33 +0200 Subject: [PATCH 076/147] Fix name --- src/galaxy/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 1e6ec82..92dc293 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -10,7 +10,7 @@ from galaxy.api.errors import ( BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownError ) -class AuthenticatedHttpClient: +class HttpClient: def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60)): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_verify_locations(certifi.where()) From c8083b90060cad3afbe5d26241dbffc1b12f8261 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 17:05:40 +0200 Subject: [PATCH 077/147] Add cookie_jar param to HttpClient --- src/galaxy/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 92dc293..eaa1454 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -11,11 +11,11 @@ from galaxy.api.errors import ( ) class HttpClient: - def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60)): + def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_verify_locations(certifi.where()) connector = aiohttp.TCPConnector(limit=limit, timeout=timeout, ssl=ssl_context) - self._session = aiohttp.ClientSession(connector=connector) + self._session = aiohttp.ClientSession(connector=connector, cookie_jar=cookie_jar) async def close(self): await self._session.close() From 701d3cf522137878dc48a3b1b061473fd64f2943 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 17:06:01 +0200 Subject: [PATCH 078/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6ec387e..09fc21b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.27", + version="0.28", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 85f1d83c28eb209ce9c2bc91c979a0f278a23441 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 17:21:05 +0200 Subject: [PATCH 079/147] Fix parameters --- src/galaxy/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index eaa1454..d6791cf 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -14,8 +14,8 @@ class HttpClient: def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.load_verify_locations(certifi.where()) - connector = aiohttp.TCPConnector(limit=limit, timeout=timeout, ssl=ssl_context) - self._session = aiohttp.ClientSession(connector=connector, cookie_jar=cookie_jar) + connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context) + self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar) async def close(self): await self._session.close() From 453734cefe99c6601e2bbdc0bfb3b1bae65e3aec Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 10 May 2019 17:21:31 +0200 Subject: [PATCH 080/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 09fc21b..a3b348d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.28", + version="0.28.1", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From f283c10a95eb79e161dc2fb6d84f78562f630c54 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 15 May 2019 19:49:50 +0200 Subject: [PATCH 081/147] Anonymise params in pass_login_credentials --- src/galaxy/api/plugin.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 1b3f302..a08088d 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -47,8 +47,16 @@ class Plugin(): self._register_method("ping", self._ping, internal=True) # implemented by developer - self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"]) - self._register_method("pass_login_credentials", self.pass_login_credentials) + self._register_method( + "init_authentication", + self.authenticate, + sensitive_params=["stored_credentials"] + ) + self._register_method( + "pass_login_credentials", + self.pass_login_credentials, + sensitive_params=["cookies", "credentials"] + ) self._register_method( "import_owned_games", self.get_owned_games, From 68fdc4d188751ace6f23424dee2b9f3bb6622891 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 17 May 2019 15:32:24 +0200 Subject: [PATCH 082/147] Printing own port --- src/galaxy/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index a08088d..b10105f 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -346,6 +346,8 @@ def create_and_run_plugin(plugin_class, argv): async def coroutine(): reader, writer = await asyncio.open_connection("127.0.0.1", port) + extra_info = writer.get_extra_info('sockname') + logging.info("Using local address: %s:%u", *extra_info) plugin = plugin_class(reader, writer, token) await plugin.run() From bdd2225262f67983ede47f2e57d113a7135a6283 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 14 May 2019 10:32:14 +0200 Subject: [PATCH 083/147] Add new interface methods for game time and achievements --- requirements.txt | 2 + src/galaxy/api/errors.py | 4 ++ src/galaxy/api/jsonrpc.py | 3 ++ src/galaxy/api/plugin.py | 89 ++++++++++++++++++++++++++++++++++++ tests/test_achievements.py | 94 +++++++++++++++++++++++++++++++++++++- tests/test_game_times.py | 94 +++++++++++++++++++++++++++++++++++++- 6 files changed, 284 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 49e755d..528cc4c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -e . pytest==4.2.0 +pytest-asyncio==0.10.0 +pytest-mock==1.10.3 pytest-flakes==4.0.0 # because of pip bug https://github.com/pypa/pip/issues/4780 aiohttp==3.5.4 diff --git a/src/galaxy/api/errors.py b/src/galaxy/api/errors.py index 3e1921d..189db00 100644 --- a/src/galaxy/api/errors.py +++ b/src/galaxy/api/errors.py @@ -77,3 +77,7 @@ class IncoherentLastMessage(ApplicationError): class MessageNotFound(ApplicationError): def __init__(self, data=None): super().__init__(500, "Message not found", data) + +class ImportInProgress(ApplicationError): + def __init__(self, data=None): + super().__init__(600, "Import already in progress", data) \ No newline at end of file diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index c46b26f..07290d7 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -12,6 +12,9 @@ class JsonRpcError(Exception): self.data = data super().__init__() + def __eq__(self, other): + return self.code == other.code and self.message == other.message and self.data == other.data + class ParseError(JsonRpcError): def __init__(self): super().__init__(-32700, "Parse error") diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index b10105f..1059f35 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -9,6 +9,7 @@ import sys from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.consts import Feature +from galaxy.api.errors import UnknownError, ImportInProgress class JSONEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=method-hidden @@ -41,6 +42,9 @@ class Plugin(): self._shutdown() self._server.register_eof(eof_handler) + self._achievements_import_in_progress = False + self._game_times_import_in_progress = False + # internal self._register_method("shutdown", self._shutdown, internal=True) self._register_method("get_capabilities", self._get_capabilities, internal=True) @@ -230,6 +234,26 @@ class Plugin(): } self._notification_client.notify("achievement_unlocked", params) + def game_achievements_import_success(self, game_id, achievements): + params = { + "game_id": game_id, + "unlocked_achievements": achievements + } + self._notification_client.notify("game_achievements_import_success", params) + + def game_achievements_import_failure(self, game_id, error): + params = { + "game_id": game_id, + "error": { + "code": error.code, + "message": error.message + } + } + self._notification_client.notify("game_achievements_import_failure", params) + + def achievements_import_finished(self): + self._notification_client.notify("achievements_import_finished", None) + def update_local_game_status(self, local_game): params = {"local_game" : local_game} self._notification_client.notify("local_game_status_changed", params) @@ -254,6 +278,23 @@ class Plugin(): params = {"game_time" : game_time} self._notification_client.notify("game_time_updated", params) + def game_time_import_success(self, game_time): + params = {"game_time" : game_time} + self._notification_client.notify("game_time_import_success", params) + + def game_time_import_failure(self, game_id, error): + params = { + "game_id": game_id, + "error": { + "code": error.code, + "message": error.message + } + } + self._notification_client.notify("game_time_import_failure", params) + + def game_times_import_finished(self): + self._notification_client.notify("game_times_import_finished", None) + def lost_authentication(self): self._notification_client.notify("authentication_lost", None) @@ -287,6 +328,28 @@ class Plugin(): async def get_unlocked_achievements(self, game_id): raise NotImplementedError() + async def start_achievements_import(self, game_ids): + if self._achievements_import_in_progress: + raise ImportInProgress() + + async def import_games_achievements(game_ids): + async def import_game_achievements(game_id): + try: + achievements = await self.get_unlocked_achievements(game_id) + self.game_achievements_import_success(game_id, achievements) + except Exception as error: + self.game_achievements_import_failure(game_id, error) + + try: + imports = [import_game_achievements(game_id) for game_id in game_ids] + await asyncio.gather(*imports) + finally: + self.achievements_import_finished() + self._achievements_import_in_progress = False + + asyncio.create_task(import_games_achievements(game_ids)) + self._achievements_import_in_progress = True + async def get_local_games(self): raise NotImplementedError() @@ -323,6 +386,32 @@ class Plugin(): async def get_game_times(self): raise NotImplementedError() + async def start_game_times_import(self, game_ids): + if self._game_times_import_in_progress: + raise ImportInProgress() + + async def import_game_times(game_ids): + try: + game_times = await self.get_game_times() + game_ids_set = set(game_ids) + for game_time in game_times: + if game_time.game_id not in game_ids_set: + continue + self.game_time_import_success(game_time) + game_ids_set.discard(game_time.game_id) + for game_id in game_ids_set: + self.game_time_import_failure(game_id, UnknownError()) + + except Exception as error: + for game_id in game_ids: + self.game_time_import_failure(game_id, error) + finally: + self.game_times_import_finished() + self._game_times_import_in_progress = False + + asyncio.create_task(import_game_times(game_ids)) + self._game_times_import_in_progress = True + def create_and_run_plugin(plugin_class, argv): if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 3926662..84421bd 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -1,9 +1,12 @@ import asyncio import json +from unittest.mock import call + +import pytest from pytest import raises from galaxy.api.types import Achievement -from galaxy.api.errors import UnknownError +from galaxy.api.errors import UnknownError, ImportInProgress, BackendError def test_initialization_no_unlock_time(): with raises(Exception): @@ -99,3 +102,92 @@ def test_unlock_achievement(plugin, write): } } } + +@pytest.mark.asyncio +async def test_game_achievements_import_success(plugin, write): + achievements = [ + Achievement(achievement_id="lvl10", unlock_time=1548421241), + Achievement(achievement_name="Got level 20", unlock_time=1548422395) + ] + plugin.game_achievements_import_success("134", achievements) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_achievements_import_success", + "params": { + "game_id": "134", + "unlocked_achievements": [ + { + "achievement_id": "lvl10", + "unlock_time": 1548421241 + }, + { + "achievement_name": "Got level 20", + "unlock_time": 1548422395 + } + ] + } + } + +@pytest.mark.asyncio +async def test_game_achievements_import_failure(plugin, write): + plugin.game_achievements_import_failure("134", ImportInProgress()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_achievements_import_failure", + "params": { + "game_id": "134", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + } + +@pytest.mark.asyncio +async def test_achievements_import_finished(plugin, write): + plugin.achievements_import_finished() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "achievements_import_finished", + "params": None + } + +@pytest.mark.asyncio +async def test_start_achievements_import(plugin, write, mocker): + game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success") + game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure") + achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished") + + game_ids = ["1", "5", "9"] + error = BackendError() + achievements = [ + Achievement(achievement_id="lvl10", unlock_time=1548421241), + Achievement(achievement_name="Got level 20", unlock_time=1548422395) + ] + plugin.get_unlocked_achievements.coro.side_effect = [ + achievements, + [], + error + ] + await plugin.start_achievements_import(game_ids) + + with pytest.raises(ImportInProgress): + await plugin.start_achievements_import(["4", "8"]) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")]) + game_achievements_import_success.assert_has_calls([ + call("1", achievements), + call("5", []) + ]) + game_achievements_import_failure.assert_called_once_with("9", error) + achievements_import_finished.assert_called_once_with() diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 38f3853..9ad7220 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -1,8 +1,10 @@ import asyncio import json +from unittest.mock import call +import pytest from galaxy.api.types import GameTime -from galaxy.api.errors import UnknownError +from galaxy.api.errors import UnknownError, ImportInProgress, BackendError def test_success(plugin, readline, write): request = { @@ -81,3 +83,93 @@ def test_update_game(plugin, write): } } } + +@pytest.mark.asyncio +async def test_game_time_import_success(plugin, write): + plugin.game_time_import_success(GameTime("3", 60, 1549550504)) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_import_success", + "params": { + "game_time": { + "game_id": "3", + "time_played": 60, + "last_played_time": 1549550504 + } + } + } + +@pytest.mark.asyncio +async def test_game_time_import_failure(plugin, write): + plugin.game_time_import_failure("134", ImportInProgress()) + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_time_import_failure", + "params": { + "game_id": "134", + "error": { + "code": 600, + "message": "Import already in progress" + } + } + } + +@pytest.mark.asyncio +async def test_game_times_import_finished(plugin, write): + plugin.game_times_import_finished() + response = json.loads(write.call_args[0][0]) + + assert response == { + "jsonrpc": "2.0", + "method": "game_times_import_finished", + "params": None + } + +@pytest.mark.asyncio +async def test_start_game_times_import(plugin, write, mocker): + game_time_import_success = mocker.patch.object(plugin, "game_time_import_success") + game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure") + game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished") + + game_ids = ["1", "5"] + game_time = GameTime("1", 10, 1549550502) + plugin.get_game_times.coro.return_value = [ + game_time + ] + await plugin.start_game_times_import(game_ids) + + with pytest.raises(ImportInProgress): + await plugin.start_game_times_import(["4", "8"]) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_game_times.coro.assert_called_once_with() + game_time_import_success.assert_called_once_with(game_time) + game_time_import_failure.assert_called_once_with("5", UnknownError()) + game_times_import_finished.assert_called_once_with() + +@pytest.mark.asyncio +async def test_start_game_times_import_failure(plugin, write, mocker): + game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure") + game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished") + + game_ids = ["1", "5"] + error = BackendError() + plugin.get_game_times.coro.side_effect = error + + await plugin.start_game_times_import(game_ids) + + # wait until all tasks are finished + for _ in range(4): + await asyncio.sleep(0) + + plugin.get_game_times.coro.assert_called_once_with() + + assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)] + game_times_import_finished.assert_called_once_with() From 6d513d86bf0a4476ee2904072750400eddd916ee Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 14 May 2019 10:34:42 +0200 Subject: [PATCH 084/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a3b348d..4cbbe8c 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.28.1", + version="0.29", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 6bc91a12fac90567a092dd46d8b8213b2cb1c91b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 14 May 2019 16:22:01 +0200 Subject: [PATCH 085/147] Register new requests --- src/galaxy/api/plugin.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 1059f35..95a4ff7 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -73,6 +73,10 @@ class Plugin(): result_name="unlocked_achievements", feature=Feature.ImportAchievements ) + self._register_method( + "start_achievements_import", + self.start_achievements_import, + ) self._register_method( "import_local_games", self.get_local_games, @@ -126,13 +130,16 @@ class Plugin(): result_name="messages", feature=Feature.Chat ) - self._register_method( "import_game_times", self.get_game_times, result_name="game_times", feature=Feature.ImportGameTime ) + self._register_method( + "start_game_times_import", + self.start_game_times_import, + ) @property def features(self): From 0ee56193de4d19b7433071e673c52083da2b5c85 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Mon, 20 May 2019 11:20:27 +0200 Subject: [PATCH 086/147] Fix aiohttp exception hierarchy --- src/galaxy/http.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index d6791cf..5b494ca 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -7,7 +7,7 @@ import certifi from galaxy.api.errors import ( AccessDenied, AuthenticationRequired, - BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownError + BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError ) class HttpClient: @@ -25,10 +25,14 @@ class HttpClient: response = await self._session.request(method, *args, **kwargs) except asyncio.TimeoutError: raise BackendTimeout() - except aiohttp.ClientConnectionError: - raise NetworkError() except aiohttp.ServerDisconnectedError: raise BackendNotAvailable() + except aiohttp.ClientConnectionError: + raise NetworkError() + except aiohttp.ContentTypeError: + raise UnknownBackendResponse() + except aiohttp.ClientError: + raise UnknownError() if response.status == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() if response.status == HTTPStatus.FORBIDDEN: From be6c0eb03e3a5d74af2796b7a42e363de6c87bed Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 20 May 2019 15:38:24 +0200 Subject: [PATCH 087/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4cbbe8c..bdab73b 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.29", + version="0.30", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From e62e7e0e6e1ef355af75c55a9a4ce098b911ed24 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 20 May 2019 15:30:08 +0200 Subject: [PATCH 088/147] Add pipeline for releasing to github --- jenkins/release.groovy | 12 ++++++++++++ jenkins/release.py | 26 ++++++++++++++++++++++++++ jenkins/requirements.txt | 1 + 3 files changed, 39 insertions(+) create mode 100644 jenkins/release.groovy create mode 100644 jenkins/release.py create mode 100644 jenkins/requirements.txt diff --git a/jenkins/release.groovy b/jenkins/release.groovy new file mode 100644 index 0000000..d25d308 --- /dev/null +++ b/jenkins/release.groovy @@ -0,0 +1,12 @@ +stage('Upload to github') +{ + node('ActiveClientWindowsBuilder') { + withPythonEnv('python') { + withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { + bat 'pip install -r jenkins/requirements.txt' + version = bat(returnStdout: true, script: 'python setup.py --version').trim() + bat "python jenkins/release.py ${version}" + } + } + } +} diff --git a/jenkins/release.py b/jenkins/release.py new file mode 100644 index 0000000..2290553 --- /dev/null +++ b/jenkins/release.py @@ -0,0 +1,26 @@ +import os +import sys +from galaxy.github.exporter import transfer_repo + +GITHUB_USERNAME = "FriendsOfGalaxy" +GITHUB_EMAIL = "friendsofgalaxy@gmail.com" +GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] +GITHUB_REPO_NAME = "galaxy-plugin-api" +SOURCE_BRANCH = os.environ["GIT_REFSPEC"] + +GITLAB_USERNAME = "galaxy-client" +GITLAB_REPO_NAME = "galaxy-plugin-api" + +def version_provider(): + return sys.argv[1] + +gh_version = transfer_repo( + version_provider=version_provider, + source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME), + source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini"], + source_branch=SOURCE_BRANCH, + dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, GITHUB_USERNAME, GITHUB_REPO_NAME), + dest_branch="master", + dest_user_email=GITHUB_EMAIL, + dest_user_name=GITLAB_USERNAME +) \ No newline at end of file diff --git a/jenkins/requirements.txt b/jenkins/requirements.txt new file mode 100644 index 0000000..d76c88e --- /dev/null +++ b/jenkins/requirements.txt @@ -0,0 +1 @@ +git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1 \ No newline at end of file From 0bc8000f1449605e67717d95d8458c439f6a1115 Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Tue, 21 May 2019 11:10:52 +0200 Subject: [PATCH 089/147] Fix release pipeline --- jenkins/release.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/jenkins/release.groovy b/jenkins/release.groovy index d25d308..64c9488 100644 --- a/jenkins/release.groovy +++ b/jenkins/release.groovy @@ -1,6 +1,8 @@ stage('Upload to github') { node('ActiveClientWindowsBuilder') { + deleteDir() + checkout scm withPythonEnv('python') { withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { bat 'pip install -r jenkins/requirements.txt' From 758909efba13b0d142c480e89692b83adf7bf139 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 21 May 2019 11:20:38 +0200 Subject: [PATCH 090/147] Fix release.py --- jenkins/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/release.py b/jenkins/release.py index 2290553..dc4c378 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -11,7 +11,7 @@ SOURCE_BRANCH = os.environ["GIT_REFSPEC"] GITLAB_USERNAME = "galaxy-client" GITLAB_REPO_NAME = "galaxy-plugin-api" -def version_provider(): +def version_provider(_): return sys.argv[1] gh_version = transfer_repo( From f5361cd5abc90d217f89ba37e62492c30ddd4a5c Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 21 May 2019 14:09:57 +0200 Subject: [PATCH 091/147] Fix pipeline --- jenkins/release.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/release.groovy b/jenkins/release.groovy index 64c9488..f5f0119 100644 --- a/jenkins/release.groovy +++ b/jenkins/release.groovy @@ -6,8 +6,8 @@ stage('Upload to github') withPythonEnv('python') { withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { bat 'pip install -r jenkins/requirements.txt' - version = bat(returnStdout: true, script: 'python setup.py --version').trim() - bat "python jenkins/release.py ${version}" + def version = bat(returnStdout: true, script: 'python setup.py --version').trim() + bat "python jenkins/release.py $version" } } } From 3b296cbcc912a6b7192dba5fc30c1f17249f8abc Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 21 May 2019 14:18:45 +0200 Subject: [PATCH 092/147] Fix pipeline --- jenkins/release.groovy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jenkins/release.groovy b/jenkins/release.groovy index f5f0119..8caf4f9 100644 --- a/jenkins/release.groovy +++ b/jenkins/release.groovy @@ -1,13 +1,13 @@ stage('Upload to github') { - node('ActiveClientWindowsBuilder') { + node('ActiveClientMacosxBuilder') { deleteDir() checkout scm withPythonEnv('python') { withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { - bat 'pip install -r jenkins/requirements.txt' - def version = bat(returnStdout: true, script: 'python setup.py --version').trim() - bat "python jenkins/release.py $version" + sh 'pip install -r jenkins/requirements.txt' + def version = sh(returnStdout: true, script: 'python setup.py --version').trim() + sh "python jenkins/release.py $version" } } } From ef7f9ccca1c9e982e279d03d4c3c447b418df6d6 Mon Sep 17 00:00:00 2001 From: Romuald Bierbasz Date: Tue, 21 May 2019 14:27:02 +0200 Subject: [PATCH 093/147] Fix pipeline --- jenkins/release.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/release.groovy b/jenkins/release.groovy index 8caf4f9..9562754 100644 --- a/jenkins/release.groovy +++ b/jenkins/release.groovy @@ -3,7 +3,7 @@ stage('Upload to github') node('ActiveClientMacosxBuilder') { deleteDir() checkout scm - withPythonEnv('python') { + withPythonEnv('/usr/local/bin/python3.7') { withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { sh 'pip install -r jenkins/requirements.txt' def version = sh(returnStdout: true, script: 'python setup.py --version').trim() From 07b6edce12dbea4d11c84b7c9007265550932020 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 24 May 2019 14:04:05 +0200 Subject: [PATCH 094/147] SDK-2840: Refactor import methods --- src/galaxy/api/plugin.py | 60 +++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 95a4ff7..db8321a 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -339,24 +339,28 @@ class Plugin(): if self._achievements_import_in_progress: raise ImportInProgress() - async def import_games_achievements(game_ids): - async def import_game_achievements(game_id): - try: - achievements = await self.get_unlocked_achievements(game_id) - self.game_achievements_import_success(game_id, achievements) - except Exception as error: - self.game_achievements_import_failure(game_id, error) - + async def import_games_achievements_task(game_ids): try: - imports = [import_game_achievements(game_id) for game_id in game_ids] - await asyncio.gather(*imports) + await self.import_games_achievements(game_ids) finally: self.achievements_import_finished() self._achievements_import_in_progress = False - asyncio.create_task(import_games_achievements(game_ids)) + asyncio.create_task(import_games_achievements_task(game_ids)) self._achievements_import_in_progress = True + async def import_games_achievements(self, game_ids): + """Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list""" + async def import_game_achievements(game_id): + try: + achievements = await self.get_unlocked_achievements(game_id) + self.game_achievements_import_success(game_id, achievements) + except Exception as error: + self.game_achievements_import_failure(game_id, error) + + imports = [import_game_achievements(game_id) for game_id in game_ids] + await asyncio.gather(*imports) + async def get_local_games(self): raise NotImplementedError() @@ -397,28 +401,32 @@ class Plugin(): if self._game_times_import_in_progress: raise ImportInProgress() - async def import_game_times(game_ids): + async def import_game_times_task(game_ids): try: - game_times = await self.get_game_times() - game_ids_set = set(game_ids) - for game_time in game_times: - if game_time.game_id not in game_ids_set: - continue - self.game_time_import_success(game_time) - game_ids_set.discard(game_time.game_id) - for game_id in game_ids_set: - self.game_time_import_failure(game_id, UnknownError()) - - except Exception as error: - for game_id in game_ids: - self.game_time_import_failure(game_id, error) + await self.import_game_times(game_ids) finally: self.game_times_import_finished() self._game_times_import_in_progress = False - asyncio.create_task(import_game_times(game_ids)) + asyncio.create_task(import_game_times_task(game_ids)) self._game_times_import_in_progress = True + async def import_game_times(self, game_ids): + """Call game_time_import_success/game_time_import_failure for each game_id on the list""" + try: + game_times = await self.get_game_times() + game_ids_set = set(game_ids) + for game_time in game_times: + if game_time.game_id not in game_ids_set: + continue + self.game_time_import_success(game_time) + game_ids_set.discard(game_time.game_id) + for game_id in game_ids_set: + self.game_time_import_failure(game_id, UnknownError()) + except Exception as error: + for game_id in game_ids: + self.game_time_import_failure(game_id, error) + def create_and_run_plugin(plugin_class, argv): if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") From f2d4127a31b5d01ba6238632653ef2d71f003622 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 24 May 2019 14:04:17 +0200 Subject: [PATCH 095/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bdab73b..dab7886 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.30", + version="0.31", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From a3ca8159751827515f07fa19f89fe8198af4be18 Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Mon, 27 May 2019 17:41:28 +0200 Subject: [PATCH 096/147] cosmetic changes to readme.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 825e160..469551b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ -# Galaxy python plugin API +# GOG Galaxy - Community Integration - Python API -## Usage +This document is still work in progress. -Implement plugin: +## Basic Usage + +Basic implementation: ```python import asyncio @@ -15,10 +17,10 @@ class PluginExample(Plugin): # run plugin event loop if __name__ == "__main__": - asyncio.run(MockPlugin().run()) + asyncio.run(PluginExample().run()) ``` -Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale. +Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executable. ## Development @@ -31,15 +33,15 @@ Run tests: ```bash pytest ``` +## Methods Documentation +TODO ## Changelog -### 0.21 -* Add `Epic` platform. ### 0.16 * Do not log sensitive data. * Return `LocalGameState` as int (possible combination of flags). ### 0.15 * `shutdown()` is called on socket disconnection. ### 0.14 -* Added required version parameter to Plugin constructor. \ No newline at end of file +* Added required version parameter to Plugin constructor. From ccbb13e685a98a868821f77bcba30783f3c0d0cc Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Mon, 27 May 2019 17:48:42 +0200 Subject: [PATCH 097/147] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 469551b..ced5b5e 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ if __name__ == "__main__": asyncio.run(PluginExample().run()) ``` -Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executable. - ## Development Install required packages: From da8da24b01341d72d267873385230555d71b9c5e Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 28 May 2019 11:19:48 +0200 Subject: [PATCH 098/147] Remove changelog --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index ced5b5e..cb63b42 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,3 @@ pytest ``` ## Methods Documentation TODO - -## Changelog - -### 0.16 -* Do not log sensitive data. -* Return `LocalGameState` as int (possible combination of flags). -### 0.15 -* `shutdown()` is called on socket disconnection. -### 0.14 -* Added required version parameter to Plugin constructor. From 4a7a759cead3ee78961c11bc59d569ec86e42f47 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 28 May 2019 11:22:38 +0200 Subject: [PATCH 099/147] Update release scripts --- jenkins/release.groovy | 2 +- jenkins/release.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/jenkins/release.groovy b/jenkins/release.groovy index 9562754..d1a8542 100644 --- a/jenkins/release.groovy +++ b/jenkins/release.groovy @@ -4,7 +4,7 @@ stage('Upload to github') deleteDir() checkout scm withPythonEnv('/usr/local/bin/python3.7') { - withCredentials([usernamePassword(credentialsId: 'github_friendsofgalaxy', usernameVariable: 'GITHUB_USERNAME', passwordVariable: 'GITHUB_TOKEN')]) { + withCredentials([string(credentialsId: 'github_goggalaxy', variable: 'GITHUB_TOKEN')]) { sh 'pip install -r jenkins/requirements.txt' def version = sh(returnStdout: true, script: 'python setup.py --version').trim() sh "python jenkins/release.py $version" diff --git a/jenkins/release.py b/jenkins/release.py index dc4c378..2895819 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -2,10 +2,10 @@ import os import sys from galaxy.github.exporter import transfer_repo -GITHUB_USERNAME = "FriendsOfGalaxy" -GITHUB_EMAIL = "friendsofgalaxy@gmail.com" +GITHUB_USERNAME = "GOG Galaxy SDK Team" +GITHUB_EMAIL = "galaxy-sdk@gog.com" GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] -GITHUB_REPO_NAME = "galaxy-plugin-api" +GITHUB_REPO_NAME = "galaxy-integrations-python-api" SOURCE_BRANCH = os.environ["GIT_REFSPEC"] GITLAB_USERNAME = "galaxy-client" From 14c2d7d9e88cf6052df5f23b2cd298f776bd6912 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 28 May 2019 11:28:32 +0200 Subject: [PATCH 100/147] Update README --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cb63b42..09aad74 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,45 @@ This document is still work in progress. Basic implementation: ```python -import asyncio -from galaxy.api.plugin import Plugin +import sys +from galaxy.api.plugin import Plugin, create_and_run_plugin +from galaxy.api.consts import Platform class PluginExample(Plugin): + def __init__(self, reader, writer, token): + super().__init__( + Platform.Generic, # Choose platform from available list + "0.1", # Version + reader, + writer, + token + ) + # implement methods async def authenticate(self, stored_credentials=None): pass +def main(): + create_and_run_plugin(PluginExample, sys.argv) + # run plugin event loop if __name__ == "__main__": - asyncio.run(PluginExample().run()) + main() +``` + +Plugin should be deployed with manifest: +```json +{ + "name": "Example plugin", + "platform": "generic", + "guid": "UNIQUE-GUID", + "version": "0.1", + "description": "Example plugin", + "author": "Name", + "email": "author@email.com", + "url": "https://github.com/user/galaxy-plugin-example", + "script": "plugin.py" +} ``` ## Development From 9a115557b3ba0053b2c3ce2288deecf53171236d Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 28 May 2019 12:03:56 +0200 Subject: [PATCH 101/147] Fix username --- jenkins/release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins/release.py b/jenkins/release.py index 2895819..0897517 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -2,7 +2,7 @@ import os import sys from galaxy.github.exporter import transfer_repo -GITHUB_USERNAME = "GOG Galaxy SDK Team" +GITHUB_USERNAME = "goggalaxy" GITHUB_EMAIL = "galaxy-sdk@gog.com" GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] GITHUB_REPO_NAME = "galaxy-integrations-python-api" From 0da0296154f6e23b7885212603b2da1b8b93f07b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 28 May 2019 17:59:11 +0200 Subject: [PATCH 102/147] Release on gogcom --- jenkins/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/release.py b/jenkins/release.py index 0897517..5ca2856 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -19,8 +19,8 @@ gh_version = transfer_repo( source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME), source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini"], source_branch=SOURCE_BRANCH, - dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, GITHUB_USERNAME, GITHUB_REPO_NAME), + dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME), dest_branch="master", dest_user_email=GITHUB_EMAIL, - dest_user_name=GITLAB_USERNAME + dest_user_name="GOG Galaxy SDK Team" ) \ No newline at end of file From 80f40b1971b93801c16bad34f3b990c4019a45cb Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 29 May 2019 13:04:17 +0200 Subject: [PATCH 103/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dab7886..2de54bc 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.31", + version="0.31.1", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From c53aab1abb68b4bafd0abcba9f0cda3e09cf14da Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Fri, 31 May 2019 11:29:05 +0200 Subject: [PATCH 104/147] Legal Notice added --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 09aad74..f452516 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,7 @@ pytest ``` ## Methods Documentation TODO + +## Legal Notice + +By integrating or attempting to integrate any applications or content with or into GOG Galaxy® 2.0. you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. From bd393a96f0116d20f6c269e63d3292147e4f998c Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 31 May 2019 11:50:53 +0200 Subject: [PATCH 105/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2de54bc..998a8c9 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.31.1", + version="0.31.2", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From ed91fd582cc3414601d26b378164da1d1c902d96 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 31 May 2019 12:08:49 +0200 Subject: [PATCH 106/147] Deploy setup py --- jenkins/release.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/jenkins/release.py b/jenkins/release.py index 5ca2856..26740a6 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -17,7 +17,7 @@ def version_provider(_): gh_version = transfer_repo( version_provider=version_provider, source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME), - source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini"], + source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini", "setup.py"], source_branch=SOURCE_BRANCH, dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME), dest_branch="master", diff --git a/setup.py b/setup.py index 998a8c9..962a712 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.31.2", + version="0.31.3", description="Galaxy python plugin API", author='Galaxy team', author_email='galaxy@gog.com', From 7099cf3195b847c73b2abea42e73f2552e28ab4c Mon Sep 17 00:00:00 2001 From: Mateusz Silaczewski Date: Thu, 6 Jun 2019 15:56:01 +0200 Subject: [PATCH 107/147] Merge branch 'documentation' of https://gitlab.gog.com/galaxy-client/galaxy-plugin-api into documentation --- .gitignore | 7 + README.md | 45 ++-- docs/make.py | 13 + docs/requirements.txt | 4 + docs/source/conf.py | 74 ++++++ docs/source/galaxy.api.rst | 40 +++ docs/source/index.rst | 14 ++ docs/source/overview.rst | 1 + jenkins/release.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- src/.readthedocs.yml | 12 + src/galaxy/api/consts.py | 15 ++ src/galaxy/api/jsonrpc.py | 17 +- src/galaxy/api/plugin.py | 481 +++++++++++++++++++++++++++++++++---- src/galaxy/api/types.py | 114 +++++++++ 16 files changed, 762 insertions(+), 81 deletions(-) create mode 100644 docs/make.py create mode 100644 docs/requirements.txt create mode 100644 docs/source/conf.py create mode 100644 docs/source/galaxy.api.rst create mode 100644 docs/source/index.rst create mode 100644 docs/source/overview.rst create mode 100644 src/.readthedocs.yml diff --git a/.gitignore b/.gitignore index a87a247..80b31d3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,9 @@ # pytest __pycache__/ +.vscode/ +.venv/ +src/galaxy.plugin.api.egg-info/ +docs/build/ +Pipfile +.idea +docs/source/_build diff --git a/README.md b/README.md index f452516..fbbb420 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,26 @@ # GOG Galaxy - Community Integration - Python API -This document is still work in progress. +This Python library allows to easily build community integrations for various gaming platforms with GOG Galaxy 2.0. -## Basic Usage +Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that needs to communicate with main instance of GOG Galaxy 2.0. -Basic implementation: +The provided features are: + +- multistep authorisation using a browser built into GOG Galaxy 2.0 +- support for GOG Galaxy 2.0 features: + - importing owned and detecting installed games + - installing and launching games + - importing achievements and game time + - importing friends lists and statuses + - importing friends recomendations list + - receiving and sending chat messages +- cache storage + +## Basic usage + +Eeach integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client in the appropriate times. +Each of those method can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`. +Communication between an integration and the client is also possible with the use of notifications, for example: :meth:`~galaxy.api.plugin.Plugin.update_local_game_status`. ```python import sys @@ -33,7 +49,11 @@ if __name__ == "__main__": main() ``` -Plugin should be deployed with manifest: +## Deployment + +The client has a built-in Python 3.7 interpreter, so the integrations are delivered as `.py` files. +The additional `manifest.json` file is required: + ```json { "name": "Example plugin", @@ -47,21 +67,6 @@ Plugin should be deployed with manifest: "script": "plugin.py" } ``` - -## Development - -Install required packages: -```bash -pip install -r requirements.txt -``` - -Run tests: -```bash -pytest -``` -## Methods Documentation -TODO - ## Legal Notice -By integrating or attempting to integrate any applications or content with or into GOG Galaxy® 2.0. you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. +By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0. you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. diff --git a/docs/make.py b/docs/make.py new file mode 100644 index 0000000..cb41897 --- /dev/null +++ b/docs/make.py @@ -0,0 +1,13 @@ +"""Builds documentation locally. Use for preview only""" + +import subprocess +import webbrowser + + +source = "docs/source" +build = "docs/build" +master_doc = 'index' + +subprocess.run(['sphinx-build', '-M', 'clean', source, build]) +subprocess.run(['sphinx-build', '-M', 'html', source, build]) +webbrowser.open(f'{build}/html/{master_doc}') \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..98fe76e --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +Sphinx==2.0.1 +sphinx-rtd-theme==0.4.3 +sphinx-autodoc-typehints==1.6.0 +m2r==0.2.1 \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..096dbb5 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,74 @@ +# Configuration file for the Sphinx documentation builder. +# Documentation: +# http://www.sphinx-doc.org/en/master/config + +import os +import sys +import subprocess + +# -- Path setup -------------------------------------------------------------- +_ROOT = os.path.join('..', '..') + +sys.path.append(os.path.abspath(os.path.join(_ROOT, 'src'))) + +# -- Project information ----------------------------------------------------- + +project = 'GOG Galaxy Integrations API' +copyright = '2019, GOG.com' + +_author, _version = subprocess.check_output( + ['python', os.path.join(_ROOT, 'setup.py'), '--author', '--version'], + universal_newlines=True).strip().split('\n') + +author = _author +version = _version +release = _version + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_autodoc_typehints', + 'm2r' # mdinclude directive for makrdown files +] +autodoc_member_order = 'bysource' +autodoc_inherit_docstrings = False +autodoc_mock_imports = ["galaxy.http"] + +set_type_checking_flag = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" +html_theme_options = { + # 'canonical_url': '', # main page to be serach in google with trailing slash + 'display_version': True, + 'style_external_links': True, + # Toc options + 'collapse_navigation': False, + 'sticky_navigation': True, + 'navigation_depth': 4, +} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +master_doc = 'index' diff --git a/docs/source/galaxy.api.rst b/docs/source/galaxy.api.rst new file mode 100644 index 0000000..7a45d37 --- /dev/null +++ b/docs/source/galaxy.api.rst @@ -0,0 +1,40 @@ +galaxy.api +================= + +plugin +------------------------ + +.. automodule:: galaxy.api.plugin + :members: + :undoc-members: + :exclude-members: JSONEncoder, features, achievements_import_finished, game_times_import_finished, start_achievements_import, start_game_times_import, get_game_times, get_unlocked_achievements + +types +----------------------- + +.. automodule:: galaxy.api.types + :members: + :undoc-members: + +consts +------------------------ + +.. automodule:: galaxy.api.consts + :members: + :undoc-members: + :show-inheritance: + :exclude-members: Feature + +errors +------------------------ + +.. autoexception:: galaxy.api.jsonrpc.ApplicationError + :show-inheritance: + +.. autoexception:: galaxy.api.jsonrpc.UnknownError + :show-inheritance: + +.. automodule:: galaxy.api.errors + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..05f97f7 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,14 @@ +GOG Galaxy Integrations Python API +================================================= + +.. toctree:: + :maxdepth: 2 + :includehidden: + + Overview + API + +Index +------------------- + +* :ref:`genindex` diff --git a/docs/source/overview.rst b/docs/source/overview.rst new file mode 100644 index 0000000..3bd447c --- /dev/null +++ b/docs/source/overview.rst @@ -0,0 +1 @@ +.. mdinclude:: ../../README.md diff --git a/jenkins/release.py b/jenkins/release.py index 26740a6..12ef332 100644 --- a/jenkins/release.py +++ b/jenkins/release.py @@ -17,7 +17,7 @@ def version_provider(_): gh_version = transfer_repo( version_provider=version_provider, source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME), - source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini", "setup.py"], + source_include_elements=["src", "docs", "tests", "requirements.txt", ".readthedocs.yml" ".gitignore", "*.md", "pytest.ini", "setup.py"], source_branch=SOURCE_BRANCH, dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME), dest_branch="master", diff --git a/requirements.txt b/requirements.txt index 528cc4c..1e99e14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ pytest-mock==1.10.3 pytest-flakes==4.0.0 # because of pip bug https://github.com/pypa/pip/issues/4780 aiohttp==3.5.4 -certifi==2019.3.9 +certifi==2019.3.9 \ No newline at end of file diff --git a/setup.py b/setup.py index 962a712..7f4de87 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", version="0.31.3", - description="Galaxy python plugin API", + description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', packages=find_packages("src"), diff --git a/src/.readthedocs.yml b/src/.readthedocs.yml new file mode 100644 index 0000000..051ee73 --- /dev/null +++ b/src/.readthedocs.yml @@ -0,0 +1,12 @@ +version: 2 + +sphinx: + configuration: docs/source/conf.py + +formats: all + +python: + version: 3.7 + install: + - requirements: requirements.txt + - requirements: docs/requirements.txt diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index a4ad274..e938977 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -1,6 +1,8 @@ from enum import Enum, Flag + class Platform(Enum): + """Supported gaming platforms""" Unknown = "unknown" Gog = "gog" Steam = "steam" @@ -12,7 +14,11 @@ class Platform(Enum): Battlenet = "battlenet" Epic = "epic" + class Feature(Enum): + """Possible features that can be implemented by an integration. + It does not have to support all or any specific features from the list. + """ Unknown = "Unknown" ImportInstalledGames = "ImportInstalledGames" ImportOwnedGames = "ImportOwnedGames" @@ -26,18 +32,27 @@ class Feature(Enum): VerifyGame = "VerifyGame" ImportFriends = "ImportFriends" + class LicenseType(Enum): + """Possible game license types, understandable for the GOG Galaxy client.""" Unknown = "Unknown" SinglePurchase = "SinglePurchase" FreeToPlay = "FreeToPlay" OtherUserLicense = "OtherUserLicense" + class LocalGameState(Flag): + """Possible states that a local game can be in. + For example a game which is both installed and currently running should have its state set as a "bitwise or" of Running and Installed flags: + ``local_game_state=`` + """ None_ = 0 Installed = 1 Running = 2 + class PresenceState(Enum): + """"Possible states that a user can be in.""" Unknown = "Unknown" Online = "online" Offline = "offline" diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 07290d7..45f1b0d 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -79,22 +79,24 @@ class Server(): def register_method(self, name, callback, internal, sensitive_params=False): """ Register method + :param name: :param callback: :param internal: if True the callback will be processed immediately (synchronously) - :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params - are considered sensitive, if True - all params are considered sensitive + :param sensitive_params: list of parameters that are anonymized before logging; \ + if False - no params are considered sensitive, if True - all params are considered sensitive """ self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) def register_notification(self, name, callback, internal, sensitive_params=False): """ Register notification + :param name: :param callback: :param internal: if True the callback will be processed immediately (synchronously) - :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params - are considered sensitive, if True - all params are considered sensitive + :param sensitive_params: list of parameters that are anonymized before logging; \ + if False - no params are considered sensitive, if True - all params are considered sensitive """ self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params) @@ -187,7 +189,7 @@ class Server(): self._send_error(request.id, MethodNotFound()) except JsonRpcError as error: self._send_error(request.id, error) - except Exception as e: #pylint: disable=broad-except + except Exception as e: #pylint: disable=broad-except logging.exception("Unexpected exception raised in plugin handler") self._send_error(request.id, UnknownError(str(e))) @@ -256,10 +258,11 @@ class NotificationClient(): def notify(self, method, params, sensitive_params=False): """ Send notification + :param method: :param params: - :param sensitive_params: list of parameters that will by anonymized before logging; if False - no params - are considered sensitive, if True - all params are considered sensitive + :param sensitive_params: list of parameters that are anonymized before logging; \ + if False - no params are considered sensitive, if True - all params are considered sensitive """ notification = { "jsonrpc": "2.0", diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index db8321a..9b0dc71 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -7,7 +7,11 @@ from enum import Enum from collections import OrderedDict import sys -from galaxy.api.jsonrpc import Server, NotificationClient +from typing import List, Dict + +from galaxy.api.types import Achievement, Game, LocalGame, FriendInfo, GameTime, UserInfo, Room + +from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError from galaxy.api.consts import Feature from galaxy.api.errors import UnknownError, ImportInProgress @@ -23,6 +27,7 @@ class JSONEncoder(json.JSONEncoder): return super().default(o) class Plugin(): + """Use and override methods of this class to create a new platform integration.""" def __init__(self, platform, version, reader, writer, handshake_token): logging.info("Creating plugin for platform %s, version %s", platform.value, version) self._platform = platform @@ -187,7 +192,7 @@ class Plugin(): self._feature_methods.setdefault(feature, []).append(handler) async def run(self): - """Plugin main coorutine""" + """Plugin main coroutine.""" async def pass_control(): while self._active: try: @@ -199,7 +204,7 @@ class Plugin(): await asyncio.gather(pass_control(), self._server.run()) def _shutdown(self): - logging.info("Shuting down") + logging.info("Shutting down") self._server.stop() self._active = False self.shutdown() @@ -216,39 +221,115 @@ class Plugin(): pass # notifications - def store_credentials(self, credentials): - """Notify client to store plugin credentials. - They will be pass to next authencicate calls. - """ + def store_credentials(self, credentials: dict): + """Notify the client to store authentication credentials. + Credentials are passed on the next authenticate call. + + :param credentials: credentials that client will store; they are stored locally on a user pc + + Example use case of store_credentials: + + .. code-block:: python + :linenos: + + async def pass_login_credentials(self, step, credentials, cookies): + if self.got_everything(credentials,cookies): + user_data = await self.parse_credentials(credentials,cookies) + else: + next_params = self.get_next_params(credentials,cookies) + next_cookies = self.get_next_cookies(credentials,cookies) + return NextStep("web_session", next_params, cookies=next_cookies) + self.store_credentials(user_data['credentials']) + return Authentication(user_data['userId'], user_data['username']) + + """ self._notification_client.notify("store_credentials", credentials, sensitive_params=True) - def add_game(self, game): + def add_game(self, game: Game): + """Notify the client to add game to the list of owned games + of the currently authenticated user. + + :param game: Game to add to the list of owned games + + Example use case of add_game: + + .. code-block:: python + :linenos: + + async def check_for_new_games(self): + games = await self.get_owned_games() + for game in games: + if game not in self.owned_games_cache: + self.owned_games_cache.append(game) + self.add_game(game) + + """ params = {"owned_game" : game} self._notification_client.notify("owned_game_added", params) - def remove_game(self, game_id): + def remove_game(self, game_id: str): + """Notify the client to remove game from the list of owned games + of the currently authenticated user. + + :param game_id: game id of the game to remove from the list of owned games + + Example use case of remove_game: + + .. code-block:: python + :linenos: + + async def check_for_removed_games(self): + games = await self.get_owned_games() + for game in self.owned_games_cache: + if game not in games: + self.owned_games_cache.remove(game) + self.remove_game(game.game_id) + + """ params = {"game_id" : game_id} self._notification_client.notify("owned_game_removed", params) - def update_game(self, game): + def update_game(self, game: Game): + """Notify the client to update the status of a game + owned by the currently authenticated user. + + :param game: Game to update + """ params = {"owned_game" : game} self._notification_client.notify("owned_game_updated", params) - def unlock_achievement(self, game_id, achievement): + def unlock_achievement(self, game_id: str, achievement: Achievement): + """Notify the client to unlock an achievement for a specific game. + + :param game_id: game_id of the game for which to unlock an achievement. + :param achievement: achievement to unlock. + """ params = { "game_id": game_id, "achievement": achievement } self._notification_client.notify("achievement_unlocked", params) - def game_achievements_import_success(self, game_id, achievements): + def game_achievements_import_success(self, game_id: str, achievements): + """Notify the client that import of achievements for a given game has succeeded. + This method is called by import_games_achievements. + + :param game_id: id of the game for which the achievements were imported + :param achievements: list of imported achievements + """ params = { "game_id": game_id, "unlocked_achievements": achievements } self._notification_client.notify("game_achievements_import_success", params) - def game_achievements_import_failure(self, game_id, error): + def game_achievements_import_failure(self, game_id: str, error: ApplicationError): + """Notify the client that import of achievements for a given game has failed. + This method is called by import_games_achievements. + + :param game_id: id of the game for which the achievements import failed + :param error: error which prevented the achievements import + """ params = { "game_id": game_id, "error": { @@ -259,21 +340,60 @@ class Plugin(): self._notification_client.notify("game_achievements_import_failure", params) def achievements_import_finished(self): + """Notify the client that importing achievements has finished. + This method is called by import_games_achievements_task""" self._notification_client.notify("achievements_import_finished", None) - def update_local_game_status(self, local_game): + def update_local_game_status(self, local_game: LocalGame): + """Notify the client to update the status of a local game. + + :param local_game: the LocalGame to update + + Example use case triggered by the :meth:`.tick` method: + + .. code-block:: python + :linenos: + :emphasize-lines: 5 + + async def _check_statuses(self): + for game in await self._get_local_games(): + if game.status == self._cached_game_statuses.get(game.id): + continue + self.update_local_game_status(LocalGame(game.id, game.status)) + self._cached_games_statuses[game.id] = game.status + asyncio.sleep(5) # interval + + def tick(self): + if self._check_statuses_task is None or self._check_statuses_task.done(): + self._check_statuses_task = asyncio.create_task(self._check_statuses()) + """ params = {"local_game" : local_game} self._notification_client.notify("local_game_status_changed", params) - def add_friend(self, user): + def add_friend(self, user: FriendInfo): + """Notify the client to add a user to friends list of the currently authenticated user. + + :param user: FriendInfo of a user that the client will add to friends list + """ params = {"friend_info" : user} self._notification_client.notify("friend_added", params) - def remove_friend(self, user_id): + def remove_friend(self, user_id: str): + """Notify the client to remove a user from friends list of the currently authenticated user. + + :param user_id: id of the user to remove from friends list + """ params = {"user_id" : user_id} self._notification_client.notify("friend_removed", params) - def update_room(self, room_id, unread_message_count=None, new_messages=None): + def update_room(self, room_id: str, unread_message_count=None, new_messages=None): + """WIP, Notify the client to update the information regarding + a chat room that the currently authenticated user is in. + + :param room_id: id of the room to update + :param unread_message_count: information about the new unread message count in the room + :param new_messages: list of new messages that the user received + """ params = {"room_id": room_id} if unread_message_count is not None: params["unread_message_count"] = unread_message_count @@ -281,15 +401,30 @@ class Plugin(): params["messages"] = new_messages self._notification_client.notify("chat_room_updated", params) - def update_game_time(self, game_time): + def update_game_time(self, game_time: GameTime): + """Notify the client to update game time for a game. + + :param game_time: game time to update + """ params = {"game_time" : game_time} self._notification_client.notify("game_time_updated", params) - def game_time_import_success(self, game_time): + def game_time_import_success(self, game_time: GameTime): + """Notify the client that import of a given game_time has succeeded. + This method is called by import_game_times. + + :param game_time: game_time which was imported + """ params = {"game_time" : game_time} self._notification_client.notify("game_time_import_success", params) - def game_time_import_failure(self, game_id, error): + def game_time_import_failure(self, game_id: str, error: ApplicationError): + """Notify the client that import of a game time for a given game has failed. + This method is called by import_game_times. + + :param game_id: id of the game for which the game time could not be imported + :param error: error which prevented the game time import + """ params = { "game_id": game_id, "error": { @@ -300,42 +435,133 @@ class Plugin(): self._notification_client.notify("game_time_import_failure", params) def game_times_import_finished(self): + """Notify the client that importing game times has finished. + This method is called by :meth:`~.import_game_times_task`. + """ self._notification_client.notify("game_times_import_finished", None) def lost_authentication(self): + """Notify the client that integration has lost authentication for the + current user and is unable to perform actions which would require it. + """ self._notification_client.notify("authentication_lost", None) # handlers def tick(self): - """This method is called periodicaly. - Override it to implement periodical tasks like refreshing cache. - This method should not be blocking - any longer actions should be - handled by asycio tasks. + """This method is called periodically. + Override it to implement periodical non-blocking tasks. + This method is called internally. + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + def tick(self): + if not self.checking_for_new_games: + asyncio.create_task(self.check_for_new_games()) + if not self.checking_for_removed_games: + asyncio.create_task(self.check_for_removed_games()) + if not self.updating_game_statuses: + asyncio.create_task(self.update_game_statuses()) + """ def shutdown(self): - """This method is called on plugin shutdown. + """This method is called on integration shutdown. Override it to implement tear down. - """ + This method is called by the GOG Galaxy client.""" # methods - async def authenticate(self, stored_credentials=None): - """Overide this method to handle plugin authentication. - The method should return galaxy.api.types.Authentication - or raise galaxy.api.types.LoginError on authentication failure. + async def authenticate(self, stored_credentials:dict=None): + """Override this method to handle user authentication. + This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished + or :class:`~galaxy.api.types.NextStep` if it requires going to another url. + This method is called by the GOG Galaxy client. + + :param stored_credentials: If the client received any credentials to store locally + in the previous session they will be passed here as a parameter. + + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def authenticate(self, stored_credentials=None): + if not stored_credentials: + return NextStep("web_session", PARAMS, cookies=COOKIES) + else: + try: + user_data = self._authenticate(stored_credentials) + except AccessDenied: + raise InvalidCredentials() + return Authentication(user_data['userId'], user_data['username']) + """ raise NotImplementedError() - async def pass_login_credentials(self, step, credentials, cookies): + async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]): + """This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials. + This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on. + This method should either return galaxy.api.types.Authentication if the authentication is finished + or galaxy.api.types.NextStep if it requires going to another cef url. + This method is called by the GOG Galaxy client. + + :param step: deprecated. + :param credentials: end_uri previous NextStep finished on. + :param cookies: cookies extracted from the end_uri site. + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def pass_login_credentials(self, step, credentials, cookies): + if self.got_everything(credentials,cookies): + user_data = await self.parse_credentials(credentials,cookies) + else: + next_params = self.get_next_params(credentials,cookies) + next_cookies = self.get_next_cookies(credentials,cookies) + return NextStep("web_session", next_params, cookies=next_cookies) + self.store_credentials(user_data['credentials']) + return Authentication(user_data['userId'], user_data['username']) + + """ raise NotImplementedError() - async def get_owned_games(self): + async def get_owned_games(self) -> List[Game]: + """Override this method to return owned games for currenly logged in user. + This method is called by the GOG Galaxy client. + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def get_owned_games(self): + if not self.authenticated(): + raise AuthenticationRequired() + + games = self.retrieve_owned_games() + return games + + """ raise NotImplementedError() - async def get_unlocked_achievements(self, game_id): + async def get_unlocked_achievements(self, game_id: str) -> List[Achievement]: + """ + .. deprecated:: 0.33 + Use :meth:`~.import_games_achievements`. + """ raise NotImplementedError() - async def start_achievements_import(self, game_ids): + async def start_achievements_import(self, game_ids: List[str]): + """Starts the task of importing achievements. + This method is called by the GOG Galaxy client. + + :param game_ids: ids of the games for which the achievements are imported + """ if self._achievements_import_in_progress: raise ImportInProgress() @@ -349,8 +575,15 @@ class Plugin(): asyncio.create_task(import_games_achievements_task(game_ids)) self._achievements_import_in_progress = True - async def import_games_achievements(self, game_ids): - """Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list""" + async def import_games_achievements(self, game_ids: List[str]): + """ + Override this method to return the unlocked achievements + of the user that is currently logged in to the plugin. + Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list. + This method is called by the GOG Galaxy client. + + :param game_id: ids of the games for which to import unlocked achievements + """ async def import_game_achievements(game_id): try: achievements = await self.get_unlocked_achievements(game_id) @@ -361,43 +594,165 @@ class Plugin(): imports = [import_game_achievements(game_id) for game_id in game_ids] await asyncio.gather(*imports) - async def get_local_games(self): + async def get_local_games(self) -> List[LocalGame]: + """Override this method to return the list of + games present locally on the users pc. + This method is called by the GOG Galaxy client. + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def get_local_games(self): + local_games = [] + for game in self.games_present_on_user_pc: + local_game = LocalGame() + local_game.game_id = game.id + local_game.local_game_state = game.get_installation_status() + local_games.append(local_game) + return local_games + + """ raise NotImplementedError() - async def launch_game(self, game_id): + async def launch_game(self, game_id: str): + """Override this method to launch the game + identified by the provided game_id. + This method is called by the GOG Galaxy client. + + :param str game_id: id of the game to launch + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def launch_game(self, game_id): + await self.open_uri(f"start client://launchgame/{game_id}") + + """ raise NotImplementedError() - async def install_game(self, game_id): + async def install_game(self, game_id: str): + """Override this method to install the game + identified by the provided game_id. + This method is called by the GOG Galaxy client. + + :param str game_id: id of the game to install + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def install_game(self, game_id): + await self.open_uri(f"start client://installgame/{game_id}") + + """ raise NotImplementedError() - async def uninstall_game(self, game_id): + async def uninstall_game(self, game_id: str): + """Override this method to uninstall the game + identified by the provided game_id. + This method is called by the GOG Galaxy client. + + :param str game_id: id of the game to uninstall + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def uninstall_game(self, game_id): + await self.open_uri(f"start client://uninstallgame/{game_id}") + + """ raise NotImplementedError() - async def get_friends(self): + async def get_friends(self) -> List[FriendInfo]: + """Override this method to return the friends list + of the currently authenticated user. + This method is called by the GOG Galaxy client. + + Example of possible override of the method: + + .. code-block:: python + :linenos: + + async def get_friends(self): + if not self._http_client.is_authenticated(): + raise AuthenticationRequired() + + friends = self.retrieve_friends() + return friends + + """ raise NotImplementedError() - async def get_users(self, user_id_list): + async def get_users(self, user_id_list: List[str]) -> List[UserInfo]: + """WIP, Override this method to return the list of users matching the provided ids. + This method is called by the GOG Galaxy client. + + :param user_id_list: list of user ids + """ raise NotImplementedError() - async def send_message(self, room_id, message_text): + async def send_message(self, room_id: str, message_text: str): + """WIP, Override this method to send message to a chat room. + This method is called by the GOG Galaxy client. + + :param room_id: id of the room to which the message should be sent + :param message_text: text which should be sent in the message + """ raise NotImplementedError() - async def mark_as_read(self, room_id, last_message_id): + async def mark_as_read(self, room_id: str, last_message_id: str): + """WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter. + This method is called by the GOG Galaxy client. + + :param room_id: id of the room + :param last_message_id: id of the last message; room is marked as read only if this id matches the last message id known to the client + """ raise NotImplementedError() - async def get_rooms(self): + async def get_rooms(self) -> List[Room]: + """WIP, Override this method to return the chat rooms in which the user is currently in. + This method is called by the GOG Galaxy client + """ raise NotImplementedError() - async def get_room_history_from_message(self, room_id, message_id): + async def get_room_history_from_message(self, room_id: str, message_id: str): + """WIP, Override this method to return the chat room history since the message provided in parameter. + This method is called by the GOG Galaxy client. + + :param room_id: id of the room + :param message_id: id of the message since which the history should be retrieved + """ raise NotImplementedError() - async def get_room_history_from_timestamp(self, room_id, from_timestamp): + async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int): + """WIP, Override this method to return the chat room history since the timestamp provided in parameter. + This method is called by the GOG Galaxy client. + + :param room_id: id of the room + :param from_timestamp: timestamp since which the history should be retrieved + """ raise NotImplementedError() - async def get_game_times(self): + async def get_game_times(self) -> List[GameTime]: + """ + .. deprecated:: 0.33 + Use :meth:`~.import_game_times`. + """ raise NotImplementedError() - async def start_game_times_import(self, game_ids): + async def start_game_times_import(self, game_ids: List[str]): + """Starts the task of importing game times + This method is called by the GOG Galaxy client. + + :param game_ids: ids of the games for which the game time is imported + """ if self._game_times_import_in_progress: raise ImportInProgress() @@ -411,8 +766,15 @@ class Plugin(): asyncio.create_task(import_game_times_task(game_ids)) self._game_times_import_in_progress = True - async def import_game_times(self, game_ids): - """Call game_time_import_success/game_time_import_failure for each game_id on the list""" + async def import_game_times(self, game_ids: List[str]): + """ + Override this method to return game times for + games owned by the currently authenticated user. + Call game_time_import_success/game_time_import_failure for each game_id on the list. + This method is called by GOG Galaxy Client. + + :param game_ids: ids of the games for which the game time is imported + """ try: game_times = await self.get_game_times() game_ids_set = set(game_ids) @@ -427,7 +789,24 @@ class Plugin(): for game_id in game_ids: self.game_time_import_failure(game_id, error) + def create_and_run_plugin(plugin_class, argv): + """Call this method as an entry point for the implemented integration. + + :param plugin_class: your plugin class. + :param argv: command line arguments with which the script was started. + + Example of possible use of the method: + + .. code-block:: python + :linenos: + + def main(): + create_and_run_plugin(PlatformPlugin, sys.argv) + + if __name__ == "__main__": + main() + """ if len(argv) < 3: logging.critical("Not enough parameters, required: token, port") sys.exit(1) diff --git a/src/galaxy/api/types.py b/src/galaxy/api/types.py index 746f3a3..fb3b908 100644 --- a/src/galaxy/api/types.py +++ b/src/galaxy/api/types.py @@ -5,11 +5,24 @@ from galaxy.api.consts import LicenseType, LocalGameState, PresenceState @dataclass class Authentication(): + """Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` + to inform the client that authentication has successfully finished. + + :param user_id: id of the authenticated user + :param user_name: username of the authenticated user + """ user_id: str user_name: str @dataclass class Cookie(): + """Cookie + + :param name: name of the cookie + :param value: value of the cookie + :param domain: optional domain of the cookie + :param path: optional path of the cookie + """ name: str value: str domain: Optional[str] = None @@ -17,6 +30,39 @@ class Cookie(): @dataclass class NextStep(): + """Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url. + For example: + + .. code-block:: python + :linenos: + + PARAMS = { + "window_title": "Login to platform", + "window_width": 800, + "window_height": 600, + "start_uri": URL, + "end_uri_regex": r"^https://platform_website\.com/.*" + } + + JS = {r"^https://platform_website\.com/.*": [ + r''' + location.reload(); + ''' + ]} + + COOKIES = [Cookie("Cookie1", "ok", ".platform.com"), + Cookie("Cookie2", "ok", ".platform.com") + ] + + async def authenticate(self, stored_credentials=None): + if not stored_credentials: + return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS) + + :param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`} + :param cookies: browser initial set of cookies + :param js: a map of the url regex patterns into the list of *js* scripts that should be executed on every document at given step of internal browser authentication. + + """ next_step: str auth_params: Dict[str, str] cookies: Optional[List[Cookie]] = None @@ -24,17 +70,35 @@ class NextStep(): @dataclass class LicenseInfo(): + """Information about the license of related product. + + :param license_type: type of license + :param owner: optional owner of the related product, defaults to currently authenticated user + """ license_type: LicenseType owner: Optional[str] = None @dataclass class Dlc(): + """Downloadable content object. + + :param dlc_id: id of the dlc + :param dlc_title: title of the dlc + :param license_info: information about the license attached to the dlc + """ dlc_id: str dlc_title: str license_info: LicenseInfo @dataclass class Game(): + """Game object. + + :param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game + :param game_title: title of the game + :param dlcs: list of dlcs available for the game + :param license_info: information about the license attached to the game + """ game_id: str game_title: str dlcs: Optional[List[Dlc]] @@ -42,6 +106,12 @@ class Game(): @dataclass class Achievement(): + """Achievement, has to be initialized with either id or name. + + :param unlock_time: unlock time of the achievement + :param achievement_id: optional id of the achievement + :param achievement_name: optional name of the achievement + """ unlock_time: int achievement_id: Optional[str] = None achievement_name: Optional[str] = None @@ -52,17 +122,36 @@ class Achievement(): @dataclass class LocalGame(): + """Game locally present on the authenticated user's computer. + + :param game_id: id of the game + :param local_game_state: state of the game + """ game_id: str local_game_state: LocalGameState @dataclass class Presence(): + """Information about a presence of a user. + + :param presence_state: the state in which the user's presence is + :param game_id: id of the game which the user is currently playing + :param presence_status: optional attached string with the detailed description of the user's presence + """ presence_state: PresenceState game_id: Optional[str] = None presence_status: Optional[str] = None @dataclass class UserInfo(): + """Detailed information about a user. + + :param user_id: of the user + :param is_friend: whether the user is a friend of the currently authenticated user + :param user_name: of the user + :param avatar_url: to the avatar of the user + :param presence: about the users presence + """ user_id: str is_friend: bool user_name: str @@ -71,17 +160,35 @@ class UserInfo(): @dataclass class FriendInfo(): + """Information about a friend of the currently authenticated user. + + :param user_id: id of the user + :param user_name: username of the user + """ user_id: str user_name: str @dataclass class Room(): + """WIP, Chatroom. + + :param room_id: id of the room + :param unread_message_count: number of unread messages in the room + :param last_message_id: id of the last message in the room + """ room_id: str unread_message_count: int last_message_id: str @dataclass class Message(): + """WIP, A chatroom message. + + :param message_id: id of the message + :param sender_id: id of the sender of the message + :param sent_time: time at which the message was sent + :param message_text: text attached to the message + """ message_id: str sender_id: str sent_time: int @@ -89,6 +196,13 @@ class Message(): @dataclass class GameTime(): + """Game time of a game, defines the total time spent in the game + and the last time the game was played. + + :param game_id: id of the related game + :param time_played: the total time spent in the game in **minutes** + :param last_time_played: last time the game was played (**unix timestamp**) + """ game_id: str time_played: int last_played_time: int From 79808e49f7a7abdf58ad7a3946439983497ce563 Mon Sep 17 00:00:00 2001 From: Mieszko Banczerowski Date: Fri, 7 Jun 2019 14:02:23 +0200 Subject: [PATCH 108/147] Bump version after adding documentation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f4de87..513ede7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.31.3", + version="0.32.0", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 5ca9254d2aced98059661c446c9a2a31a91e2cf8 Mon Sep 17 00:00:00 2001 From: Mieszko Banczerowski Date: Mon, 10 Jun 2019 18:53:02 +0200 Subject: [PATCH 109/147] GPI-438: Add link to docs in readme.md; fix make.py --- README.md | 10 +++++++--- docs/make.py | 15 +++++++++------ docs/source/overview.rst | 6 ++++++ setup.py | 2 +- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fbbb420..326c283 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# GOG Galaxy - Community Integration - Python API +# GOG Galaxy Integrations Python API This Python library allows to easily build community integrations for various gaming platforms with GOG Galaxy 2.0. -Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that needs to communicate with main instance of GOG Galaxy 2.0. +- refer to our documentation + +## Features + +Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that which needs to communicate with main instance of GOG Galaxy 2.0. The provided features are: @@ -69,4 +73,4 @@ The additional `manifest.json` file is required: ``` ## Legal Notice -By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0. you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. +By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0 you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. diff --git a/docs/make.py b/docs/make.py index cb41897..42f6a02 100644 --- a/docs/make.py +++ b/docs/make.py @@ -1,13 +1,16 @@ """Builds documentation locally. Use for preview only""" +import pathlib import subprocess import webbrowser -source = "docs/source" -build = "docs/build" -master_doc = 'index' +source = pathlib.Path("docs", "source") +build = pathlib.Path("docs", "build") +master_doc = 'index.html' -subprocess.run(['sphinx-build', '-M', 'clean', source, build]) -subprocess.run(['sphinx-build', '-M', 'html', source, build]) -webbrowser.open(f'{build}/html/{master_doc}') \ No newline at end of file +subprocess.run(['sphinx-build', '-M', 'clean', str(source), str(build)]) +subprocess.run(['sphinx-build', '-M', 'html', str(source), str(build)]) + +master_path = build / 'html' / master_doc +webbrowser.open(f'file://{master_path.resolve()}') \ No newline at end of file diff --git a/docs/source/overview.rst b/docs/source/overview.rst index 3bd447c..cb8accb 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -1 +1,7 @@ .. mdinclude:: ../../README.md + :end-line: 4 + +.. excluding self-pointing documentation link + +.. mdinclude:: ../../README.md + :start-line: 6 diff --git a/setup.py b/setup.py index 513ede7..850b589 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.32.0", + version="0.32.1", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From f6522be74d0330eb0f4ee3ed05bf7aaf23c1ffdf Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Wed, 12 Jun 2019 17:13:41 +0200 Subject: [PATCH 110/147] SDK-2874: Persistent cache --- src/galaxy/api/plugin.py | 99 ++++++++++++++++++++++------------ tests/conftest.py | 1 + tests/test_persistent_cache.py | 71 ++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 tests/test_persistent_cache.py diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 9b0dc71..0b7f2a2 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -15,8 +15,9 @@ from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError from galaxy.api.consts import Feature from galaxy.api.errors import UnknownError, ImportInProgress + class JSONEncoder(json.JSONEncoder): - def default(self, o): # pylint: disable=method-hidden + def default(self, o): # pylint: disable=method-hidden if dataclasses.is_dataclass(o): # filter None values def dict_factory(elements): @@ -26,7 +27,8 @@ class JSONEncoder(json.JSONEncoder): return o.value return super().default(o) -class Plugin(): + +class Plugin: """Use and override methods of this class to create a new platform integration.""" def __init__(self, platform, version, reader, writer, handshake_token): logging.info("Creating plugin for platform %s, version %s", platform.value, version) @@ -50,9 +52,12 @@ class Plugin(): self._achievements_import_in_progress = False self._game_times_import_in_progress = False + self._persistent_cache = dict() + # internal self._register_method("shutdown", self._shutdown, internal=True) self._register_method("get_capabilities", self._get_capabilities, internal=True) + self._register_method("initialize_cache", self._initialize_cache, internal=True) self._register_method("ping", self._ping, internal=True) # implemented by developer @@ -156,6 +161,12 @@ class Plugin(): return features + @property + def persistent_cache(self) -> Dict: + """The cache is only available after the :meth:`~.handshake_complete()` is called. + """ + return self._persistent_cache + def _implements(self, handlers): for handler in handlers: if handler.__name__ not in self.__class__.__dict__: @@ -192,7 +203,7 @@ class Plugin(): self._feature_methods.setdefault(feature, []).append(handler) async def run(self): - """Plugin main coroutine.""" + """Plugin's main coroutine.""" async def pass_control(): while self._active: try: @@ -216,6 +227,10 @@ class Plugin(): "token": self._handshake_token } + def _initialize_cache(self, data: Dict): + self._persistent_cache = data + self.handshake_complete() + @staticmethod def _ping(): pass @@ -264,7 +279,7 @@ class Plugin(): self.add_game(game) """ - params = {"owned_game" : game} + params = {"owned_game": game} self._notification_client.notify("owned_game_added", params) def remove_game(self, game_id: str): @@ -286,7 +301,7 @@ class Plugin(): self.remove_game(game.game_id) """ - params = {"game_id" : game_id} + params = {"game_id": game_id} self._notification_client.notify("owned_game_removed", params) def update_game(self, game: Game): @@ -295,7 +310,7 @@ class Plugin(): :param game: Game to update """ - params = {"owned_game" : game} + params = {"owned_game": game} self._notification_client.notify("owned_game_updated", params) def unlock_achievement(self, game_id: str, achievement: Achievement): @@ -367,7 +382,7 @@ class Plugin(): if self._check_statuses_task is None or self._check_statuses_task.done(): self._check_statuses_task = asyncio.create_task(self._check_statuses()) """ - params = {"local_game" : local_game} + params = {"local_game": local_game} self._notification_client.notify("local_game_status_changed", params) def add_friend(self, user: FriendInfo): @@ -375,7 +390,7 @@ class Plugin(): :param user: FriendInfo of a user that the client will add to friends list """ - params = {"friend_info" : user} + params = {"friend_info": user} self._notification_client.notify("friend_added", params) def remove_friend(self, user_id: str): @@ -383,7 +398,7 @@ class Plugin(): :param user_id: id of the user to remove from friends list """ - params = {"user_id" : user_id} + params = {"user_id": user_id} self._notification_client.notify("friend_removed", params) def update_room(self, room_id: str, unread_message_count=None, new_messages=None): @@ -406,7 +421,7 @@ class Plugin(): :param game_time: game time to update """ - params = {"game_time" : game_time} + params = {"game_time": game_time} self._notification_client.notify("game_time_updated", params) def game_time_import_success(self, game_time: GameTime): @@ -415,7 +430,7 @@ class Plugin(): :param game_time: game_time which was imported """ - params = {"game_time" : game_time} + params = {"game_time": game_time} self._notification_client.notify("game_time_import_success", params) def game_time_import_failure(self, game_id: str, error: ApplicationError): @@ -446,7 +461,22 @@ class Plugin(): """ self._notification_client.notify("authentication_lost", None) + def push_cache(self): + """Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one. + """ + self._notification_client.notify( + "push_cache", + params={"data": self._persistent_cache} + ) + # handlers + def handshake_complete(self): + """This method is called right after the handshake with the GOG Galaxy Client is complete and + before any other operations are called by the GOG Galaxy Client. + Persistent cache is available when this method is called. + Override it if you need to do additional plugin initializations. + This method is called internally.""" + def tick(self): """This method is called periodically. Override it to implement periodical non-blocking tasks. @@ -470,14 +500,14 @@ class Plugin(): def shutdown(self): """This method is called on integration shutdown. Override it to implement tear down. - This method is called by the GOG Galaxy client.""" + This method is called by the GOG Galaxy Client.""" # methods - async def authenticate(self, stored_credentials:dict=None): + async def authenticate(self, stored_credentials: dict = None): """Override this method to handle user authentication. This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished or :class:`~galaxy.api.types.NextStep` if it requires going to another url. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param stored_credentials: If the client received any credentials to store locally in the previous session they will be passed here as a parameter. @@ -506,7 +536,7 @@ class Plugin(): This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on. This method should either return galaxy.api.types.Authentication if the authentication is finished or galaxy.api.types.NextStep if it requires going to another cef url. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param step: deprecated. :param credentials: end_uri previous NextStep finished on. @@ -531,8 +561,8 @@ class Plugin(): raise NotImplementedError() async def get_owned_games(self) -> List[Game]: - """Override this method to return owned games for currenly logged in user. - This method is called by the GOG Galaxy client. + """Override this method to return owned games for currently logged in user. + This method is called by the GOG Galaxy Client. Example of possible override of the method: @@ -558,7 +588,7 @@ class Plugin(): async def start_achievements_import(self, game_ids: List[str]): """Starts the task of importing achievements. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param game_ids: ids of the games for which the achievements are imported """ @@ -580,9 +610,9 @@ class Plugin(): Override this method to return the unlocked achievements of the user that is currently logged in to the plugin. Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. - :param game_id: ids of the games for which to import unlocked achievements + :param game_ids: ids of the games for which to import unlocked achievements """ async def import_game_achievements(game_id): try: @@ -597,7 +627,7 @@ class Plugin(): async def get_local_games(self) -> List[LocalGame]: """Override this method to return the list of games present locally on the users pc. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. Example of possible override of the method: @@ -619,7 +649,7 @@ class Plugin(): async def launch_game(self, game_id: str): """Override this method to launch the game identified by the provided game_id. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param str game_id: id of the game to launch @@ -637,7 +667,7 @@ class Plugin(): async def install_game(self, game_id: str): """Override this method to install the game identified by the provided game_id. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param str game_id: id of the game to install @@ -655,7 +685,7 @@ class Plugin(): async def uninstall_game(self, game_id: str): """Override this method to uninstall the game identified by the provided game_id. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param str game_id: id of the game to uninstall @@ -673,7 +703,7 @@ class Plugin(): async def get_friends(self) -> List[FriendInfo]: """Override this method to return the friends list of the currently authenticated user. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. Example of possible override of the method: @@ -692,7 +722,7 @@ class Plugin(): async def get_users(self, user_id_list: List[str]) -> List[UserInfo]: """WIP, Override this method to return the list of users matching the provided ids. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param user_id_list: list of user ids """ @@ -700,7 +730,7 @@ class Plugin(): async def send_message(self, room_id: str, message_text: str): """WIP, Override this method to send message to a chat room. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param room_id: id of the room to which the message should be sent :param message_text: text which should be sent in the message @@ -709,22 +739,23 @@ class Plugin(): async def mark_as_read(self, room_id: str, last_message_id: str): """WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param room_id: id of the room - :param last_message_id: id of the last message; room is marked as read only if this id matches the last message id known to the client + :param last_message_id: id of the last message; room is marked as read only if this id matches + the last message id known to the client """ raise NotImplementedError() async def get_rooms(self) -> List[Room]: """WIP, Override this method to return the chat rooms in which the user is currently in. - This method is called by the GOG Galaxy client + This method is called by the GOG Galaxy Client """ raise NotImplementedError() async def get_room_history_from_message(self, room_id: str, message_id: str): """WIP, Override this method to return the chat room history since the message provided in parameter. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param room_id: id of the room :param message_id: id of the message since which the history should be retrieved @@ -733,7 +764,7 @@ class Plugin(): async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int): """WIP, Override this method to return the chat room history since the timestamp provided in parameter. - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param room_id: id of the room :param from_timestamp: timestamp since which the history should be retrieved @@ -749,7 +780,7 @@ class Plugin(): async def start_game_times_import(self, game_ids: List[str]): """Starts the task of importing game times - This method is called by the GOG Galaxy client. + This method is called by the GOG Galaxy Client. :param game_ids: ids of the games for which the game time is imported """ @@ -829,7 +860,7 @@ def create_and_run_plugin(plugin_class, argv): async def coroutine(): reader, writer = await asyncio.open_connection("127.0.0.1", port) - extra_info = writer.get_extra_info('sockname') + extra_info = writer.get_extra_info("sockname") logging.info("Using local address: %s:%u", *extra_info) plugin = plugin_class(reader, writer, token) await plugin.run() diff --git a/tests/conftest.py b/tests/conftest.py index fed2e87..d373c32 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,6 +33,7 @@ def write(writer): def plugin(reader, writer): """Return plugin instance with all feature methods mocked""" async_methods = ( + "handshake_complete", "authenticate", "get_owned_games", "get_unlocked_achievements", diff --git a/tests/test_persistent_cache.py b/tests/test_persistent_cache.py new file mode 100644 index 0000000..056ddb5 --- /dev/null +++ b/tests/test_persistent_cache.py @@ -0,0 +1,71 @@ +import asyncio +import json + +import pytest + + +def assert_rpc_response(write, response_id, result=None): + assert json.loads(write.call_args[0][0]) == { + "jsonrpc": "2.0", + "id": str(response_id), + "result": result + } + + +def assert_rpc_request(write, method, params=None): + assert json.loads(write.call_args[0][0]) == { + "jsonrpc": "2.0", + "method": method, + "params": {"data": params} + } + + +@pytest.fixture +def cache_data(): + return { + "persistent key": "persistent value", + "persistent object": {"answer to everything": 42} + } + + +def test_initialize_cache(plugin, readline, write, cache_data): + request_id = 3 + request = { + "jsonrpc": "2.0", + "id": str(request_id), + "method": "initialize_cache", + "params": {"data": cache_data} + } + readline.side_effect = [json.dumps(request)] + + assert {} == plugin.persistent_cache + asyncio.run(plugin.run()) + plugin.handshake_complete.assert_called_once_with() + assert cache_data == plugin.persistent_cache + assert_rpc_response(write, response_id=request_id) + + +def test_set_cache(plugin, write, cache_data): + async def runner(): + assert {} == plugin.persistent_cache + + plugin.persistent_cache.update(cache_data) + plugin.push_cache() + + assert_rpc_request(write, "push_cache", cache_data) + assert cache_data == plugin.persistent_cache + + asyncio.run(runner()) + + +def test_clear_cache(plugin, write, cache_data): + async def runner(): + plugin._persistent_cache = cache_data + + plugin.persistent_cache.clear() + plugin.push_cache() + + assert_rpc_request(write, "push_cache", {}) + assert {} == plugin.persistent_cache + + asyncio.run(runner()) From 8647b06ca251984f846ba071472958b5ce3181ca Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Wed, 12 Jun 2019 16:41:08 +0200 Subject: [PATCH 111/147] Add TooManyRequests error --- src/galaxy/api/errors.py | 6 +++++- src/galaxy/http.py | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/galaxy/api/errors.py b/src/galaxy/api/errors.py index 189db00..5a23002 100644 --- a/src/galaxy/api/errors.py +++ b/src/galaxy/api/errors.py @@ -22,6 +22,10 @@ class UnknownBackendResponse(ApplicationError): def __init__(self, data=None): super().__init__(4, "Backend responded in uknown way", data) +class TooManyRequests(ApplicationError): + def __init__(self, data=None): + super().__init__(5, "Too many requests. Try again later", data) + class InvalidCredentials(ApplicationError): def __init__(self, data=None): super().__init__(100, "Invalid credentials", data) @@ -80,4 +84,4 @@ class MessageNotFound(ApplicationError): class ImportInProgress(ApplicationError): def __init__(self, data=None): - super().__init__(600, "Import already in progress", data) \ No newline at end of file + super().__init__(600, "Import already in progress", data) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 5b494ca..0e7b8b2 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -6,10 +6,11 @@ import aiohttp import certifi from galaxy.api.errors import ( - AccessDenied, AuthenticationRequired, - BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError + AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError, + TooManyRequests, UnknownBackendResponse, UnknownError ) + class HttpClient: def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) @@ -39,6 +40,8 @@ class HttpClient: raise AccessDenied() if response.status == HTTPStatus.SERVICE_UNAVAILABLE: raise BackendNotAvailable() + if response.status == HTTPStatus.TOO_MANY_REQUESTS: + raise TooManyRequests() if response.status >= 500: raise BackendError() if response.status >= 400: From 2e90c663908e887f9fdfcf591cf36470d42e1055 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Thu, 13 Jun 2019 12:21:34 +0200 Subject: [PATCH 112/147] Remove not used errors --- src/galaxy/api/errors.py | 12 ------------ tests/test_authenticate.py | 6 +----- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/galaxy/api/errors.py b/src/galaxy/api/errors.py index 5a23002..6564b48 100644 --- a/src/galaxy/api/errors.py +++ b/src/galaxy/api/errors.py @@ -54,18 +54,6 @@ class AccessDenied(ApplicationError): def __init__(self, data=None): super().__init__(106, "Access denied", data) -class ParentalControlBlock(ApplicationError): - def __init__(self, data=None): - super().__init__(107, "Parental control block", data) - -class DeviceBlocked(ApplicationError): - def __init__(self, data=None): - super().__init__(108, "Device blocked", data) - -class RegionBlocked(ApplicationError): - def __init__(self, data=None): - super().__init__(109, "Region blocked", data) - class FailedParsingManifest(ApplicationError): def __init__(self, data=None): super().__init__(200, "Failed parsing manifest", data) diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 4c25ba5..6cb96a6 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -6,8 +6,7 @@ import pytest from galaxy.api.types import Authentication from galaxy.api.errors import ( UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError, - BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied, - ParentalControlBlock, DeviceBlocked, RegionBlocked + BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied ) def test_success(plugin, readline, write): @@ -44,9 +43,6 @@ def test_success(plugin, readline, write): pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"), pytest.param(Banned, 105, "Banned", id="banned"), pytest.param(AccessDenied, 106, "Access denied", id="access_denied"), - pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"), - pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"), - pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked") ]) def test_failure(plugin, readline, write, error, code, message): request = { From 9b4537c54f5934e8625802fb3e03e752ea0d7325 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Thu, 13 Jun 2019 12:25:57 +0200 Subject: [PATCH 113/147] Fix anonimize params method --- src/galaxy/api/jsonrpc.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 45f1b0d..31a80dd 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -54,17 +54,15 @@ Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_p def anonymise_sensitive_params(params, sensitive_params): anomized_data = "****" - if not sensitive_params: - return params + + if isinstance(sensitive_params, bool): + if sensitive_params: + return {k:anomized_data for k,v in params.items()} if isinstance(sensitive_params, Iterable): - anomized_params = params.copy() - for key in anomized_params.keys(): - if key in sensitive_params: - anomized_params[key] = anomized_data - return anomized_params + return {k: anomized_data if k in sensitive_params else v for k, v in params.items()} - return anomized_data + return params class Server(): def __init__(self, reader, writer, encoder=json.JSONEncoder()): From 909cc107622c34ee70c3691233e9894e03f1f6be Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Thu, 13 Jun 2019 12:25:56 +0200 Subject: [PATCH 114/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 850b589..1a58faa 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.32.1", + version="0.33", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From bb482d4ed60e507b429816b37bccb6a152d283cb Mon Sep 17 00:00:00 2001 From: Mieszko Banczerowski Date: Fri, 14 Jun 2019 11:51:30 +0200 Subject: [PATCH 115/147] SDK-2872 update documentation about plugin deployment --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 326c283..fcd0775 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,20 @@ if __name__ == "__main__": ## Deployment -The client has a built-in Python 3.7 interpreter, so the integrations are delivered as `.py` files. -The additional `manifest.json` file is required: +The client has a built-in Python 3.7 interpreter, so the integrations are delivered as python modules. +In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the python files, the integration folder has to contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [examplary structure](#deploy-structure-example). + +### Lookup directory: +- Windows: + + `%localappdata%\GOG.com\Galaxy\plugins\installed` + +- macOS: + + `~/Library/Application Support/GOG.com/Galaxy/plugins/installed` + +### Manifest +Obligatory JSON file to be placed in a integration folder. ```json { @@ -71,6 +83,32 @@ The additional `manifest.json` file is required: "script": "plugin.py" } ``` +| property | description | +|---------------|---| +| `guid` | | +| `description` | | +| `url` | | +| `script` | path of the entry point module, relative to the integration folder | + +### Dependencies +All third-party packages (packages not included in Python 3.7 standard library) should be deployed along with plugin files. Use the folowing command structure: + +```pip install DEP --target DIR --implementation cp --python-version 37``` + +For example plugin that uses *requests* has structure as follows: + + +```bash +installed +└── my_integration +    ├── galaxy +    │   └── api +    ├── requests +    │   └── ... +    ├── plugin.py + └── manifest.json +``` + ## Legal Notice By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0 you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws. From 9e8748b032413d60638f5cb0665dd617290743b3 Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 14 Jun 2019 14:41:34 +0200 Subject: [PATCH 116/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1a58faa..02ae4a5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.33", + version="0.33.1", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From ed1049b5435fb5cc9c5e2089c3bb75f49c2b253d Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 14 Jun 2019 16:32:07 +0200 Subject: [PATCH 117/147] Write down unexpected responses from http client --- src/galaxy/http.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/galaxy/http.py b/src/galaxy/http.py index 0e7b8b2..667f55a 100644 --- a/src/galaxy/http.py +++ b/src/galaxy/http.py @@ -4,6 +4,7 @@ from http import HTTPStatus import aiohttp import certifi +import logging from galaxy.api.errors import ( AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError, @@ -21,9 +22,9 @@ class HttpClient: async def close(self): await self._session.close() - async def request(self, method, *args, **kwargs): + async def request(self, method, url, *args, **kwargs): try: - response = await self._session.request(method, *args, **kwargs) + response = await self._session.request(method, url, *args, **kwargs) except asyncio.TimeoutError: raise BackendTimeout() except aiohttp.ServerDisconnectedError: @@ -33,6 +34,8 @@ class HttpClient: except aiohttp.ContentTypeError: raise UnknownBackendResponse() except aiohttp.ClientError: + logging.exception( + "Caught exception while running {} request for {}".format(method, url)) raise UnknownError() if response.status == HTTPStatus.UNAUTHORIZED: raise AuthenticationRequired() @@ -45,6 +48,8 @@ class HttpClient: if response.status >= 500: raise BackendError() if response.status >= 400: + logging.warning( + "Got status {} while running {} request for {}".format(response.status, method, url)) raise UnknownError() return response From da59670d8ec0b4cf6377558f8c85f281ce74124c Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 14 Jun 2019 16:35:20 +0200 Subject: [PATCH 118/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02ae4a5..f75584e 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.33.1", + version="0.34", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 69ffef2fde70daa5cf3b06b3774724593296be40 Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Fri, 14 Jun 2019 19:25:08 +0200 Subject: [PATCH 119/147] create platform id list --- PLATFORM_IDs.md | 1 + README.md | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 PLATFORM_IDs.md diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md new file mode 100644 index 0000000..bb05a36 --- /dev/null +++ b/PLATFORM_IDs.md @@ -0,0 +1 @@ +### PLATFORM ID LIST diff --git a/README.md b/README.md index fcd0775..978a6e8 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,12 @@ The provided features are: - receiving and sending chat messages - cache storage +## Platform Id's + +Each integration can implement only one platform. + +[Contribution guidelines for this project](PLATFORM_IDs.md) + ## Basic usage Eeach integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client in the appropriate times. From 6dec4a99d3524c350cb67529dc40532fd07aad9e Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Fri, 14 Jun 2019 19:30:40 +0200 Subject: [PATCH 120/147] add platforms list --- PLATFORM_IDs.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md index bb05a36..1c5c72a 100644 --- a/PLATFORM_IDs.md +++ b/PLATFORM_IDs.md @@ -1 +1,80 @@ ### PLATFORM ID LIST + +| ID | Name | +| --- | --- | +| steam | Steam | +| psn | Playstation Network | +| xboxone | Xbox Live | +| generic | Manually added games | +| origin | EA Origin | +| uplay | Uplay | +| battlenet | Battle.net | +| epic | Epic Games Store | +| bethesda | Bethesda | +| paradox | Paradox Plaza | +| humble | Humble Bundle | +| kartridge | Kartridge | +| itch | Itch.io | +| nswitch | Nintendo Switch | +| nwiiu | Nintendo Wii U | +| nwii | Nintendo Wii | +| ncube | Nintendo Game Cube | +| riot | Riot | +| wargaming | Wargaming | +| ngameboy | Nintendo Game Boy | +| atari | Atari | +| amiga | Amiga | +| snes | Super Nintendo Entertainment System | +| beamdog | Beamdog | +| d2d | Direct2Drive | +| discord | Discord | +| dotemu | DotEmu | +| gamehouse | GameHouse | +| gmg | Green Man Gaming | +| weplay | WePlay | +| zx | Zx Spectrum PC | +| vision | ColecoVision and Vision family consoles | +| nes | Nintendo Entertainment System | +| sms | Sega Master System | +| c64 | Commodore 64 | +| pce | PC Engine | +| segag | Sega Genesis | +| neo | NeoGeo | +| sega32 | Sega 32X | +| segacd | Sega CD | +| 3do | 3DO Interactive | +| saturn | SegaSaturn | +| psx | Sony PlayStation | +| ps2 | Sony PlayStation 2 | +| n64 | Nintendo64 | +| jaguar | Atari Jaguar | +| dc | Sega Dreamcast | +| xboxog | Original Xbox games | +| amazon | Amazon | +| gg | GamersGate | +| egg | Newegg | +| bb | BestBuy | +| gameuk | Game UK | +| fanatical | Fanatical store | +| playasia | PlayAsia | +| stadia | Google Stadia | +| arc | ARC - Perfect World launcher | +| eso | Elder Scrolls Online launcher | +| glyph | Trion World launcher | +| aionl | Aion: Legions of War | +| aion | Normal mmo Aion launcher | +| blade | Blade and Soul mmo launcher | +| gw | Guild Wars launcher | +| gw2 | Guild Wars 2 launcher | +| lin2 | Lineage 2 launcher | +| ffxi | Final Fantasy XI launcher | +| ffxiv | Final Fantasy XIV launcher | +| totalwar | TotalWar series launcher | +| winstore | Windows store | +| elites | Elite Dangerous | +| star | Star Citizen launcher | +| psp | Playstation Portable | +| psvita | Playstation Vita | +| nds | Nintendo DS | +| 3ds | Nintendo 3DS | + From be3d3bb7e58c7ea3088aaee7ae9eca4b467da3aa Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Sat, 15 Jun 2019 10:16:04 +0200 Subject: [PATCH 121/147] text cleanup --- PLATFORM_IDs.md | 2 ++ README.md | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md index 1c5c72a..f23acab 100644 --- a/PLATFORM_IDs.md +++ b/PLATFORM_IDs.md @@ -1,5 +1,7 @@ ### PLATFORM ID LIST +Platform ID list for GOG Galaxy 2.0 Integrations + | ID | Name | | --- | --- | | steam | Steam | diff --git a/README.md b/README.md index 978a6e8..7ef7d1a 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ The provided features are: ## Platform Id's -Each integration can implement only one platform. +Each integration can implement only one platform. Each integration must declare which platform it's integrating. -[Contribution guidelines for this project](PLATFORM_IDs.md) +[List of possible Platofrm IDs](PLATFORM_IDs.md) ## Basic usage From b28fc60088f3606587cc29ea317464f88526a331 Mon Sep 17 00:00:00 2001 From: Pawel Czoppa Date: Sat, 15 Jun 2019 14:57:20 +0200 Subject: [PATCH 122/147] include Platform id's in sphinx generated documentation + some fixed --- README.md | 6 ++++-- docs/source/index.rst | 1 + docs/source/overview.rst | 8 ++++++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7ef7d1a..4330b1f 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,8 @@ if __name__ == "__main__": The client has a built-in Python 3.7 interpreter, so the integrations are delivered as python modules. In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the python files, the integration folder has to contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [examplary structure](#deploy-structure-example). -### Lookup directory: +### Lookup directory + - Windows: `%localappdata%\GOG.com\Galaxy\plugins\installed` @@ -73,7 +74,8 @@ In order to be found by GOG Galaxy 2.0 an integration folder should be placed in `~/Library/Application Support/GOG.com/Galaxy/plugins/installed` -### Manifest +### Manifest + Obligatory JSON file to be placed in a integration folder. ```json diff --git a/docs/source/index.rst b/docs/source/index.rst index 05f97f7..4455599 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,7 @@ GOG Galaxy Integrations Python API Overview API + Platform ID's Index ------------------- diff --git a/docs/source/overview.rst b/docs/source/overview.rst index cb8accb..ea506a8 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -5,3 +5,11 @@ .. mdinclude:: ../../README.md :start-line: 6 + :end-line: 26 + +.. excluding Platforms Id's link + +:ref:`platforms-link`. + +.. mdinclude:: ../../README.md + :start-line: 28 From ce9f33f5d0bf140af57a4653e5017c7b949939bb Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Sat, 15 Jun 2019 15:05:12 +0200 Subject: [PATCH 123/147] platform names clieanup --- PLATFORM_IDs.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md index f23acab..99e0832 100644 --- a/PLATFORM_IDs.md +++ b/PLATFORM_IDs.md @@ -5,14 +5,14 @@ Platform ID list for GOG Galaxy 2.0 Integrations | ID | Name | | --- | --- | | steam | Steam | -| psn | Playstation Network | +| psn | PlayStation Network | | xboxone | Xbox Live | | generic | Manually added games | -| origin | EA Origin | +| origin | Origin | | uplay | Uplay | | battlenet | Battle.net | | epic | Epic Games Store | -| bethesda | Bethesda | +| bethesda | Bethesda.net | | paradox | Paradox Plaza | | humble | Humble Bundle | | kartridge | Kartridge | @@ -26,7 +26,7 @@ Platform ID list for GOG Galaxy 2.0 Integrations | ngameboy | Nintendo Game Boy | | atari | Atari | | amiga | Amiga | -| snes | Super Nintendo Entertainment System | +| snes | SNES | | beamdog | Beamdog | | d2d | Direct2Drive | | discord | Discord | @@ -35,8 +35,8 @@ Platform ID list for GOG Galaxy 2.0 Integrations | gmg | Green Man Gaming | | weplay | WePlay | | zx | Zx Spectrum PC | -| vision | ColecoVision and Vision family consoles | -| nes | Nintendo Entertainment System | +| vision | ColecoVision | +| nes | NES | | sms | Sega Master System | | c64 | Commodore 64 | | pce | PC Engine | @@ -60,21 +60,21 @@ Platform ID list for GOG Galaxy 2.0 Integrations | fanatical | Fanatical store | | playasia | PlayAsia | | stadia | Google Stadia | -| arc | ARC - Perfect World launcher | -| eso | Elder Scrolls Online launcher | -| glyph | Trion World launcher | +| arc | ARC | +| eso | ESO | +| glyph | Trion World | | aionl | Aion: Legions of War | -| aion | Normal mmo Aion launcher | -| blade | Blade and Soul mmo launcher | -| gw | Guild Wars launcher | -| gw2 | Guild Wars 2 launcher | -| lin2 | Lineage 2 launcher | -| ffxi | Final Fantasy XI launcher | -| ffxiv | Final Fantasy XIV launcher | -| totalwar | TotalWar series launcher | -| winstore | Windows store | +| aion | Aion | +| blade | Blade and Soul | +| gw | Guild Wars | +| gw2 | Guild Wars | +| lin2 | Lineage 2 | +| ffxi | Final Fantasy XI | +| ffxiv | Final Fantasy XIV | +| totalwar | TotalWar | +| winstore | Windows Store | | elites | Elite Dangerous | -| star | Star Citizen launcher | +| star | Star Citizen | | psp | Playstation Portable | | psvita | Playstation Vita | | nds | Nintendo DS | From d6f2d00fb9bd7f7ef371beb396da3d3ca96838c9 Mon Sep 17 00:00:00 2001 From: Pawel Czoppa Date: Sat, 15 Jun 2019 15:11:05 +0200 Subject: [PATCH 124/147] typo --- PLATFORM_IDs.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md index 99e0832..c58dbc4 100644 --- a/PLATFORM_IDs.md +++ b/PLATFORM_IDs.md @@ -66,11 +66,11 @@ Platform ID list for GOG Galaxy 2.0 Integrations | aionl | Aion: Legions of War | | aion | Aion | | blade | Blade and Soul | -| gw | Guild Wars | -| gw2 | Guild Wars | +| gw | Guild Wars | +| gw2 | Guild Wars 2 | | lin2 | Lineage 2 | | ffxi | Final Fantasy XI | -| ffxiv | Final Fantasy XIV | +| ffxiv | Final Fantasy XIV | | totalwar | TotalWar | | winstore | Windows Store | | elites | Elite Dangerous | From d74ed3a4b5e3e475f0b3460b3ccd17bac5b85c48 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Sat, 15 Jun 2019 15:37:19 +0200 Subject: [PATCH 125/147] Hide persistent cache data --- src/galaxy/api/plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 0b7f2a2..b876c57 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -57,7 +57,12 @@ class Plugin: # internal self._register_method("shutdown", self._shutdown, internal=True) self._register_method("get_capabilities", self._get_capabilities, internal=True) - self._register_method("initialize_cache", self._initialize_cache, internal=True) + self._register_method( + "initialize_cache", + self._initialize_cache, + internal=True, + sensitive_params="data" + ) self._register_method("ping", self._ping, internal=True) # implemented by developer @@ -466,7 +471,8 @@ class Plugin: """ self._notification_client.notify( "push_cache", - params={"data": self._persistent_cache} + params={"data": self._persistent_cache}, + sensitive_params="data" ) # handlers From 5d90ba0c090cbe755121cb91f7403959bd87c848 Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Sat, 15 Jun 2019 15:39:52 +0200 Subject: [PATCH 126/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f75584e..a657b90 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.34", + version="0.35", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 479023863820646c17bd89c0a2eb43f1d13a65a6 Mon Sep 17 00:00:00 2001 From: Pawel Czoppa Date: Mon, 17 Jun 2019 17:38:43 +0200 Subject: [PATCH 127/147] Update setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a657b90..6416391 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.35", + version="0.35.1", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From cb1a5fa5e49b1aea8ea96a6f8f0599904981df34 Mon Sep 17 00:00:00 2001 From: Pawel Czoppa Date: Mon, 17 Jun 2019 18:01:09 +0200 Subject: [PATCH 128/147] fix for merge request --- docs/source/overview.rst | 2 +- docs/source/platforms.rst | 2 ++ setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 docs/source/platforms.rst diff --git a/docs/source/overview.rst b/docs/source/overview.rst index ea506a8..400f745 100644 --- a/docs/source/overview.rst +++ b/docs/source/overview.rst @@ -9,7 +9,7 @@ .. excluding Platforms Id's link -:ref:`platforms-link`. +:ref:`platforms-link` .. mdinclude:: ../../README.md :start-line: 28 diff --git a/docs/source/platforms.rst b/docs/source/platforms.rst new file mode 100644 index 0000000..2b79f6f --- /dev/null +++ b/docs/source/platforms.rst @@ -0,0 +1,2 @@ +.. _platforms-link: +.. mdinclude:: ../../PLATFORM_IDs.md \ No newline at end of file diff --git a/setup.py b/setup.py index 6416391..43f5cf6 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.35.1", + version="0.35.2", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From c07c7a2c2abcd1ee63baae1828f46f7f03d7dbcf Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Tue, 18 Jun 2019 15:56:45 +0200 Subject: [PATCH 129/147] Platform list --- setup.py | 2 +- src/galaxy/api/consts.py | 68 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 43f5cf6..6162140 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.35.2", + version="0.36", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index e938977..fd5204e 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -13,7 +13,73 @@ class Platform(Enum): Uplay = "uplay" Battlenet = "battlenet" Epic = "epic" - + Bethesda = "bethesda" + ParadoxPlaza = "paradox" + HumbleBundle = "humble" + Kartridge = "kartridge" + ItchIo = "itch" + NintendoSwitch = "nswitch" + NintendoWiiU = "nwiiu" + NintendoWii = "nwii" + NintendoGameCube = "ncube" + RiotGames = "riot" + Wargaming = "wargaming" + NintendoGameBoy = "ngameboy" + Atari = "atari" + Amiga = "amiga" + SuperNintendoEntertainmentSystem = "snes" + Beamdog = "beamdog" + Direct2Drive = "d2d" + Discord = "discord" + DotEmu = "dotemu" + GameHouse = "gamehouse" + GreenManGaming = "gmg" + WePlay = "weplay" + ZxSpectrum = "zx" + ColecoVision = "vision" + NintendoEntertainmentSystem = "nes" + SegaMasterSystem = "sms" + Commodore64 = "c64" + PcEngine = "pce" + SegaGenesis = "segag" + NeoGeo = "neo" + Sega32X = "sega32" + SegaCd = "segacd" + _3Do = "3do" + SegaSaturn = "saturn" + PlayStation = "psx" + PlayStation2 = "ps2" + Nintendo64 = "n64" + AtariJaguar = "jaguar" + SegaDreamcast = "dc" + Xbox = "xboxog" + Amazon = "amazon" + GamersGate = "gg" + Newegg = "egg" + BestBuy = "bb" + GameUk = "gameuk" + Fanatical = "fanatical" + PlayAsia = "playasia" + Stadia = "stadia" + Arc = "arc" + ElderScrollsOnline = "eso" + Glyph = "glyph" + AionLegionsOfWar = "aionl" + Aion = "aion" + BladeAndSoul = "blade" + GuildWars = "gw" + GuildWars2 = "gw2" + Lineage2 = "lin2" + FinalFantasy11 = "ffxi" + FinalFantasy14 = "ffxiv" + TotalWar = "totalwar" + WindowsStore = "winstore" + EliteDangerous = "elites" + StarCitizen = "star" + PlayStationPortable = "psp" + PlayStationVita = "psvita" + NintendoDs = "nds" + Nintendo3Ds = "3ds" class Feature(Enum): """Possible features that can be implemented by an integration. From be03c83d4541286e1077c7248be51ebabef94f3c Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 14 Jun 2019 18:11:26 +0200 Subject: [PATCH 130/147] SDK-2873: Add typing to API --- src/galaxy/api/plugin.py | 77 ++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index b876c57..22eb9d4 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -7,13 +7,14 @@ from enum import Enum from collections import OrderedDict import sys -from typing import List, Dict +from typing import Any, List, Dict, Optional, Union from galaxy.api.types import Achievement, Game, LocalGame, FriendInfo, GameTime, UserInfo, Room from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError from galaxy.api.consts import Feature from galaxy.api.errors import UnknownError, ImportInProgress +from galaxy.api.types import Authentication, NextStep, Message class JSONEncoder(json.JSONEncoder): @@ -241,7 +242,7 @@ class Plugin: pass # notifications - def store_credentials(self, credentials: dict): + def store_credentials(self, credentials: Dict[str, Any]) -> None: """Notify the client to store authentication credentials. Credentials are passed on the next authenticate call. @@ -265,7 +266,7 @@ class Plugin: """ self._notification_client.notify("store_credentials", credentials, sensitive_params=True) - def add_game(self, game: Game): + def add_game(self, game: Game) -> None: """Notify the client to add game to the list of owned games of the currently authenticated user. @@ -287,7 +288,7 @@ class Plugin: params = {"owned_game": game} self._notification_client.notify("owned_game_added", params) - def remove_game(self, game_id: str): + def remove_game(self, game_id: str) -> None: """Notify the client to remove game from the list of owned games of the currently authenticated user. @@ -309,7 +310,7 @@ class Plugin: params = {"game_id": game_id} self._notification_client.notify("owned_game_removed", params) - def update_game(self, game: Game): + def update_game(self, game: Game) -> None: """Notify the client to update the status of a game owned by the currently authenticated user. @@ -318,7 +319,7 @@ class Plugin: params = {"owned_game": game} self._notification_client.notify("owned_game_updated", params) - def unlock_achievement(self, game_id: str, achievement: Achievement): + def unlock_achievement(self, game_id: str, achievement: Achievement) -> None: """Notify the client to unlock an achievement for a specific game. :param game_id: game_id of the game for which to unlock an achievement. @@ -330,7 +331,7 @@ class Plugin: } self._notification_client.notify("achievement_unlocked", params) - def game_achievements_import_success(self, game_id: str, achievements): + def game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None: """Notify the client that import of achievements for a given game has succeeded. This method is called by import_games_achievements. @@ -343,7 +344,7 @@ class Plugin: } self._notification_client.notify("game_achievements_import_success", params) - def game_achievements_import_failure(self, game_id: str, error: ApplicationError): + def game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None: """Notify the client that import of achievements for a given game has failed. This method is called by import_games_achievements. @@ -359,12 +360,12 @@ class Plugin: } self._notification_client.notify("game_achievements_import_failure", params) - def achievements_import_finished(self): + def achievements_import_finished(self) -> None: """Notify the client that importing achievements has finished. This method is called by import_games_achievements_task""" self._notification_client.notify("achievements_import_finished", None) - def update_local_game_status(self, local_game: LocalGame): + def update_local_game_status(self, local_game: LocalGame) -> None: """Notify the client to update the status of a local game. :param local_game: the LocalGame to update @@ -390,7 +391,7 @@ class Plugin: params = {"local_game": local_game} self._notification_client.notify("local_game_status_changed", params) - def add_friend(self, user: FriendInfo): + def add_friend(self, user: FriendInfo) -> None: """Notify the client to add a user to friends list of the currently authenticated user. :param user: FriendInfo of a user that the client will add to friends list @@ -398,7 +399,7 @@ class Plugin: params = {"friend_info": user} self._notification_client.notify("friend_added", params) - def remove_friend(self, user_id: str): + def remove_friend(self, user_id: str) -> None: """Notify the client to remove a user from friends list of the currently authenticated user. :param user_id: id of the user to remove from friends list @@ -406,7 +407,12 @@ class Plugin: params = {"user_id": user_id} self._notification_client.notify("friend_removed", params) - def update_room(self, room_id: str, unread_message_count=None, new_messages=None): + def update_room( + self, + room_id: str, + unread_message_count: Optional[int]=None, + new_messages: Optional[List[Message]]=None + ) -> None: """WIP, Notify the client to update the information regarding a chat room that the currently authenticated user is in. @@ -421,7 +427,7 @@ class Plugin: params["messages"] = new_messages self._notification_client.notify("chat_room_updated", params) - def update_game_time(self, game_time: GameTime): + def update_game_time(self, game_time: GameTime) -> None: """Notify the client to update game time for a game. :param game_time: game time to update @@ -429,7 +435,7 @@ class Plugin: params = {"game_time": game_time} self._notification_client.notify("game_time_updated", params) - def game_time_import_success(self, game_time: GameTime): + def game_time_import_success(self, game_time: GameTime) -> None: """Notify the client that import of a given game_time has succeeded. This method is called by import_game_times. @@ -438,7 +444,7 @@ class Plugin: params = {"game_time": game_time} self._notification_client.notify("game_time_import_success", params) - def game_time_import_failure(self, game_id: str, error: ApplicationError): + def game_time_import_failure(self, game_id: str, error: ApplicationError) -> None: """Notify the client that import of a game time for a given game has failed. This method is called by import_game_times. @@ -454,19 +460,19 @@ class Plugin: } self._notification_client.notify("game_time_import_failure", params) - def game_times_import_finished(self): + def game_times_import_finished(self) -> None: """Notify the client that importing game times has finished. This method is called by :meth:`~.import_game_times_task`. """ self._notification_client.notify("game_times_import_finished", None) - def lost_authentication(self): + def lost_authentication(self) -> None: """Notify the client that integration has lost authentication for the current user and is unable to perform actions which would require it. """ self._notification_client.notify("authentication_lost", None) - def push_cache(self): + def push_cache(self) -> None: """Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one. """ self._notification_client.notify( @@ -476,14 +482,14 @@ class Plugin: ) # handlers - def handshake_complete(self): + def handshake_complete(self) -> None: """This method is called right after the handshake with the GOG Galaxy Client is complete and before any other operations are called by the GOG Galaxy Client. Persistent cache is available when this method is called. Override it if you need to do additional plugin initializations. This method is called internally.""" - def tick(self): + def tick(self) -> None: """This method is called periodically. Override it to implement periodical non-blocking tasks. This method is called internally. @@ -503,13 +509,13 @@ class Plugin: """ - def shutdown(self): + def shutdown(self) -> None: """This method is called on integration shutdown. Override it to implement tear down. This method is called by the GOG Galaxy Client.""" # methods - async def authenticate(self, stored_credentials: dict = None): + async def authenticate(self, stored_credentials: Optional[Dict] = None) -> Union[NextStep, Authentication]: """Override this method to handle user authentication. This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished or :class:`~galaxy.api.types.NextStep` if it requires going to another url. @@ -537,7 +543,8 @@ class Plugin: """ raise NotImplementedError() - async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]): + async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \ + -> Union[NextStep, Authentication]: """This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials. This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on. This method should either return galaxy.api.types.Authentication if the authentication is finished @@ -592,7 +599,7 @@ class Plugin: """ raise NotImplementedError() - async def start_achievements_import(self, game_ids: List[str]): + async def start_achievements_import(self, game_ids: List[str]) -> None: """Starts the task of importing achievements. This method is called by the GOG Galaxy Client. @@ -611,7 +618,7 @@ class Plugin: asyncio.create_task(import_games_achievements_task(game_ids)) self._achievements_import_in_progress = True - async def import_games_achievements(self, game_ids: List[str]): + async def import_games_achievements(self, game_ids: List[str]) -> None: """ Override this method to return the unlocked achievements of the user that is currently logged in to the plugin. @@ -652,7 +659,7 @@ class Plugin: """ raise NotImplementedError() - async def launch_game(self, game_id: str): + async def launch_game(self, game_id: str) -> None: """Override this method to launch the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -670,7 +677,7 @@ class Plugin: """ raise NotImplementedError() - async def install_game(self, game_id: str): + async def install_game(self, game_id: str) -> None: """Override this method to install the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -688,7 +695,7 @@ class Plugin: """ raise NotImplementedError() - async def uninstall_game(self, game_id: str): + async def uninstall_game(self, game_id: str) -> None: """Override this method to uninstall the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -734,7 +741,7 @@ class Plugin: """ raise NotImplementedError() - async def send_message(self, room_id: str, message_text: str): + async def send_message(self, room_id: str, message_text: str) -> None: """WIP, Override this method to send message to a chat room. This method is called by the GOG Galaxy Client. @@ -743,7 +750,7 @@ class Plugin: """ raise NotImplementedError() - async def mark_as_read(self, room_id: str, last_message_id: str): + async def mark_as_read(self, room_id: str, last_message_id: str) -> None: """WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter. This method is called by the GOG Galaxy Client. @@ -759,7 +766,7 @@ class Plugin: """ raise NotImplementedError() - async def get_room_history_from_message(self, room_id: str, message_id: str): + async def get_room_history_from_message(self, room_id: str, message_id: str) -> List[Message]: """WIP, Override this method to return the chat room history since the message provided in parameter. This method is called by the GOG Galaxy Client. @@ -768,7 +775,7 @@ class Plugin: """ raise NotImplementedError() - async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int): + async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int) -> List[Message]: """WIP, Override this method to return the chat room history since the timestamp provided in parameter. This method is called by the GOG Galaxy Client. @@ -784,7 +791,7 @@ class Plugin: """ raise NotImplementedError() - async def start_game_times_import(self, game_ids: List[str]): + async def start_game_times_import(self, game_ids: List[str]) -> None: """Starts the task of importing game times This method is called by the GOG Galaxy Client. @@ -803,7 +810,7 @@ class Plugin: asyncio.create_task(import_game_times_task(game_ids)) self._game_times_import_in_progress = True - async def import_game_times(self, game_ids: List[str]): + async def import_game_times(self, game_ids: List[str]) -> None: """ Override this method to return game times for games owned by the currently authenticated user. From 58b17d94fa2db8435f9cd482b47652a75490487b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 25 Jun 2019 17:53:16 +0200 Subject: [PATCH 131/147] SDK-2880: Read in chunks --- src/galaxy/api/jsonrpc.py | 18 ++++++++++++++- tests/conftest.py | 6 ++--- tests/test_achievements.py | 8 +++---- tests/test_authenticate.py | 14 ++++++------ tests/test_chat.py | 40 +++++++++++++++++----------------- tests/test_chunk_messages.py | 17 +++++++++++++++ tests/test_friends.py | 8 +++---- tests/test_game_times.py | 8 +++---- tests/test_install_game.py | 4 ++-- tests/test_internal.py | 16 +++++++------- tests/test_launch_game.py | 4 ++-- tests/test_local_games.py | 8 +++---- tests/test_owned_games.py | 8 +++---- tests/test_persistent_cache.py | 4 ++-- tests/test_uninstall_game.py | 4 ++-- tests/test_users.py | 8 +++---- 16 files changed, 104 insertions(+), 71 deletions(-) create mode 100644 tests/test_chunk_messages.py diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 31a80dd..62a36b4 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -73,6 +73,7 @@ class Server(): self._methods = {} self._notifications = {} self._eof_listeners = [] + self._input_buffer = bytes() def register_method(self, name, callback, internal, sensitive_params=False): """ @@ -104,7 +105,7 @@ class Server(): async def run(self): while self._active: try: - data = await self._reader.readline() + data = await self._readline() if not data: self._eof() continue @@ -115,6 +116,21 @@ class Server(): logging.debug("Received %d bytes of data", len(data)) self._handle_input(data) + async def _readline(self): + """Like StreamReader.readline but without limit""" + while True: + chunk = await self._reader.read(1024) + if not chunk: + return chunk + previous_size = len(self._input_buffer) + self._input_buffer += chunk + it = self._input_buffer.find(b"\n", previous_size) + if it < 0: + continue + line = self._input_buffer[:it] + self._input_buffer = self._input_buffer[it+1:] + return line + def stop(self): self._active = False diff --git a/tests/conftest.py b/tests/conftest.py index d373c32..d94a9f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from galaxy.unittest.mock import AsyncMock, coroutine_mock @pytest.fixture() def reader(): stream = MagicMock(name="stream_reader") - stream.readline = AsyncMock() + stream.read = AsyncMock() yield stream @pytest.fixture() @@ -22,8 +22,8 @@ def writer(): yield stream @pytest.fixture() -def readline(reader): - yield reader.readline +def read(reader): + yield reader.read @pytest.fixture() def write(writer): diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 84421bd..9a6ec30 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -16,7 +16,7 @@ def test_initialization_no_id_nor_name(): with raises(AssertionError): Achievement(unlock_time=1234567890) -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -25,7 +25,7 @@ def test_success(plugin, readline, write): "game_id": "14" } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_unlocked_achievements.coro.return_value = [ Achievement(achievement_id="lvl10", unlock_time=1548421241), Achievement(achievement_name="Got level 20", unlock_time=1548422395), @@ -57,7 +57,7 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -67,7 +67,7 @@ def test_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_unlocked_achievements.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called() diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 6cb96a6..1d84c60 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -9,14 +9,14 @@ from galaxy.api.errors import ( BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied ) -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "init_authentication" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() @@ -44,14 +44,14 @@ def test_success(plugin, readline, write): pytest.param(Banned, 105, "Banned", id="banned"), pytest.param(AccessDenied, 106, "Access denied", id="access_denied"), ]) -def test_failure(plugin, readline, write, error, code, message): +def test_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", "method": "init_authentication" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.side_effect = error() asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() @@ -66,7 +66,7 @@ def test_failure(plugin, readline, write, error, code, message): } } -def test_stored_credentials(plugin, readline, write): +def test_stored_credentials(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -77,7 +77,7 @@ def test_stored_credentials(plugin, readline, write): } } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) @@ -100,7 +100,7 @@ def test_store_credentials(plugin, write): "params": credentials } -def test_lost_authentication(plugin, readline, write): +def test_lost_authentication(plugin, write): async def couritine(): plugin.lost_authentication() diff --git a/tests/test_chat.py b/tests/test_chat.py index 97dad89..db840b4 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -9,7 +9,7 @@ from galaxy.api.errors import ( TooManyMessagesSent, IncoherentLastMessage, MessageNotFound ) -def test_send_message_success(plugin, readline, write): +def test_send_message_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -20,7 +20,7 @@ def test_send_message_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.send_message.coro.return_value = None asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="14", message="Hello!") @@ -40,7 +40,7 @@ def test_send_message_success(plugin, readline, write): pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages") ]) -def test_send_message_failure(plugin, readline, write, error, code, message): +def test_send_message_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "6", @@ -51,7 +51,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.send_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="15", message="Bye") @@ -66,7 +66,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } } -def test_mark_as_read_success(plugin, readline, write): +def test_mark_as_read_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", @@ -77,7 +77,7 @@ def test_mark_as_read_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.mark_as_read.coro.return_value = None asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") @@ -102,7 +102,7 @@ def test_mark_as_read_success(plugin, readline, write): id="incoherent_last_message" ) ]) -def test_mark_as_read_failure(plugin, readline, write, error, code, message): +def test_mark_as_read_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "4", @@ -113,7 +113,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.mark_as_read.coro.side_effect = error() asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") @@ -128,14 +128,14 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } } -def test_get_rooms_success(plugin, readline, write): +def test_get_rooms_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "2", "method": "import_rooms" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_rooms.coro.return_value = [ Room("13", 0, None), Room("15", 34, "8") @@ -162,14 +162,14 @@ def test_get_rooms_success(plugin, readline, write): } } -def test_get_rooms_failure(plugin, readline, write): +def test_get_rooms_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "9", "method": "import_rooms" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_rooms.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_rooms.assert_called_with() @@ -184,7 +184,7 @@ def test_get_rooms_failure(plugin, readline, write): } } -def test_get_room_history_from_message_success(plugin, readline, write): +def test_get_room_history_from_message_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "2", @@ -195,7 +195,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_message.coro.return_value = [ Message("13", "149", 1549454837, "Hello"), Message("14", "812", 1549454899, "Hi") @@ -233,7 +233,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found") ]) -def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message): +def test_get_room_history_from_message_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "7", @@ -244,7 +244,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") @@ -259,7 +259,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } } -def test_get_room_history_from_timestamp_success(plugin, readline, write): +def test_get_room_history_from_timestamp_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", @@ -270,7 +270,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_timestamp.coro.return_value = [ Message("12", "155", 1549454836, "Bye") ] @@ -296,7 +296,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } } -def test_get_room_history_from_timestamp_failure(plugin, readline, write): +def test_get_room_history_from_timestamp_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -307,7 +307,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_room_history_from_timestamp.assert_called_with( diff --git a/tests/test_chunk_messages.py b/tests/test_chunk_messages.py new file mode 100644 index 0000000..3e09adb --- /dev/null +++ b/tests/test_chunk_messages.py @@ -0,0 +1,17 @@ +import asyncio +import json + +def test_chunked_messages(plugin, read): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + message = json.dumps(request).encode() + b"\n" + read.side_effect = [message[:5], message[5:], b""] + plugin.get_owned_games.return_value = None + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") diff --git a/tests/test_friends.py b/tests/test_friends.py index 52cdd9b..030f029 100644 --- a/tests/test_friends.py +++ b/tests/test_friends.py @@ -5,14 +5,14 @@ from galaxy.api.types import FriendInfo from galaxy.api.errors import UnknownError -def test_get_friends_success(plugin, readline, write): +def test_get_friends_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_friends" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_friends.coro.return_value = [ FriendInfo("3", "Jan"), FriendInfo("5", "Ola") @@ -33,14 +33,14 @@ def test_get_friends_success(plugin, readline, write): } -def test_get_friends_failure(plugin, readline, write): +def test_get_friends_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_friends" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_friends.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_friends.assert_called_with() diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 9ad7220..8f5d5ae 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -6,14 +6,14 @@ import pytest from galaxy.api.types import GameTime from galaxy.api.errors import UnknownError, ImportInProgress, BackendError -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_game_times" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_game_times.coro.return_value = [ GameTime("3", 60, 1549550504), GameTime("5", 10, 1549550502) @@ -41,14 +41,14 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_game_times" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_game_times.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() diff --git a/tests/test_install_game.py b/tests/test_install_game.py index ca9c4d0..744bb1a 100644 --- a/tests/test_install_game.py +++ b/tests/test_install_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "install_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.install_game.assert_called_with(game_id="3") diff --git a/tests/test_internal.py b/tests/test_internal.py index 44f8ee7..d381f4f 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(reader, writer, readline, write): +def test_get_capabilites(reader, writer, read, write): class PluginImpl(Plugin): #pylint: disable=abstract-method async def get_owned_games(self): pass @@ -16,7 +16,7 @@ def test_get_capabilites(reader, writer, readline, write): } token = "token" plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token) - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) assert response == { @@ -31,13 +31,13 @@ def test_get_capabilites(reader, writer, readline, write): } } -def test_shutdown(plugin, readline, write): +def test_shutdown(plugin, read, write): request = { "jsonrpc": "2.0", "id": "5", "method": "shutdown" } - readline.side_effect = [json.dumps(request)] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) plugin.shutdown.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -47,13 +47,13 @@ def test_shutdown(plugin, readline, write): "result": None } -def test_ping(plugin, readline, write): +def test_ping(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", "method": "ping" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) assert response == { @@ -62,7 +62,7 @@ def test_ping(plugin, readline, write): "result": None } -def test_tick(plugin, readline): - readline.side_effect = [""] +def test_tick(plugin, read): + read.side_effect = [b""] asyncio.run(plugin.run()) plugin.tick.assert_called_with() diff --git a/tests/test_launch_game.py b/tests/test_launch_game.py index fa654e9..551f7cf 100644 --- a/tests/test_launch_game.py +++ b/tests/test_launch_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "launch_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.launch_game.assert_called_with(game_id="3") diff --git a/tests/test_local_games.py b/tests/test_local_games.py index 445e699..b53056b 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -7,14 +7,14 @@ from galaxy.api.types import LocalGame from galaxy.api.consts import LocalGameState from galaxy.api.errors import UnknownError, FailedParsingManifest -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_local_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_local_games.coro.return_value = [ LocalGame("1", LocalGameState.Running), @@ -53,14 +53,14 @@ def test_success(plugin, readline, write): pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing") ], ) -def test_failure(plugin, readline, write, error, code, message): +def test_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", "method": "import_local_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_local_games.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index 1202c9e..f455914 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -5,14 +5,14 @@ from galaxy.api.types import Game, Dlc, LicenseInfo from galaxy.api.consts import LicenseType from galaxy.api.errors import UnknownError -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_owned_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.coro.return_value = [ Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)), Game( @@ -67,14 +67,14 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_owned_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() diff --git a/tests/test_persistent_cache.py b/tests/test_persistent_cache.py index 056ddb5..d2d9d8c 100644 --- a/tests/test_persistent_cache.py +++ b/tests/test_persistent_cache.py @@ -28,7 +28,7 @@ def cache_data(): } -def test_initialize_cache(plugin, readline, write, cache_data): +def test_initialize_cache(plugin, read, write, cache_data): request_id = 3 request = { "jsonrpc": "2.0", @@ -36,7 +36,7 @@ def test_initialize_cache(plugin, readline, write, cache_data): "method": "initialize_cache", "params": {"data": cache_data} } - readline.side_effect = [json.dumps(request)] + read.side_effect = [json.dumps(request).encode() + b"\n"] assert {} == plugin.persistent_cache asyncio.run(plugin.run()) diff --git a/tests/test_uninstall_game.py b/tests/test_uninstall_game.py index 2e7c4ef..40a316b 100644 --- a/tests/test_uninstall_game.py +++ b/tests/test_uninstall_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "uninstall_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.uninstall_game.assert_called_with(game_id="3") diff --git a/tests/test_users.py b/tests/test_users.py index 24c9dbb..47837ef 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -6,7 +6,7 @@ from galaxy.api.errors import UnknownError from galaxy.api.consts import PresenceState -def test_get_users_success(plugin, readline, write): +def test_get_users_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "8", @@ -16,7 +16,7 @@ def test_get_users_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_users.coro.return_value = [ UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) ] @@ -43,7 +43,7 @@ def test_get_users_success(plugin, readline, write): } -def test_get_users_failure(plugin, readline, write): +def test_get_users_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "12", @@ -53,7 +53,7 @@ def test_get_users_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_users.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) From 05042fe430262cd8322a96fe60d5e2cc5cbe074a Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Tue, 25 Jun 2019 17:53:35 +0200 Subject: [PATCH 132/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6162140..cf48618 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.36", + version="0.37", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 207b1e1313021cd8575c23d397bf962c0a04dfff Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 26 Jun 2019 12:02:25 +0200 Subject: [PATCH 133/147] SDK-2880: Fix readline --- src/galaxy/api/jsonrpc.py | 13 +++++++----- tests/test_chunk_messages.py | 39 +++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 62a36b4..e491c3f 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -74,6 +74,7 @@ class Server(): self._notifications = {} self._eof_listeners = [] self._input_buffer = bytes() + self._input_buffer_it = 0 def register_method(self, name, callback, internal, sensitive_params=False): """ @@ -120,15 +121,17 @@ class Server(): """Like StreamReader.readline but without limit""" while True: chunk = await self._reader.read(1024) - if not chunk: - return chunk - previous_size = len(self._input_buffer) self._input_buffer += chunk - it = self._input_buffer.find(b"\n", previous_size) + it = self._input_buffer.find(b"\n", self._input_buffer_it) if it < 0: - continue + if not chunk: + return bytes() # EOF + else: + self._input_buffer_it = len(self._input_buffer) + continue line = self._input_buffer[:it] self._input_buffer = self._input_buffer[it+1:] + self._input_buffer_it = 0 return line def stop(self): diff --git a/tests/test_chunk_messages.py b/tests/test_chunk_messages.py index 3e09adb..68a4b52 100644 --- a/tests/test_chunk_messages.py +++ b/tests/test_chunk_messages.py @@ -12,6 +12,43 @@ def test_chunked_messages(plugin, read): message = json.dumps(request).encode() + b"\n" read.side_effect = [message[:5], message[5:], b""] - plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.install_game.assert_called_with(game_id="3") + +def test_joined_messages(plugin, read): + requests = [ + { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + }, + { + "jsonrpc": "2.0", + "method": "launch_game", + "params": { + "game_id": "3" + } + } + ] + data = b"".join([json.dumps(request).encode() + b"\n" for request in requests]) + + read.side_effect = [data, b""] + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") + plugin.launch_game.assert_called_with(game_id="3") + +def test_not_finished(plugin, read): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + message = json.dumps(request).encode() # no new line + read.side_effect = [message, b""] + asyncio.run(plugin.run()) + plugin.install_game.assert_not_called() From 692bdbf370b6285c992c4f561f00106c6bfd6614 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 26 Jun 2019 12:07:28 +0200 Subject: [PATCH 134/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cf48618..555c8ea 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.37", + version="0.38", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From f1fd00fcd3abd57ea1b5c3e099d273fbf4f90843 Mon Sep 17 00:00:00 2001 From: GOG Galaxy SDK Team Date: Wed, 26 Jun 2019 12:46:23 +0200 Subject: [PATCH 135/147] version 0.38 --- setup.py | 2 +- src/galaxy/api/consts.py | 68 +++++++++++++++++++++++++++++- src/galaxy/api/jsonrpc.py | 21 +++++++++- src/galaxy/api/plugin.py | 77 ++++++++++++++++++---------------- tests/conftest.py | 6 +-- tests/test_achievements.py | 8 ++-- tests/test_authenticate.py | 14 +++---- tests/test_chat.py | 40 +++++++++--------- tests/test_chunk_messages.py | 54 ++++++++++++++++++++++++ tests/test_friends.py | 8 ++-- tests/test_game_times.py | 8 ++-- tests/test_install_game.py | 4 +- tests/test_internal.py | 16 +++---- tests/test_launch_game.py | 4 +- tests/test_local_games.py | 8 ++-- tests/test_owned_games.py | 8 ++-- tests/test_persistent_cache.py | 4 +- tests/test_uninstall_game.py | 4 +- tests/test_users.py | 8 ++-- 19 files changed, 254 insertions(+), 108 deletions(-) create mode 100644 tests/test_chunk_messages.py diff --git a/setup.py b/setup.py index 43f5cf6..555c8ea 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.35.2", + version="0.38", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index e938977..fd5204e 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -13,7 +13,73 @@ class Platform(Enum): Uplay = "uplay" Battlenet = "battlenet" Epic = "epic" - + Bethesda = "bethesda" + ParadoxPlaza = "paradox" + HumbleBundle = "humble" + Kartridge = "kartridge" + ItchIo = "itch" + NintendoSwitch = "nswitch" + NintendoWiiU = "nwiiu" + NintendoWii = "nwii" + NintendoGameCube = "ncube" + RiotGames = "riot" + Wargaming = "wargaming" + NintendoGameBoy = "ngameboy" + Atari = "atari" + Amiga = "amiga" + SuperNintendoEntertainmentSystem = "snes" + Beamdog = "beamdog" + Direct2Drive = "d2d" + Discord = "discord" + DotEmu = "dotemu" + GameHouse = "gamehouse" + GreenManGaming = "gmg" + WePlay = "weplay" + ZxSpectrum = "zx" + ColecoVision = "vision" + NintendoEntertainmentSystem = "nes" + SegaMasterSystem = "sms" + Commodore64 = "c64" + PcEngine = "pce" + SegaGenesis = "segag" + NeoGeo = "neo" + Sega32X = "sega32" + SegaCd = "segacd" + _3Do = "3do" + SegaSaturn = "saturn" + PlayStation = "psx" + PlayStation2 = "ps2" + Nintendo64 = "n64" + AtariJaguar = "jaguar" + SegaDreamcast = "dc" + Xbox = "xboxog" + Amazon = "amazon" + GamersGate = "gg" + Newegg = "egg" + BestBuy = "bb" + GameUk = "gameuk" + Fanatical = "fanatical" + PlayAsia = "playasia" + Stadia = "stadia" + Arc = "arc" + ElderScrollsOnline = "eso" + Glyph = "glyph" + AionLegionsOfWar = "aionl" + Aion = "aion" + BladeAndSoul = "blade" + GuildWars = "gw" + GuildWars2 = "gw2" + Lineage2 = "lin2" + FinalFantasy11 = "ffxi" + FinalFantasy14 = "ffxiv" + TotalWar = "totalwar" + WindowsStore = "winstore" + EliteDangerous = "elites" + StarCitizen = "star" + PlayStationPortable = "psp" + PlayStationVita = "psvita" + NintendoDs = "nds" + Nintendo3Ds = "3ds" class Feature(Enum): """Possible features that can be implemented by an integration. diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 31a80dd..e491c3f 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -73,6 +73,8 @@ class Server(): self._methods = {} self._notifications = {} self._eof_listeners = [] + self._input_buffer = bytes() + self._input_buffer_it = 0 def register_method(self, name, callback, internal, sensitive_params=False): """ @@ -104,7 +106,7 @@ class Server(): async def run(self): while self._active: try: - data = await self._reader.readline() + data = await self._readline() if not data: self._eof() continue @@ -115,6 +117,23 @@ class Server(): logging.debug("Received %d bytes of data", len(data)) self._handle_input(data) + async def _readline(self): + """Like StreamReader.readline but without limit""" + while True: + chunk = await self._reader.read(1024) + self._input_buffer += chunk + it = self._input_buffer.find(b"\n", self._input_buffer_it) + if it < 0: + if not chunk: + return bytes() # EOF + else: + self._input_buffer_it = len(self._input_buffer) + continue + line = self._input_buffer[:it] + self._input_buffer = self._input_buffer[it+1:] + self._input_buffer_it = 0 + return line + def stop(self): self._active = False diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index b876c57..22eb9d4 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -7,13 +7,14 @@ from enum import Enum from collections import OrderedDict import sys -from typing import List, Dict +from typing import Any, List, Dict, Optional, Union from galaxy.api.types import Achievement, Game, LocalGame, FriendInfo, GameTime, UserInfo, Room from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError from galaxy.api.consts import Feature from galaxy.api.errors import UnknownError, ImportInProgress +from galaxy.api.types import Authentication, NextStep, Message class JSONEncoder(json.JSONEncoder): @@ -241,7 +242,7 @@ class Plugin: pass # notifications - def store_credentials(self, credentials: dict): + def store_credentials(self, credentials: Dict[str, Any]) -> None: """Notify the client to store authentication credentials. Credentials are passed on the next authenticate call. @@ -265,7 +266,7 @@ class Plugin: """ self._notification_client.notify("store_credentials", credentials, sensitive_params=True) - def add_game(self, game: Game): + def add_game(self, game: Game) -> None: """Notify the client to add game to the list of owned games of the currently authenticated user. @@ -287,7 +288,7 @@ class Plugin: params = {"owned_game": game} self._notification_client.notify("owned_game_added", params) - def remove_game(self, game_id: str): + def remove_game(self, game_id: str) -> None: """Notify the client to remove game from the list of owned games of the currently authenticated user. @@ -309,7 +310,7 @@ class Plugin: params = {"game_id": game_id} self._notification_client.notify("owned_game_removed", params) - def update_game(self, game: Game): + def update_game(self, game: Game) -> None: """Notify the client to update the status of a game owned by the currently authenticated user. @@ -318,7 +319,7 @@ class Plugin: params = {"owned_game": game} self._notification_client.notify("owned_game_updated", params) - def unlock_achievement(self, game_id: str, achievement: Achievement): + def unlock_achievement(self, game_id: str, achievement: Achievement) -> None: """Notify the client to unlock an achievement for a specific game. :param game_id: game_id of the game for which to unlock an achievement. @@ -330,7 +331,7 @@ class Plugin: } self._notification_client.notify("achievement_unlocked", params) - def game_achievements_import_success(self, game_id: str, achievements): + def game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None: """Notify the client that import of achievements for a given game has succeeded. This method is called by import_games_achievements. @@ -343,7 +344,7 @@ class Plugin: } self._notification_client.notify("game_achievements_import_success", params) - def game_achievements_import_failure(self, game_id: str, error: ApplicationError): + def game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None: """Notify the client that import of achievements for a given game has failed. This method is called by import_games_achievements. @@ -359,12 +360,12 @@ class Plugin: } self._notification_client.notify("game_achievements_import_failure", params) - def achievements_import_finished(self): + def achievements_import_finished(self) -> None: """Notify the client that importing achievements has finished. This method is called by import_games_achievements_task""" self._notification_client.notify("achievements_import_finished", None) - def update_local_game_status(self, local_game: LocalGame): + def update_local_game_status(self, local_game: LocalGame) -> None: """Notify the client to update the status of a local game. :param local_game: the LocalGame to update @@ -390,7 +391,7 @@ class Plugin: params = {"local_game": local_game} self._notification_client.notify("local_game_status_changed", params) - def add_friend(self, user: FriendInfo): + def add_friend(self, user: FriendInfo) -> None: """Notify the client to add a user to friends list of the currently authenticated user. :param user: FriendInfo of a user that the client will add to friends list @@ -398,7 +399,7 @@ class Plugin: params = {"friend_info": user} self._notification_client.notify("friend_added", params) - def remove_friend(self, user_id: str): + def remove_friend(self, user_id: str) -> None: """Notify the client to remove a user from friends list of the currently authenticated user. :param user_id: id of the user to remove from friends list @@ -406,7 +407,12 @@ class Plugin: params = {"user_id": user_id} self._notification_client.notify("friend_removed", params) - def update_room(self, room_id: str, unread_message_count=None, new_messages=None): + def update_room( + self, + room_id: str, + unread_message_count: Optional[int]=None, + new_messages: Optional[List[Message]]=None + ) -> None: """WIP, Notify the client to update the information regarding a chat room that the currently authenticated user is in. @@ -421,7 +427,7 @@ class Plugin: params["messages"] = new_messages self._notification_client.notify("chat_room_updated", params) - def update_game_time(self, game_time: GameTime): + def update_game_time(self, game_time: GameTime) -> None: """Notify the client to update game time for a game. :param game_time: game time to update @@ -429,7 +435,7 @@ class Plugin: params = {"game_time": game_time} self._notification_client.notify("game_time_updated", params) - def game_time_import_success(self, game_time: GameTime): + def game_time_import_success(self, game_time: GameTime) -> None: """Notify the client that import of a given game_time has succeeded. This method is called by import_game_times. @@ -438,7 +444,7 @@ class Plugin: params = {"game_time": game_time} self._notification_client.notify("game_time_import_success", params) - def game_time_import_failure(self, game_id: str, error: ApplicationError): + def game_time_import_failure(self, game_id: str, error: ApplicationError) -> None: """Notify the client that import of a game time for a given game has failed. This method is called by import_game_times. @@ -454,19 +460,19 @@ class Plugin: } self._notification_client.notify("game_time_import_failure", params) - def game_times_import_finished(self): + def game_times_import_finished(self) -> None: """Notify the client that importing game times has finished. This method is called by :meth:`~.import_game_times_task`. """ self._notification_client.notify("game_times_import_finished", None) - def lost_authentication(self): + def lost_authentication(self) -> None: """Notify the client that integration has lost authentication for the current user and is unable to perform actions which would require it. """ self._notification_client.notify("authentication_lost", None) - def push_cache(self): + def push_cache(self) -> None: """Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one. """ self._notification_client.notify( @@ -476,14 +482,14 @@ class Plugin: ) # handlers - def handshake_complete(self): + def handshake_complete(self) -> None: """This method is called right after the handshake with the GOG Galaxy Client is complete and before any other operations are called by the GOG Galaxy Client. Persistent cache is available when this method is called. Override it if you need to do additional plugin initializations. This method is called internally.""" - def tick(self): + def tick(self) -> None: """This method is called periodically. Override it to implement periodical non-blocking tasks. This method is called internally. @@ -503,13 +509,13 @@ class Plugin: """ - def shutdown(self): + def shutdown(self) -> None: """This method is called on integration shutdown. Override it to implement tear down. This method is called by the GOG Galaxy Client.""" # methods - async def authenticate(self, stored_credentials: dict = None): + async def authenticate(self, stored_credentials: Optional[Dict] = None) -> Union[NextStep, Authentication]: """Override this method to handle user authentication. This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished or :class:`~galaxy.api.types.NextStep` if it requires going to another url. @@ -537,7 +543,8 @@ class Plugin: """ raise NotImplementedError() - async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]): + async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \ + -> Union[NextStep, Authentication]: """This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials. This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on. This method should either return galaxy.api.types.Authentication if the authentication is finished @@ -592,7 +599,7 @@ class Plugin: """ raise NotImplementedError() - async def start_achievements_import(self, game_ids: List[str]): + async def start_achievements_import(self, game_ids: List[str]) -> None: """Starts the task of importing achievements. This method is called by the GOG Galaxy Client. @@ -611,7 +618,7 @@ class Plugin: asyncio.create_task(import_games_achievements_task(game_ids)) self._achievements_import_in_progress = True - async def import_games_achievements(self, game_ids: List[str]): + async def import_games_achievements(self, game_ids: List[str]) -> None: """ Override this method to return the unlocked achievements of the user that is currently logged in to the plugin. @@ -652,7 +659,7 @@ class Plugin: """ raise NotImplementedError() - async def launch_game(self, game_id: str): + async def launch_game(self, game_id: str) -> None: """Override this method to launch the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -670,7 +677,7 @@ class Plugin: """ raise NotImplementedError() - async def install_game(self, game_id: str): + async def install_game(self, game_id: str) -> None: """Override this method to install the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -688,7 +695,7 @@ class Plugin: """ raise NotImplementedError() - async def uninstall_game(self, game_id: str): + async def uninstall_game(self, game_id: str) -> None: """Override this method to uninstall the game identified by the provided game_id. This method is called by the GOG Galaxy Client. @@ -734,7 +741,7 @@ class Plugin: """ raise NotImplementedError() - async def send_message(self, room_id: str, message_text: str): + async def send_message(self, room_id: str, message_text: str) -> None: """WIP, Override this method to send message to a chat room. This method is called by the GOG Galaxy Client. @@ -743,7 +750,7 @@ class Plugin: """ raise NotImplementedError() - async def mark_as_read(self, room_id: str, last_message_id: str): + async def mark_as_read(self, room_id: str, last_message_id: str) -> None: """WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter. This method is called by the GOG Galaxy Client. @@ -759,7 +766,7 @@ class Plugin: """ raise NotImplementedError() - async def get_room_history_from_message(self, room_id: str, message_id: str): + async def get_room_history_from_message(self, room_id: str, message_id: str) -> List[Message]: """WIP, Override this method to return the chat room history since the message provided in parameter. This method is called by the GOG Galaxy Client. @@ -768,7 +775,7 @@ class Plugin: """ raise NotImplementedError() - async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int): + async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int) -> List[Message]: """WIP, Override this method to return the chat room history since the timestamp provided in parameter. This method is called by the GOG Galaxy Client. @@ -784,7 +791,7 @@ class Plugin: """ raise NotImplementedError() - async def start_game_times_import(self, game_ids: List[str]): + async def start_game_times_import(self, game_ids: List[str]) -> None: """Starts the task of importing game times This method is called by the GOG Galaxy Client. @@ -803,7 +810,7 @@ class Plugin: asyncio.create_task(import_game_times_task(game_ids)) self._game_times_import_in_progress = True - async def import_game_times(self, game_ids: List[str]): + async def import_game_times(self, game_ids: List[str]) -> None: """ Override this method to return game times for games owned by the currently authenticated user. diff --git a/tests/conftest.py b/tests/conftest.py index d373c32..d94a9f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from galaxy.unittest.mock import AsyncMock, coroutine_mock @pytest.fixture() def reader(): stream = MagicMock(name="stream_reader") - stream.readline = AsyncMock() + stream.read = AsyncMock() yield stream @pytest.fixture() @@ -22,8 +22,8 @@ def writer(): yield stream @pytest.fixture() -def readline(reader): - yield reader.readline +def read(reader): + yield reader.read @pytest.fixture() def write(writer): diff --git a/tests/test_achievements.py b/tests/test_achievements.py index 84421bd..9a6ec30 100644 --- a/tests/test_achievements.py +++ b/tests/test_achievements.py @@ -16,7 +16,7 @@ def test_initialization_no_id_nor_name(): with raises(AssertionError): Achievement(unlock_time=1234567890) -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -25,7 +25,7 @@ def test_success(plugin, readline, write): "game_id": "14" } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_unlocked_achievements.coro.return_value = [ Achievement(achievement_id="lvl10", unlock_time=1548421241), Achievement(achievement_name="Got level 20", unlock_time=1548422395), @@ -57,7 +57,7 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -67,7 +67,7 @@ def test_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_unlocked_achievements.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_unlocked_achievements.assert_called() diff --git a/tests/test_authenticate.py b/tests/test_authenticate.py index 6cb96a6..1d84c60 100644 --- a/tests/test_authenticate.py +++ b/tests/test_authenticate.py @@ -9,14 +9,14 @@ from galaxy.api.errors import ( BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied ) -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "init_authentication" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() @@ -44,14 +44,14 @@ def test_success(plugin, readline, write): pytest.param(Banned, 105, "Banned", id="banned"), pytest.param(AccessDenied, 106, "Access denied", id="access_denied"), ]) -def test_failure(plugin, readline, write, error, code, message): +def test_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", "method": "init_authentication" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.side_effect = error() asyncio.run(plugin.run()) plugin.authenticate.assert_called_with() @@ -66,7 +66,7 @@ def test_failure(plugin, readline, write, error, code, message): } } -def test_stored_credentials(plugin, readline, write): +def test_stored_credentials(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -77,7 +77,7 @@ def test_stored_credentials(plugin, readline, write): } } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.authenticate.coro.return_value = Authentication("132", "Zenek") asyncio.run(plugin.run()) plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"}) @@ -100,7 +100,7 @@ def test_store_credentials(plugin, write): "params": credentials } -def test_lost_authentication(plugin, readline, write): +def test_lost_authentication(plugin, write): async def couritine(): plugin.lost_authentication() diff --git a/tests/test_chat.py b/tests/test_chat.py index 97dad89..db840b4 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -9,7 +9,7 @@ from galaxy.api.errors import ( TooManyMessagesSent, IncoherentLastMessage, MessageNotFound ) -def test_send_message_success(plugin, readline, write): +def test_send_message_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -20,7 +20,7 @@ def test_send_message_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.send_message.coro.return_value = None asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="14", message="Hello!") @@ -40,7 +40,7 @@ def test_send_message_success(plugin, readline, write): pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages") ]) -def test_send_message_failure(plugin, readline, write, error, code, message): +def test_send_message_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "6", @@ -51,7 +51,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.send_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.send_message.assert_called_with(room_id="15", message="Bye") @@ -66,7 +66,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message): } } -def test_mark_as_read_success(plugin, readline, write): +def test_mark_as_read_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", @@ -77,7 +77,7 @@ def test_mark_as_read_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.mark_as_read.coro.return_value = None asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67") @@ -102,7 +102,7 @@ def test_mark_as_read_success(plugin, readline, write): id="incoherent_last_message" ) ]) -def test_mark_as_read_failure(plugin, readline, write, error, code, message): +def test_mark_as_read_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "4", @@ -113,7 +113,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.mark_as_read.coro.side_effect = error() asyncio.run(plugin.run()) plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7") @@ -128,14 +128,14 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message): } } -def test_get_rooms_success(plugin, readline, write): +def test_get_rooms_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "2", "method": "import_rooms" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_rooms.coro.return_value = [ Room("13", 0, None), Room("15", 34, "8") @@ -162,14 +162,14 @@ def test_get_rooms_success(plugin, readline, write): } } -def test_get_rooms_failure(plugin, readline, write): +def test_get_rooms_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "9", "method": "import_rooms" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_rooms.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_rooms.assert_called_with() @@ -184,7 +184,7 @@ def test_get_rooms_failure(plugin, readline, write): } } -def test_get_room_history_from_message_success(plugin, readline, write): +def test_get_room_history_from_message_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "2", @@ -195,7 +195,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_message.coro.return_value = [ Message("13", "149", 1549454837, "Hello"), Message("14", "812", 1549454899, "Hi") @@ -233,7 +233,7 @@ def test_get_room_history_from_message_success(plugin, readline, write): pytest.param(BackendError, 4, "Backend error", id="backend_error"), pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found") ]) -def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message): +def test_get_room_history_from_message_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "7", @@ -244,7 +244,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_message.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88") @@ -259,7 +259,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c } } -def test_get_room_history_from_timestamp_success(plugin, readline, write): +def test_get_room_history_from_timestamp_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", @@ -270,7 +270,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_timestamp.coro.return_value = [ Message("12", "155", 1549454836, "Bye") ] @@ -296,7 +296,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write): } } -def test_get_room_history_from_timestamp_failure(plugin, readline, write): +def test_get_room_history_from_timestamp_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", @@ -307,7 +307,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_room_history_from_timestamp.assert_called_with( diff --git a/tests/test_chunk_messages.py b/tests/test_chunk_messages.py new file mode 100644 index 0000000..68a4b52 --- /dev/null +++ b/tests/test_chunk_messages.py @@ -0,0 +1,54 @@ +import asyncio +import json + +def test_chunked_messages(plugin, read): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + message = json.dumps(request).encode() + b"\n" + read.side_effect = [message[:5], message[5:], b""] + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") + +def test_joined_messages(plugin, read): + requests = [ + { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + }, + { + "jsonrpc": "2.0", + "method": "launch_game", + "params": { + "game_id": "3" + } + } + ] + data = b"".join([json.dumps(request).encode() + b"\n" for request in requests]) + + read.side_effect = [data, b""] + asyncio.run(plugin.run()) + plugin.install_game.assert_called_with(game_id="3") + plugin.launch_game.assert_called_with(game_id="3") + +def test_not_finished(plugin, read): + request = { + "jsonrpc": "2.0", + "method": "install_game", + "params": { + "game_id": "3" + } + } + + message = json.dumps(request).encode() # no new line + read.side_effect = [message, b""] + asyncio.run(plugin.run()) + plugin.install_game.assert_not_called() diff --git a/tests/test_friends.py b/tests/test_friends.py index 52cdd9b..030f029 100644 --- a/tests/test_friends.py +++ b/tests/test_friends.py @@ -5,14 +5,14 @@ from galaxy.api.types import FriendInfo from galaxy.api.errors import UnknownError -def test_get_friends_success(plugin, readline, write): +def test_get_friends_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_friends" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_friends.coro.return_value = [ FriendInfo("3", "Jan"), FriendInfo("5", "Ola") @@ -33,14 +33,14 @@ def test_get_friends_success(plugin, readline, write): } -def test_get_friends_failure(plugin, readline, write): +def test_get_friends_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_friends" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_friends.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_friends.assert_called_with() diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 9ad7220..8f5d5ae 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -6,14 +6,14 @@ import pytest from galaxy.api.types import GameTime from galaxy.api.errors import UnknownError, ImportInProgress, BackendError -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_game_times" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_game_times.coro.return_value = [ GameTime("3", 60, 1549550504), GameTime("5", 10, 1549550502) @@ -41,14 +41,14 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_game_times" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_game_times.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() diff --git a/tests/test_install_game.py b/tests/test_install_game.py index ca9c4d0..744bb1a 100644 --- a/tests/test_install_game.py +++ b/tests/test_install_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "install_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.install_game.assert_called_with(game_id="3") diff --git a/tests/test_internal.py b/tests/test_internal.py index 44f8ee7..d381f4f 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(reader, writer, readline, write): +def test_get_capabilites(reader, writer, read, write): class PluginImpl(Plugin): #pylint: disable=abstract-method async def get_owned_games(self): pass @@ -16,7 +16,7 @@ def test_get_capabilites(reader, writer, readline, write): } token = "token" plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token) - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) assert response == { @@ -31,13 +31,13 @@ def test_get_capabilites(reader, writer, readline, write): } } -def test_shutdown(plugin, readline, write): +def test_shutdown(plugin, read, write): request = { "jsonrpc": "2.0", "id": "5", "method": "shutdown" } - readline.side_effect = [json.dumps(request)] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) plugin.shutdown.assert_called_with() response = json.loads(write.call_args[0][0]) @@ -47,13 +47,13 @@ def test_shutdown(plugin, readline, write): "result": None } -def test_ping(plugin, readline, write): +def test_ping(plugin, read, write): request = { "jsonrpc": "2.0", "id": "7", "method": "ping" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] asyncio.run(plugin.run()) response = json.loads(write.call_args[0][0]) assert response == { @@ -62,7 +62,7 @@ def test_ping(plugin, readline, write): "result": None } -def test_tick(plugin, readline): - readline.side_effect = [""] +def test_tick(plugin, read): + read.side_effect = [b""] asyncio.run(plugin.run()) plugin.tick.assert_called_with() diff --git a/tests/test_launch_game.py b/tests/test_launch_game.py index fa654e9..551f7cf 100644 --- a/tests/test_launch_game.py +++ b/tests/test_launch_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "launch_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.launch_game.assert_called_with(game_id="3") diff --git a/tests/test_local_games.py b/tests/test_local_games.py index 445e699..b53056b 100644 --- a/tests/test_local_games.py +++ b/tests/test_local_games.py @@ -7,14 +7,14 @@ from galaxy.api.types import LocalGame from galaxy.api.consts import LocalGameState from galaxy.api.errors import UnknownError, FailedParsingManifest -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_local_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_local_games.coro.return_value = [ LocalGame("1", LocalGameState.Running), @@ -53,14 +53,14 @@ def test_success(plugin, readline, write): pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing") ], ) -def test_failure(plugin, readline, write, error, code, message): +def test_failure(plugin, read, write, error, code, message): request = { "jsonrpc": "2.0", "id": "3", "method": "import_local_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_local_games.coro.side_effect = error() asyncio.run(plugin.run()) plugin.get_local_games.assert_called_with() diff --git a/tests/test_owned_games.py b/tests/test_owned_games.py index 1202c9e..f455914 100644 --- a/tests/test_owned_games.py +++ b/tests/test_owned_games.py @@ -5,14 +5,14 @@ from galaxy.api.types import Game, Dlc, LicenseInfo from galaxy.api.consts import LicenseType from galaxy.api.errors import UnknownError -def test_success(plugin, readline, write): +def test_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_owned_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.coro.return_value = [ Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)), Game( @@ -67,14 +67,14 @@ def test_success(plugin, readline, write): } } -def test_failure(plugin, readline, write): +def test_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "3", "method": "import_owned_games" } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_owned_games.assert_called_with() diff --git a/tests/test_persistent_cache.py b/tests/test_persistent_cache.py index 056ddb5..d2d9d8c 100644 --- a/tests/test_persistent_cache.py +++ b/tests/test_persistent_cache.py @@ -28,7 +28,7 @@ def cache_data(): } -def test_initialize_cache(plugin, readline, write, cache_data): +def test_initialize_cache(plugin, read, write, cache_data): request_id = 3 request = { "jsonrpc": "2.0", @@ -36,7 +36,7 @@ def test_initialize_cache(plugin, readline, write, cache_data): "method": "initialize_cache", "params": {"data": cache_data} } - readline.side_effect = [json.dumps(request)] + read.side_effect = [json.dumps(request).encode() + b"\n"] assert {} == plugin.persistent_cache asyncio.run(plugin.run()) diff --git a/tests/test_uninstall_game.py b/tests/test_uninstall_game.py index 2e7c4ef..40a316b 100644 --- a/tests/test_uninstall_game.py +++ b/tests/test_uninstall_game.py @@ -1,7 +1,7 @@ import asyncio import json -def test_success(plugin, readline): +def test_success(plugin, read): request = { "jsonrpc": "2.0", "method": "uninstall_game", @@ -10,7 +10,7 @@ def test_success(plugin, readline): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_owned_games.return_value = None asyncio.run(plugin.run()) plugin.uninstall_game.assert_called_with(game_id="3") diff --git a/tests/test_users.py b/tests/test_users.py index 24c9dbb..47837ef 100644 --- a/tests/test_users.py +++ b/tests/test_users.py @@ -6,7 +6,7 @@ from galaxy.api.errors import UnknownError from galaxy.api.consts import PresenceState -def test_get_users_success(plugin, readline, write): +def test_get_users_success(plugin, read, write): request = { "jsonrpc": "2.0", "id": "8", @@ -16,7 +16,7 @@ def test_get_users_success(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_users.coro.return_value = [ UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline)) ] @@ -43,7 +43,7 @@ def test_get_users_success(plugin, readline, write): } -def test_get_users_failure(plugin, readline, write): +def test_get_users_failure(plugin, read, write): request = { "jsonrpc": "2.0", "id": "12", @@ -53,7 +53,7 @@ def test_get_users_failure(plugin, readline, write): } } - readline.side_effect = [json.dumps(request), ""] + read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_users.coro.side_effect = UnknownError() asyncio.run(plugin.run()) plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"]) From 77d742ce18c3fb22f09627c5006a722eda93a38a Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 28 Jun 2019 11:58:32 +0200 Subject: [PATCH 136/147] SDK-2910: Fix readline --- src/galaxy/api/jsonrpc.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index e491c3f..eb40ab3 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -74,7 +74,7 @@ class Server(): self._notifications = {} self._eof_listeners = [] self._input_buffer = bytes() - self._input_buffer_it = 0 + self._processed_input_buffer_it = 0 def register_method(self, name, callback, internal, sensitive_params=False): """ @@ -120,18 +120,21 @@ class Server(): async def _readline(self): """Like StreamReader.readline but without limit""" while True: - chunk = await self._reader.read(1024) - self._input_buffer += chunk - it = self._input_buffer.find(b"\n", self._input_buffer_it) - if it < 0: + # check if there is no unprocessed data in the buffer + if not self._input_buffer or self._processed_input_buffer_it != 0: + chunk = await self._reader.read(1024) if not chunk: return bytes() # EOF - else: - self._input_buffer_it = len(self._input_buffer) - continue + self._input_buffer += chunk + + it = self._input_buffer.find(b"\n", self._processed_input_buffer_it) + if it < 0: + self._processed_input_buffer_it = len(self._input_buffer) + continue + line = self._input_buffer[:it] self._input_buffer = self._input_buffer[it+1:] - self._input_buffer_it = 0 + self._processed_input_buffer_it = 0 return line def stop(self): From 67e8681de60c350ab63408a34c5570144fd01a9b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 28 Jun 2019 11:59:01 +0200 Subject: [PATCH 137/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 555c8ea..73b51c8 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.38", + version="0.39", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 4e1ea8056d66bb58ac5ca0e83cc6de7ce30aaf36 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Fri, 28 Jun 2019 14:00:44 +0200 Subject: [PATCH 138/147] Add StreamLineReader with unit tests --- src/galaxy/api/jsonrpc.py | 28 +++-------------- src/galaxy/reader.py | 28 +++++++++++++++++ tests/test_stream_line_reader.py | 52 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 src/galaxy/reader.py create mode 100644 tests/test_stream_line_reader.py diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index eb40ab3..37d0e17 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -5,6 +5,8 @@ import logging import inspect import json +from galaxy.reader import StreamLineReader + class JsonRpcError(Exception): def __init__(self, code, message, data=None): self.code = code @@ -67,14 +69,12 @@ def anonymise_sensitive_params(params, sensitive_params): class Server(): def __init__(self, reader, writer, encoder=json.JSONEncoder()): self._active = True - self._reader = reader + self._reader = StreamLineReader(reader) self._writer = writer self._encoder = encoder self._methods = {} self._notifications = {} self._eof_listeners = [] - self._input_buffer = bytes() - self._processed_input_buffer_it = 0 def register_method(self, name, callback, internal, sensitive_params=False): """ @@ -106,7 +106,7 @@ class Server(): async def run(self): while self._active: try: - data = await self._readline() + data = await self._reader.readline() if not data: self._eof() continue @@ -117,26 +117,6 @@ class Server(): logging.debug("Received %d bytes of data", len(data)) self._handle_input(data) - async def _readline(self): - """Like StreamReader.readline but without limit""" - while True: - # check if there is no unprocessed data in the buffer - if not self._input_buffer or self._processed_input_buffer_it != 0: - chunk = await self._reader.read(1024) - if not chunk: - return bytes() # EOF - self._input_buffer += chunk - - it = self._input_buffer.find(b"\n", self._processed_input_buffer_it) - if it < 0: - self._processed_input_buffer_it = len(self._input_buffer) - continue - - line = self._input_buffer[:it] - self._input_buffer = self._input_buffer[it+1:] - self._processed_input_buffer_it = 0 - return line - def stop(self): self._active = False diff --git a/src/galaxy/reader.py b/src/galaxy/reader.py new file mode 100644 index 0000000..551f803 --- /dev/null +++ b/src/galaxy/reader.py @@ -0,0 +1,28 @@ +from asyncio import StreamReader + + +class StreamLineReader: + """Handles StreamReader readline without buffer limit""" + def __init__(self, reader: StreamReader): + self._reader = reader + self._buffer = bytes() + self._processed_buffer_it = 0 + + async def readline(self): + while True: + # check if there is no unprocessed data in the buffer + if not self._buffer or self._processed_buffer_it != 0: + chunk = await self._reader.read(1024) + if not chunk: + return bytes() # EOF + self._buffer += chunk + + it = self._buffer.find(b"\n", self._processed_buffer_it) + if it < 0: + self._processed_buffer_it = len(self._buffer) + continue + + line = self._buffer[:it] + self._buffer = self._buffer[it+1:] + self._processed_buffer_it = 0 + return line diff --git a/tests/test_stream_line_reader.py b/tests/test_stream_line_reader.py new file mode 100644 index 0000000..2f81e6c --- /dev/null +++ b/tests/test_stream_line_reader.py @@ -0,0 +1,52 @@ +from unittest.mock import MagicMock + +import pytest + +from galaxy.reader import StreamLineReader +from galaxy.unittest.mock import AsyncMock + +@pytest.fixture() +def stream_reader(): + reader = MagicMock() + reader.read = AsyncMock() + return reader + +@pytest.fixture() +def read(stream_reader): + return stream_reader.read + +@pytest.fixture() +def reader(stream_reader): + return StreamLineReader(stream_reader) + +@pytest.mark.asyncio +async def test_message(reader, read): + read.return_value = b"a\n" + assert await reader.readline() == b"a" + read.assert_called_once() + +@pytest.mark.asyncio +async def test_separate_messages(reader, read): + read.side_effect = [b"a\n", b"b\n"] + assert await reader.readline() == b"a" + assert await reader.readline() == b"b" + assert read.call_count == 2 + +@pytest.mark.asyncio +async def test_connected_messages(reader, read): + read.return_value = b"a\nb\n" + assert await reader.readline() == b"a" + assert await reader.readline() == b"b" + read.assert_called_once() + +@pytest.mark.asyncio +async def test_cut_message(reader, read): + read.side_effect = [b"a", b"b\n"] + assert await reader.readline() == b"ab" + assert read.call_count == 2 + +@pytest.mark.asyncio +async def test_half_message(reader, read): + read.side_effect = [b"a", b""] + assert await reader.readline() == b"" + assert read.call_count == 2 From 2ebdfabd9b92eca29e2caf9b2adc7e7b0d3c9a36 Mon Sep 17 00:00:00 2001 From: Piotr Marzec Date: Fri, 28 Jun 2019 14:49:56 +0200 Subject: [PATCH 139/147] Path of Exile added --- PLATFORM_IDs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PLATFORM_IDs.md b/PLATFORM_IDs.md index c58dbc4..d931b1d 100644 --- a/PLATFORM_IDs.md +++ b/PLATFORM_IDs.md @@ -79,4 +79,4 @@ Platform ID list for GOG Galaxy 2.0 Integrations | psvita | Playstation Vita | | nds | Nintendo DS | | 3ds | Nintendo 3DS | - +| pathofexile | Path of Exile | From 7b3965ff4b2df55e90f6b98997d8d383ce57507e Mon Sep 17 00:00:00 2001 From: Aliaksei Paulouski Date: Fri, 28 Jun 2019 15:09:46 +0200 Subject: [PATCH 140/147] Add poe platform --- src/galaxy/api/consts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index fd5204e..d006714 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -80,6 +80,7 @@ class Platform(Enum): PlayStationVita = "psvita" NintendoDs = "nds" Nintendo3Ds = "3ds" + PathOfExile = "pathofexile" class Feature(Enum): """Possible features that can be implemented by an integration. From ff30675a256294d55dd39bd5b38163ecb3138d7b Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 27 Jun 2019 21:18:16 +0200 Subject: [PATCH 141/147] Do not invoke tick before handshake --- src/galaxy/api/jsonrpc.py | 1 + src/galaxy/api/plugin.py | 20 ++++++++++++-------- tests/test_internal.py | 13 ++++++++++++- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/galaxy/api/jsonrpc.py b/src/galaxy/api/jsonrpc.py index 37d0e17..87bff71 100644 --- a/src/galaxy/api/jsonrpc.py +++ b/src/galaxy/api/jsonrpc.py @@ -116,6 +116,7 @@ class Server(): data = data.strip() logging.debug("Received %d bytes of data", len(data)) self._handle_input(data) + await asyncio.sleep(0) # To not starve task queue def stop(self): self._active = False diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 22eb9d4..43eb95f 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -38,6 +38,7 @@ class Plugin: self._feature_methods = OrderedDict() self._active = True + self._pass_control_task = None self._reader, self._writer = reader, writer self._handshake_token = handshake_token @@ -210,15 +211,17 @@ class Plugin: async def run(self): """Plugin's main coroutine.""" - async def pass_control(): - while self._active: - try: - self.tick() - except Exception: - logging.exception("Unexpected exception raised in plugin tick") - await asyncio.sleep(1) + await self._server.run() + if self._pass_control_task is not None: + await self._pass_control_task - await asyncio.gather(pass_control(), self._server.run()) + async def _pass_control(self): + while self._active: + try: + self.tick() + except Exception: + logging.exception("Unexpected exception raised in plugin tick") + await asyncio.sleep(1) def _shutdown(self): logging.info("Shutting down") @@ -236,6 +239,7 @@ class Plugin: def _initialize_cache(self, data: Dict): self._persistent_cache = data self.handshake_complete() + self._pass_control_task = asyncio.create_task(self._pass_control()) @staticmethod def _ping(): diff --git a/tests/test_internal.py b/tests/test_internal.py index d381f4f..ec3dd77 100644 --- a/tests/test_internal.py +++ b/tests/test_internal.py @@ -62,7 +62,18 @@ def test_ping(plugin, read, write): "result": None } -def test_tick(plugin, read): +def test_tick_before_handshake(plugin, read): read.side_effect = [b""] asyncio.run(plugin.run()) + plugin.tick.assert_not_called() + +def test_tick_after_handshake(plugin, read): + request = { + "jsonrpc": "2.0", + "id": "6", + "method": "initialize_cache", + "params": {"data": {}} + } + read.side_effect = [json.dumps(request).encode() + b"\n", b""] + asyncio.run(plugin.run()) plugin.tick.assert_called_with() From 48e17824842dcd2468214763c83c75f6e0d2101a Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Thu, 27 Jun 2019 18:35:56 +0200 Subject: [PATCH 142/147] SDK-2893: Optional game time and last played --- src/galaxy/api/types.py | 4 ++-- tests/test_game_times.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/galaxy/api/types.py b/src/galaxy/api/types.py index fb3b908..21466ac 100644 --- a/src/galaxy/api/types.py +++ b/src/galaxy/api/types.py @@ -204,5 +204,5 @@ class GameTime(): :param last_time_played: last time the game was played (**unix timestamp**) """ game_id: str - time_played: int - last_played_time: int + time_played: Optional[int] + last_played_time: Optional[int] diff --git a/tests/test_game_times.py b/tests/test_game_times.py index 8f5d5ae..a9f11f7 100644 --- a/tests/test_game_times.py +++ b/tests/test_game_times.py @@ -16,7 +16,8 @@ def test_success(plugin, read, write): read.side_effect = [json.dumps(request).encode() + b"\n", b""] plugin.get_game_times.coro.return_value = [ GameTime("3", 60, 1549550504), - GameTime("5", 10, 1549550502) + GameTime("5", 10, None), + GameTime("7", None, 1549550502), ] asyncio.run(plugin.run()) plugin.get_game_times.assert_called_with() @@ -35,7 +36,10 @@ def test_success(plugin, read, write): { "game_id": "5", "time_played": 10, - "last_played_time": 1549550502 + }, + { + "game_id": "7", + "last_played_time": 1549550502 } ] } From c364b716f4301a32a208a7576105610e75f9e4cb Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 1 Jul 2019 13:16:08 +0200 Subject: [PATCH 143/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73b51c8..6b82ec5 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.39", + version="0.40", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From 9d9376286748b2d79c5bc0602a803114f223cbf6 Mon Sep 17 00:00:00 2001 From: Mieszko Banczerowski Date: Mon, 1 Jul 2019 14:32:23 +0200 Subject: [PATCH 144/147] Workaround for removing creds on push_cache --- src/galaxy/api/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index 43eb95f..bfa1d75 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -268,6 +268,7 @@ class Plugin: return Authentication(user_data['userId'], user_data['username']) """ + self.persistent_cache['credentials'] = credentials self._notification_client.notify("store_credentials", credentials, sensitive_params=True) def add_game(self, game: Game) -> None: From 2db9d0f38392208ff9e908e15273341b8e9e32cc Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Mon, 1 Jul 2019 14:35:05 +0200 Subject: [PATCH 145/147] Increment version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6b82ec5..2a3bc00 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="galaxy.plugin.api", - version="0.40", + version="0.40.1", description="GOG Galaxy Integrations Python API", author='Galaxy team', author_email='galaxy@gog.com', From f6b5a12b24bc6bc07b8219a30e1facf151dd8fb5 Mon Sep 17 00:00:00 2001 From: Romuald Juchnowicz-Bierbasz Date: Wed, 3 Jul 2019 13:42:06 +0200 Subject: [PATCH 146/147] SDK-2932: Remove github deployment (use mirroring) --- jenkins/release.groovy | 14 -------------- jenkins/release.py | 26 -------------------------- jenkins/requirements.txt | 1 - 3 files changed, 41 deletions(-) delete mode 100644 jenkins/release.groovy delete mode 100644 jenkins/release.py delete mode 100644 jenkins/requirements.txt diff --git a/jenkins/release.groovy b/jenkins/release.groovy deleted file mode 100644 index d1a8542..0000000 --- a/jenkins/release.groovy +++ /dev/null @@ -1,14 +0,0 @@ -stage('Upload to github') -{ - node('ActiveClientMacosxBuilder') { - deleteDir() - checkout scm - withPythonEnv('/usr/local/bin/python3.7') { - withCredentials([string(credentialsId: 'github_goggalaxy', variable: 'GITHUB_TOKEN')]) { - sh 'pip install -r jenkins/requirements.txt' - def version = sh(returnStdout: true, script: 'python setup.py --version').trim() - sh "python jenkins/release.py $version" - } - } - } -} diff --git a/jenkins/release.py b/jenkins/release.py deleted file mode 100644 index 12ef332..0000000 --- a/jenkins/release.py +++ /dev/null @@ -1,26 +0,0 @@ -import os -import sys -from galaxy.github.exporter import transfer_repo - -GITHUB_USERNAME = "goggalaxy" -GITHUB_EMAIL = "galaxy-sdk@gog.com" -GITHUB_TOKEN = os.environ["GITHUB_TOKEN"] -GITHUB_REPO_NAME = "galaxy-integrations-python-api" -SOURCE_BRANCH = os.environ["GIT_REFSPEC"] - -GITLAB_USERNAME = "galaxy-client" -GITLAB_REPO_NAME = "galaxy-plugin-api" - -def version_provider(_): - return sys.argv[1] - -gh_version = transfer_repo( - version_provider=version_provider, - source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME), - source_include_elements=["src", "docs", "tests", "requirements.txt", ".readthedocs.yml" ".gitignore", "*.md", "pytest.ini", "setup.py"], - source_branch=SOURCE_BRANCH, - dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME), - dest_branch="master", - dest_user_email=GITHUB_EMAIL, - dest_user_name="GOG Galaxy SDK Team" -) \ No newline at end of file diff --git a/jenkins/requirements.txt b/jenkins/requirements.txt deleted file mode 100644 index d76c88e..0000000 --- a/jenkins/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1 \ No newline at end of file From c083a3089a6738f5fed01bb843961e523fc21132 Mon Sep 17 00:00:00 2001 From: rbierbasz-gog <52658196+rbierbasz-gog@users.noreply.github.com> Date: Mon, 8 Jul 2019 15:40:25 +0200 Subject: [PATCH 147/147] Create .travis.yml --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..388c5ad --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +dist: xenial # required for Python >= 3.7 +language: python +python: + - "3.7" +install: + - pip install -r requirements.txt +script: + - pytest