Compare commits

...

16 Commits
0.25 ... 0.29

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
6bc91a12fa Register new requests 2019-05-17 16:42:13 +02:00
Romuald Juchnowicz-Bierbasz
6d513d86bf Increment version 2019-05-17 16:42:13 +02:00
Romuald Juchnowicz-Bierbasz
bdd2225262 Add new interface methods for game time and achievements 2019-05-17 16:42:12 +02:00
Rafal Makagon
68fdc4d188 Printing own port 2019-05-17 15:32:24 +02:00
Romuald Juchnowicz-Bierbasz
f283c10a95 Anonymise params in pass_login_credentials 2019-05-15 19:49:50 +02:00
Romuald Juchnowicz-Bierbasz
453734cefe Increment version 2019-05-10 17:21:31 +02:00
Romuald Juchnowicz-Bierbasz
85f1d83c28 Fix parameters 2019-05-10 17:21:05 +02:00
Romuald Juchnowicz-Bierbasz
701d3cf522 Increment version 2019-05-10 17:06:01 +02:00
Romuald Juchnowicz-Bierbasz
c8083b9006 Add cookie_jar param to HttpClient 2019-05-10 17:05:40 +02:00
Romuald Juchnowicz-Bierbasz
0608ade6d3 Fix name 2019-05-10 14:14:33 +02:00
Romuald Juchnowicz-Bierbasz
c349a3df8e Add timeout 2019-05-10 13:56:53 +02:00
Romuald Juchnowicz-Bierbasz
1fd959a665 Handle ServerDisconnectedError 2019-05-10 13:50:46 +02:00
Romuald Juchnowicz-Bierbasz
234a21d085 Add HttpClient 2019-05-10 13:36:32 +02:00
Romuald Juchnowicz-Bierbasz
90835ece58 Change project layout 2019-05-10 13:16:28 +02:00
Aliaksei Paulouski
9e1c8cfddd Add JS to NextStep params 2019-05-03 15:56:40 +02:00
Aliaksei Paulouski
f7f170b9ca Increment version 2019-05-03 15:56:40 +02:00
15 changed files with 359 additions and 7 deletions

View File

@@ -1,2 +1,8 @@
-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
certifi==2019.3.9

View File

@@ -2,9 +2,14 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.25",
version="0.29",
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'},
install_requires=[
"aiohttp==3.5.4",
"certifi==2019.3.9"
]
)

View File

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

View File

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

View File

@@ -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,14 +42,25 @@ 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)
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,
@@ -61,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,
@@ -114,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):
@@ -222,6 +241,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)
@@ -246,6 +285,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)
@@ -279,6 +335,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()
@@ -315,6 +393,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")
@@ -338,6 +442,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()

View File

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

43
src/galaxy/http.py Normal file
View File

@@ -0,0 +1,43 @@
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 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, ssl=ssl_context)
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
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()
except aiohttp.ServerDisconnectedError:
raise BackendNotAvailable()
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

View File

View File

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

View File

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