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
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
exclude_patterns = [] # type: ignore
# -- 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]
addopts = --flakes
addopts = --flakes --mypy

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.45",
version="0.47",
description="GOG Galaxy Integrations Python API",
author='Galaxy team',
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"
ImportFriends = "ImportFriends"
ShutdownPlatformClient = "ShutdownPlatformClient"
LaunchPlatformClient = "LaunchPlatformClient"
class LicenseType(Enum):

View File

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

View File

@@ -107,6 +107,9 @@ class Plugin:
self._register_notification("shutdown_platform_client", self.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._detect_feature(Feature.ImportFriends, ["get_friends"])
@@ -270,7 +273,7 @@ class Plugin:
"""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
:param game_id: the id of the game to remove from the list of owned games
Example use case of remove_game:
@@ -300,7 +303,7 @@ class Plugin:
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.
:param game_id: the id of the game for which to unlock an achievement.
:param achievement: achievement to unlock.
"""
params = {
@@ -545,15 +548,18 @@ class Plugin:
finally:
self._achievements_import_finished()
self._achievements_import_in_progress = False
self.achievements_import_complete()
self.create_task(import_games_achievements(game_ids, context), "Games unlocked achievements import")
self._achievements_import_in_progress = True
async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
"""Override this method to prepare context for get_unlocked_achievements.
This allows for optimizations like batch requests to platform API.
Default implementation returns None.
:param game_ids: the ids of the games for which achievements are imported
:return: context
"""
return None
@@ -562,12 +568,17 @@ class Plugin:
for the game identified by the provided game_id.
This method is called by import task initialized by GOG Galaxy Client.
:param game_id:
:param context: Value return from :meth:`prepare_achievements_context`
:return:
:param game_id: the id of the game for which the achievements are returned
:param context: the value returned from :meth:`prepare_achievements_context`
:return: list of Achievement objects
"""
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]:
"""Override this method to return the list of
games present locally on the users pc.
@@ -595,7 +606,7 @@ class Plugin:
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
:param str game_id: the id of the game to launch
Example of possible override of the method:
@@ -613,7 +624,7 @@ class Plugin:
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
:param str game_id: the id of the game to install
Example of possible override of the method:
@@ -631,7 +642,7 @@ class Plugin:
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
:param str game_id: the id of the game to uninstall
Example of possible override of the method:
@@ -649,6 +660,11 @@ class Plugin:
This method is called by the GOG Galaxy Client."""
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]:
"""Override this method to return the friends list
of the currently authenticated user.
@@ -692,6 +708,7 @@ class Plugin:
finally:
self._game_times_import_finished()
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._game_times_import_in_progress = True
@@ -700,6 +717,9 @@ class Plugin:
"""Override this method to prepare context for get_game_time.
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
@@ -708,12 +728,17 @@ class Plugin:
identified by the provided game_id.
This method is called by import task initialized by GOG Galaxy Client.
:param game_id:
:param context: Value return from :meth:`prepare_game_times_context`
:return:
:param game_id: the id of the game for which the game time is returned
:param context: the value returned from :meth:`prepare_game_times_context`
:return: GameTime object
"""
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):
"""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 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)
@@ -16,7 +13,7 @@ class ProcessInfo:
binary_path: Optional[str]
if is_windows():
if sys.platform == "win32":
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
from ctypes.wintypes import DWORD
@@ -25,14 +22,14 @@ if is_windows():
_PROC_ID_T = DWORD
list_size = 4096
def try_get_pids(list_size: int) -> Set[ProcessId]:
def try_get_pids(list_size: int) -> List[ProcessId]:
result_size = DWORD()
proc_id_list = (_PROC_ID_T * list_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:
proc_ids = try_get_pids(list_size)
@@ -59,7 +56,7 @@ if is_windows():
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
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)
) else None
@@ -86,6 +83,6 @@ else:
return process_info
def process_iter() -> Iterable[ProcessInfo]:
def process_iter() -> Iterable[Optional[ProcessInfo]]:
for pid in pids():
yield get_process_info(pid)

View File

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

View File

@@ -40,6 +40,7 @@ async def test_get_unlocked_achievements_success(plugin, read, write):
await plugin.run()
plugin.prepare_achievements_context.assert_called_with(["14"])
plugin.get_unlocked_achievements.assert_called_with("14", 5)
plugin.achievements_import_complete.asert_called_with()
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
await plugin.run()
plugin.get_unlocked_achievements.assert_called()
plugin.achievements_import_complete.asert_called_with()
assert get_messages(write) == [
{

View File

@@ -13,7 +13,8 @@ def test_base_class():
Feature.ImportAchievements,
Feature.ImportGameTime,
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("7", "abc"),
])
plugin.game_times_import_complete.assert_called_once_with()
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
await plugin.run()
plugin.get_game_time.assert_called()
plugin.game_times_import_complete.assert_called_once_with()
assert get_messages(write) == [
{

View File

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