Compare commits

..

7 Commits
0.45 ... 0.47

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
cec36695b6 Increment version 2019-08-13 16:08:59 +02:00
Romuald Juchnowicz-Bierbasz
4cc8be8f5d SDK-2997: Add launch_platform_client method 2019-08-13 15:23:20 +02:00
Vadim Suharnikov
f5eb32aa19 Fix a comment typo 2019-08-04 01:06:22 +02:00
Romuald Bierbasz
a76345ff6b Docs fixes 2019-08-02 17:23:56 +02:00
Romuald Bierbasz
c3bbeee54d Turn on type checking 2019-08-02 15:15:29 +02:00
Romuald Juchnowicz-Bierbasz
13a3f7577b Increment version 2019-08-02 15:13:11 +02:00
Romuald Bierbasz
f5b9adfbd5 Add achievements_import_complete and game_times_import_complete 2019-08-02 15:11:05 +02:00
16 changed files with 92 additions and 39 deletions

View File

@@ -47,7 +47,7 @@ templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and # List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files. # directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path. # This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [] exclude_patterns = [] # type: ignore
# -- Options for HTML output ------------------------------------------------- # -- Options for HTML output -------------------------------------------------

2
mypy.ini Normal file
View File

@@ -0,0 +1,2 @@
[mypy]
ignore_missing_imports = True

View File

@@ -1,2 +1,2 @@
[pytest] [pytest]
addopts = --flakes addopts = --flakes --mypy

View File

@@ -2,6 +2,7 @@
pytest==4.2.0 pytest==4.2.0
pytest-asyncio==0.10.0 pytest-asyncio==0.10.0
pytest-mock==1.10.3 pytest-mock==1.10.3
pytest-mypy==0.3.2
pytest-flakes==4.0.0 pytest-flakes==4.0.0
# because of pip bug https://github.com/pypa/pip/issues/4780 # because of pip bug https://github.com/pypa/pip/issues/4780
aiohttp==3.5.4 aiohttp==3.5.4

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="galaxy.plugin.api", name="galaxy.plugin.api",
version="0.45", version="0.47",
description="GOG Galaxy Integrations Python API", description="GOG Galaxy Integrations Python API",
author='Galaxy team', author='Galaxy team',
author_email='galaxy@gog.com', author_email='galaxy@gog.com',

View File

@@ -1 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__) __path__: str = __import__('pkgutil').extend_path(__path__, __name__)

View File

@@ -99,6 +99,7 @@ class Feature(Enum):
VerifyGame = "VerifyGame" VerifyGame = "VerifyGame"
ImportFriends = "ImportFriends" ImportFriends = "ImportFriends"
ShutdownPlatformClient = "ShutdownPlatformClient" ShutdownPlatformClient = "ShutdownPlatformClient"
LaunchPlatformClient = "LaunchPlatformClient"
class LicenseType(Enum): class LicenseType(Enum):

View File

@@ -1,6 +1,6 @@
from galaxy.api.jsonrpc import ApplicationError, UnknownError from galaxy.api.jsonrpc import ApplicationError, UnknownError
UnknownError = UnknownError assert UnknownError
class AuthenticationRequired(ApplicationError): class AuthenticationRequired(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):

View File

@@ -107,6 +107,9 @@ class Plugin:
self._register_notification("shutdown_platform_client", self.shutdown_platform_client) self._register_notification("shutdown_platform_client", self.shutdown_platform_client)
self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"]) self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"])
self._register_notification("launch_platform_client", self.launch_platform_client)
self._detect_feature(Feature.LaunchPlatformClient, ["launch_platform_client"])
self._register_method("import_friends", self.get_friends, result_name="friend_info_list") self._register_method("import_friends", self.get_friends, result_name="friend_info_list")
self._detect_feature(Feature.ImportFriends, ["get_friends"]) self._detect_feature(Feature.ImportFriends, ["get_friends"])
@@ -270,7 +273,7 @@ class Plugin:
"""Notify the client to remove game from the list of owned games """Notify the client to remove game from the list of owned games
of the currently authenticated user. of the currently authenticated user.
:param game_id: game id of the game to remove from the list of owned games :param game_id: the id of the game to remove from the list of owned games
Example use case of remove_game: Example use case of remove_game:
@@ -300,7 +303,7 @@ class Plugin:
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None: def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
"""Notify the client to unlock an achievement for a specific game. """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 game_id: the id of the game for which to unlock an achievement.
:param achievement: achievement to unlock. :param achievement: achievement to unlock.
""" """
params = { params = {
@@ -545,15 +548,18 @@ class Plugin:
finally: finally:
self._achievements_import_finished() self._achievements_import_finished()
self._achievements_import_in_progress = False self._achievements_import_in_progress = False
self.achievements_import_complete()
self.create_task(import_games_achievements(game_ids, context), "Games unlocked achievements import") self.create_task(import_games_achievements(game_ids, context), "Games unlocked achievements import")
self._achievements_import_in_progress = True self._achievements_import_in_progress = True
async def prepare_achievements_context(self, game_ids: List[str]) -> Any: async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
"""Override this method to prepare context for get_unlocked_achievements. """Override this method to prepare context for get_unlocked_achievements.
This allows for optimizations like batch requests to platform API. This allows for optimizations like batch requests to platform API.
Default implementation returns None. Default implementation returns None.
:param game_ids: the ids of the games for which achievements are imported
:return: context
""" """
return None return None
@@ -562,12 +568,17 @@ class Plugin:
for the game identified by the provided game_id. for the game identified by the provided game_id.
This method is called by import task initialized by GOG Galaxy Client. This method is called by import task initialized by GOG Galaxy Client.
:param game_id: :param game_id: the id of the game for which the achievements are returned
:param context: Value return from :meth:`prepare_achievements_context` :param context: the value returned from :meth:`prepare_achievements_context`
:return: :return: list of Achievement objects
""" """
raise NotImplementedError() raise NotImplementedError()
def achievements_import_complete(self):
"""Override this method to handle operations after achievements import is finished
(like updating cache).
"""
async def get_local_games(self) -> List[LocalGame]: async def get_local_games(self) -> List[LocalGame]:
"""Override this method to return the list of """Override this method to return the list of
games present locally on the users pc. games present locally on the users pc.
@@ -595,7 +606,7 @@ class Plugin:
identified by the provided game_id. 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 :param str game_id: the id of the game to launch
Example of possible override of the method: Example of possible override of the method:
@@ -613,7 +624,7 @@ class Plugin:
identified by the provided game_id. 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 :param str game_id: the id of the game to install
Example of possible override of the method: Example of possible override of the method:
@@ -631,7 +642,7 @@ class Plugin:
identified by the provided game_id. 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 :param str game_id: the id of the game to uninstall
Example of possible override of the method: Example of possible override of the method:
@@ -649,6 +660,11 @@ class Plugin:
This method is called by the GOG Galaxy Client.""" This method is called by the GOG Galaxy Client."""
raise NotImplementedError() raise NotImplementedError()
async def launch_platform_client(self) -> None:
"""Override this method to launch platform client.
This method is called by the GOG Galaxy Client."""
raise NotImplementedError()
async def get_friends(self) -> List[FriendInfo]: async def get_friends(self) -> List[FriendInfo]:
"""Override this method to return the friends list """Override this method to return the friends list
of the currently authenticated user. of the currently authenticated user.
@@ -692,6 +708,7 @@ class Plugin:
finally: finally:
self._game_times_import_finished() self._game_times_import_finished()
self._game_times_import_in_progress = False self._game_times_import_in_progress = False
self.game_times_import_complete()
self.create_task(import_game_times(game_ids, context), "Game times import") self.create_task(import_game_times(game_ids, context), "Game times import")
self._game_times_import_in_progress = True self._game_times_import_in_progress = True
@@ -700,6 +717,9 @@ class Plugin:
"""Override this method to prepare context for get_game_time. """Override this method to prepare context for get_game_time.
This allows for optimizations like batch requests to platform API. This allows for optimizations like batch requests to platform API.
Default implementation returns None. Default implementation returns None.
:param game_ids: the ids of the games for which game time are imported
:return: context
""" """
return None return None
@@ -708,12 +728,17 @@ class Plugin:
identified by the provided game_id. identified by the provided game_id.
This method is called by import task initialized by GOG Galaxy Client. This method is called by import task initialized by GOG Galaxy Client.
:param game_id: :param game_id: the id of the game for which the game time is returned
:param context: Value return from :meth:`prepare_game_times_context` :param context: the value returned from :meth:`prepare_game_times_context`
:return: :return: GameTime object
""" """
raise NotImplementedError() raise NotImplementedError()
def game_times_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): def create_and_run_plugin(plugin_class, argv):
"""Call this method as an entry point for the implemented integration. """Call this method as an entry point for the implemented integration.

View File

@@ -1,11 +1,8 @@
import platform import sys
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterable, NewType, Optional, Set from typing import Iterable, NewType, Optional, List, cast
def is_windows():
return platform.system() == "Windows"
ProcessId = NewType("ProcessId", int) ProcessId = NewType("ProcessId", int)
@@ -16,7 +13,7 @@ class ProcessInfo:
binary_path: Optional[str] binary_path: Optional[str]
if is_windows(): if sys.platform == "win32":
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
from ctypes.wintypes import DWORD from ctypes.wintypes import DWORD
@@ -25,14 +22,14 @@ if is_windows():
_PROC_ID_T = DWORD _PROC_ID_T = DWORD
list_size = 4096 list_size = 4096
def try_get_pids(list_size: int) -> Set[ProcessId]: def try_get_pids(list_size: int) -> List[ProcessId]:
result_size = DWORD() result_size = DWORD()
proc_id_list = (_PROC_ID_T * list_size)() proc_id_list = (_PROC_ID_T * list_size)()
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)): if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
raise WinError(descr="Failed to get process ID list: %s" % FormatError()) raise WinError(descr="Failed to get process ID list: %s" % FormatError()) # type: ignore
return proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))] return cast(List[ProcessId], proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))])
while True: while True:
proc_ids = try_get_pids(list_size) proc_ids = try_get_pids(list_size)
@@ -59,7 +56,7 @@ if is_windows():
exe_path_buffer = create_unicode_buffer(_MAX_PATH) exe_path_buffer = create_unicode_buffer(_MAX_PATH)
exe_path_len = DWORD(len(exe_path_buffer)) exe_path_len = DWORD(len(exe_path_buffer))
return exe_path_buffer[:exe_path_len.value] if windll.kernel32.QueryFullProcessImageNameW( return cast(str, exe_path_buffer[:exe_path_len.value]) if windll.kernel32.QueryFullProcessImageNameW(
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len) h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
) else None ) else None
@@ -86,6 +83,6 @@ else:
return process_info return process_info
def process_iter() -> Iterable[ProcessInfo]: def process_iter() -> Iterable[Optional[ProcessInfo]]:
for pid in pids(): for pid in pids():
yield get_process_info(pid) yield get_process_info(pid)

View File

@@ -37,13 +37,16 @@ def plugin(reader, writer):
"get_owned_games", "get_owned_games",
"prepare_achievements_context", "prepare_achievements_context",
"get_unlocked_achievements", "get_unlocked_achievements",
"achievements_import_complete",
"get_local_games", "get_local_games",
"launch_game", "launch_game",
"launch_platform_client",
"install_game", "install_game",
"uninstall_game", "uninstall_game",
"get_friends", "get_friends",
"get_game_time", "get_game_time",
"prepare_game_times_context", "prepare_game_times_context",
"game_times_import_complete",
"shutdown_platform_client", "shutdown_platform_client",
"shutdown", "shutdown",
"tick" "tick"

View File

@@ -40,6 +40,7 @@ async def test_get_unlocked_achievements_success(plugin, read, write):
await plugin.run() await plugin.run()
plugin.prepare_achievements_context.assert_called_with(["14"]) plugin.prepare_achievements_context.assert_called_with(["14"])
plugin.get_unlocked_achievements.assert_called_with("14", 5) plugin.get_unlocked_achievements.assert_called_with("14", 5)
plugin.achievements_import_complete.asert_called_with()
assert get_messages(write) == [ assert get_messages(write) == [
{ {
@@ -97,6 +98,7 @@ async def test_get_unlocked_achievements_error(exception, code, message, plugin,
plugin.get_unlocked_achievements.side_effect = exception plugin.get_unlocked_achievements.side_effect = exception
await plugin.run() await plugin.run()
plugin.get_unlocked_achievements.assert_called() plugin.get_unlocked_achievements.assert_called()
plugin.achievements_import_complete.asert_called_with()
assert get_messages(write) == [ assert get_messages(write) == [
{ {

View File

@@ -13,7 +13,8 @@ def test_base_class():
Feature.ImportAchievements, Feature.ImportAchievements,
Feature.ImportGameTime, Feature.ImportGameTime,
Feature.ImportFriends, Feature.ImportFriends,
Feature.ShutdownPlatformClient Feature.ShutdownPlatformClient,
Feature.LaunchPlatformClient
} }

View File

@@ -31,6 +31,7 @@ async def test_get_game_time_success(plugin, read, write):
call("5", "abc"), call("5", "abc"),
call("7", "abc"), call("7", "abc"),
]) ])
plugin.game_times_import_complete.assert_called_once_with()
assert get_messages(write) == [ assert get_messages(write) == [
{ {
@@ -96,6 +97,7 @@ async def test_get_game_time_error(exception, code, message, plugin, read, write
plugin.get_game_time.side_effect = exception plugin.get_game_time.side_effect = exception
await plugin.run() await plugin.run()
plugin.get_game_time.assert_called() plugin.get_game_time.assert_called()
plugin.game_times_import_complete.assert_called_once_with()
assert get_messages(write) == [ assert get_messages(write) == [
{ {

View File

@@ -3,6 +3,8 @@ from http import HTTPStatus
import aiohttp import aiohttp
import pytest import pytest
from multidict import CIMultiDict, CIMultiDictProxy
from yarl import URL
from galaxy.api.errors import ( from galaxy.api.errors import (
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError, AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
@@ -10,7 +12,7 @@ from galaxy.api.errors import (
) )
from galaxy.http import handle_exception from galaxy.http import handle_exception
request_info = aiohttp.RequestInfo("http://o.pl", "GET", {}) request_info = aiohttp.RequestInfo(URL("http://o.pl"), "GET", CIMultiDictProxy(CIMultiDict()))
@pytest.mark.parametrize( @pytest.mark.parametrize(
"aiohttp_exception,expected_exception_type", "aiohttp_exception,expected_exception_type",
@@ -18,15 +20,15 @@ request_info = aiohttp.RequestInfo("http://o.pl", "GET", {})
(asyncio.TimeoutError(), BackendTimeout), (asyncio.TimeoutError(), BackendTimeout),
(aiohttp.ServerDisconnectedError(), BackendNotAvailable), (aiohttp.ServerDisconnectedError(), BackendNotAvailable),
(aiohttp.ClientConnectionError(), NetworkError), (aiohttp.ClientConnectionError(), NetworkError),
(aiohttp.ContentTypeError(request_info, []), UnknownBackendResponse), (aiohttp.ContentTypeError(request_info, ()), UnknownBackendResponse),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.FORBIDDEN), AccessDenied), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.FORBIDDEN), AccessDenied),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.NOT_IMPLEMENTED), BackendError), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_IMPLEMENTED), BackendError),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.BAD_REQUEST), UnknownError), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.BAD_REQUEST), UnknownError),
(aiohttp.ClientResponseError(request_info, [], status=HTTPStatus.NOT_FOUND), UnknownError), (aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_FOUND), UnknownError),
(aiohttp.ClientError(), UnknownError) (aiohttp.ClientError(), UnknownError)
] ]
) )

View File

@@ -0,0 +1,17 @@
import pytest
from galaxy.unittest.mock import async_return_value
from tests import create_message
@pytest.mark.asyncio
async def test_success(plugin, read):
request = {
"jsonrpc": "2.0",
"method": "launch_platform_client"
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
plugin.launch_platform_client.return_value = async_return_value(None)
await plugin.run()
plugin.launch_platform_client.assert_called_with()