From f57e03db2de6dec138b823f6d4639dc95e9241db Mon Sep 17 00:00:00 2001 From: Rafal Makagon Date: Fri, 27 Sep 2019 16:15:57 +0200 Subject: [PATCH] Add game library settings feature --- src/galaxy/api/consts.py | 1 + src/galaxy/api/plugin.py | 77 ++++++++++- src/galaxy/api/types.py | 12 ++ tests/conftest.py | 5 +- tests/test_features.py | 3 +- tests/test_game_library_settings.py | 196 ++++++++++++++++++++++++++++ 6 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 tests/test_game_library_settings.py diff --git a/src/galaxy/api/consts.py b/src/galaxy/api/consts.py index d636613..71e9c68 100644 --- a/src/galaxy/api/consts.py +++ b/src/galaxy/api/consts.py @@ -110,6 +110,7 @@ class Feature(Enum): ImportFriends = "ImportFriends" ShutdownPlatformClient = "ShutdownPlatformClient" LaunchPlatformClient = "LaunchPlatformClient" + ImportGameLibrarySettings = "ImportGameLibrarySettings" class LicenseType(Enum): diff --git a/src/galaxy/api/plugin.py b/src/galaxy/api/plugin.py index e2330bb..82b1c7d 100644 --- a/src/galaxy/api/plugin.py +++ b/src/galaxy/api/plugin.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional, Set, Union from galaxy.api.consts import Feature from galaxy.api.errors import ImportInProgress, UnknownError from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server -from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep +from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep, GameLibrarySettings from galaxy.task_manager import TaskManager class JSONEncoder(json.JSONEncoder): @@ -46,6 +46,7 @@ class Plugin: self._achievements_import_in_progress = False self._game_times_import_in_progress = False + self._game_library_settings_import_in_progress = False self._persistent_cache = dict() @@ -109,6 +110,9 @@ class Plugin: self._register_method("start_game_times_import", self._start_game_times_import) self._detect_feature(Feature.ImportGameTime, ["get_game_time"]) + self._register_method("start_game_library_settings_import", self._start_game_library_settings_import) + self._detect_feature(Feature.ImportGameLibrarySettings, ["get_game_library_settings"]) + async def __aenter__(self): return self @@ -402,6 +406,20 @@ class Plugin: def _game_times_import_finished(self) -> None: self._notification_client.notify("game_times_import_finished", None) + def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None: + params = {"game_library_settings": game_library_settings} + self._notification_client.notify("game_library_settings_import_success", params) + + def _game_library_settings_import_failure(self, game_id: str, error: ApplicationError) -> None: + params = { + "game_id": game_id, + "error": error.json() + } + self._notification_client.notify("game_library_settings_import_failure", params) + + def _game_library_settings_import_finished(self) -> None: + self._notification_client.notify("game_library_settings_import_finished", None) + 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. @@ -750,6 +768,63 @@ class Plugin: (like updating cache). """ + async def _start_game_library_settings_import(self, game_ids: List[str]) -> None: + if self._game_library_settings_import_in_progress: + raise ImportInProgress() + + context = await self.prepare_game_library_settings_context(game_ids) + + async def import_game_library_settings(game_id, context_): + try: + game_library_settings = await self.get_game_library_settings(game_id, context_) + self._game_library_settings_import_success(game_library_settings) + except ApplicationError as error: + self._game_library_settings_import_failure(game_id, error) + except Exception: + logging.exception("Unexpected exception raised in import_game_library_settings") + self._game_library_settings_import_failure(game_id, UnknownError()) + + async def import_game_library_settings_set(game_ids_, context_): + try: + imports = [import_game_library_settings(game_id, context_) for game_id in game_ids_] + await asyncio.gather(*imports) + finally: + self._game_library_settings_import_finished() + self._game_library_settings_import_in_progress = False + self.game_library_settings_import_complete() + + self._external_task_manager.create_task( + import_game_library_settings_set(game_ids, context), + "game library settings import", + handle_exceptions=False + ) + self._game_library_settings_import_in_progress = True + + async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any: + """Override this method to prepare context for get_game_library_settings. + This allows for optimizations like batch requests to platform API. + Default implementation returns None. + + :param game_ids: the ids of the games for which game time are imported + :return: context + """ + return None + + async def get_game_library_settings(self, game_id: str, context: Any) -> GameLibrarySettings: + """Override this method to return the game library settings for the game + identified by the provided game_id. + This method is called by import task initialized by GOG Galaxy Client. + + :param game_id: the id of the game for which the game time is returned + :param context: the value returned from :meth:`prepare_game_library_settings_context` + :return: GameLibrarySettings object + """ + raise NotImplementedError() + + def game_library_settings_import_complete(self) -> None: + """Override this method to handle operations after game times import is finished + (like updating cache). + """ def create_and_run_plugin(plugin_class, argv): """Call this method as an entry point for the implemented integration. diff --git a/src/galaxy/api/types.py b/src/galaxy/api/types.py index 37d55a3..9fee40b 100644 --- a/src/galaxy/api/types.py +++ b/src/galaxy/api/types.py @@ -151,3 +151,15 @@ class GameTime(): game_id: str time_played: Optional[int] last_played_time: Optional[int] + +@dataclass +class GameLibrarySettings(): + """Library settings of a game, defines assigned tags and visibility flag. + + :param game_id: id of the related game + :param tags: collection of tags assigned to the game + :param hidden: indicates if the game should be hidden in GOG Galaxy application + """ + game_id: str + tags: Optional[List[str]] + hidden: Optional[bool] diff --git a/tests/conftest.py b/tests/conftest.py index a8fce46..623281a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,7 +49,10 @@ async def plugin(reader, writer): "game_times_import_complete", "shutdown_platform_client", "shutdown", - "tick" + "tick", + "get_game_library_settings", + "prepare_game_library_settings_context", + "game_library_settings_import_complete", ) with ExitStack() as stack: diff --git a/tests/test_features.py b/tests/test_features.py index 68a6cd0..27cb402 100644 --- a/tests/test_features.py +++ b/tests/test_features.py @@ -14,7 +14,8 @@ def test_base_class(): Feature.ImportGameTime, Feature.ImportFriends, Feature.ShutdownPlatformClient, - Feature.LaunchPlatformClient + Feature.LaunchPlatformClient, + Feature.ImportGameLibrarySettings } diff --git a/tests/test_game_library_settings.py b/tests/test_game_library_settings.py new file mode 100644 index 0000000..e335256 --- /dev/null +++ b/tests/test_game_library_settings.py @@ -0,0 +1,196 @@ +from unittest.mock import call + +import pytest +from galaxy.api.types import GameLibrarySettings +from galaxy.api.errors import BackendError +from galaxy.unittest.mock import async_return_value + +from tests import create_message, get_messages + + +@pytest.mark.asyncio +async def test_get_game_time_success(plugin, read, write): + plugin.prepare_game_library_settings_context.return_value = async_return_value("abc") + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_library_settings_import", + "params": { + "game_ids": ["3", "5", "7"] + } + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_game_library_settings.side_effect = [ + async_return_value(GameLibrarySettings("3", None, True)), + async_return_value(GameLibrarySettings("5", [], False)), + async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)), + ] + await plugin.run() + plugin.get_game_library_settings.assert_has_calls([ + call("3", "abc"), + call("5", "abc"), + call("7", "abc"), + ]) + plugin.game_library_settings_import_complete.assert_called_once_with() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_success", + "params": { + "game_library_settings": { + "game_id": "3", + "hidden": True + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_success", + "params": { + "game_library_settings": { + "game_id": "5", + "tags": [], + "hidden": False + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_success", + "params": { + "game_library_settings": { + "game_id": "7", + "tags": ["tag1", "tag2", "tag3"] + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_finished", + "params": None + } + ] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("exception,code,message", [ + (BackendError, 4, "Backend error"), + (KeyError, 0, "Unknown error") +]) +async def test_get_game_library_settings_error(exception, code, message, plugin, read, write): + plugin.prepare_game_library_settings_context.return_value = async_return_value(None) + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_library_settings_import", + "params": { + "game_ids": ["6"] + } + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + plugin.get_game_library_settings.side_effect = exception + await plugin.run() + plugin.get_game_library_settings.assert_called() + plugin.game_library_settings_import_complete.assert_called_once_with() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "result": None + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_failure", + "params": { + "game_id": "6", + "error": { + "code": code, + "message": message + } + } + }, + { + "jsonrpc": "2.0", + "method": "game_library_settings_import_finished", + "params": None + } + ] + + +@pytest.mark.asyncio +async def test_prepare_get_game_library_settings_context_error(plugin, read, write): + plugin.prepare_game_library_settings_context.side_effect = BackendError() + request = { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_library_settings_import", + "params": { + "game_ids": ["6"] + } + } + read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)] + await plugin.run() + + assert get_messages(write) == [ + { + "jsonrpc": "2.0", + "id": "3", + "error": { + "code": 4, + "message": "Backend error" + } + } + ] + + +@pytest.mark.asyncio +async def test_import_in_progress(plugin, read, write): + plugin.prepare_game_library_settings_context.return_value = async_return_value(None) + requests = [ + { + "jsonrpc": "2.0", + "id": "3", + "method": "start_game_library_settings_import", + "params": { + "game_ids": ["6"] + } + }, + { + "jsonrpc": "2.0", + "id": "4", + "method": "start_game_library_settings_import", + "params": { + "game_ids": ["7"] + } + } + ] + read.side_effect = [ + async_return_value(create_message(requests[0])), + async_return_value(create_message(requests[1])), + async_return_value(b"", 10) + ] + + await plugin.run() + + messages = get_messages(write) + assert { + "jsonrpc": "2.0", + "id": "3", + "result": None + } in messages + assert { + "jsonrpc": "2.0", + "id": "4", + "error": { + "code": 600, + "message": "Import already in progress" + } + } in messages +