Compare commits

..

28 Commits

Author SHA1 Message Date
Rafal Makagon
ed91fd582c Deploy setup py 2019-05-31 12:08:49 +02:00
Rafal Makagon
bd393a96f0 Increment version 2019-05-31 11:50:53 +02:00
Piotr Marzec
c53aab1abb Legal Notice added 2019-05-31 11:29:05 +02:00
Romuald Juchnowicz-Bierbasz
80f40b1971 Increment version 2019-05-29 13:04:35 +02:00
Romuald Juchnowicz-Bierbasz
0da0296154 Release on gogcom 2019-05-28 17:59:11 +02:00
Romuald Juchnowicz-Bierbasz
9a115557b3 Fix username 2019-05-28 12:03:56 +02:00
Romuald Juchnowicz-Bierbasz
14c2d7d9e8 Update README 2019-05-28 11:28:32 +02:00
Romuald Juchnowicz-Bierbasz
4a7a759cea Update release scripts 2019-05-28 11:22:38 +02:00
Romuald Juchnowicz-Bierbasz
da8da24b01 Remove changelog 2019-05-28 11:19:48 +02:00
Rafal Makagon
ccbb13e685 Update README.md 2019-05-27 17:48:42 +02:00
Piotr Marzec
a3ca815975 cosmetic changes to readme.md 2019-05-27 17:41:28 +02:00
Romuald Juchnowicz-Bierbasz
f2d4127a31 Increment version 2019-05-24 14:04:17 +02:00
Romuald Juchnowicz-Bierbasz
07b6edce12 SDK-2840: Refactor import methods 2019-05-24 14:04:05 +02:00
Romuald Bierbasz
ef7f9ccca1 Fix pipeline 2019-05-21 14:27:02 +02:00
Romuald Juchnowicz-Bierbasz
3b296cbcc9 Fix pipeline 2019-05-21 14:18:45 +02:00
Romuald Juchnowicz-Bierbasz
f5361cd5ab Fix pipeline 2019-05-21 14:09:57 +02:00
Romuald Juchnowicz-Bierbasz
758909efba Fix release.py 2019-05-21 11:20:38 +02:00
Romuald Bierbasz
0bc8000f14 Fix release pipeline 2019-05-21 11:10:52 +02:00
Romuald Juchnowicz-Bierbasz
e62e7e0e6e Add pipeline for releasing to github 2019-05-20 15:52:35 +02:00
Romuald Juchnowicz-Bierbasz
be6c0eb03e Increment version 2019-05-20 15:38:24 +02:00
Aliaksei Paulouski
0ee56193de Fix aiohttp exception hierarchy 2019-05-20 11:20:27 +02:00
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
12 changed files with 402 additions and 28 deletions

View File

@@ -1,24 +1,52 @@
# 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
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(MockPlugin().run())
main()
```
Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale.
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
@@ -31,15 +59,9 @@ Run tests:
```bash
pytest
```
## Methods Documentation
TODO
## Changelog
## Legal Notice
### 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.
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.

14
jenkins/release.groovy Normal file
View File

@@ -0,0 +1,14 @@
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"
}
}
}
}

26
jenkins/release.py Normal file
View File

@@ -0,0 +1,26 @@
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", "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",
dest_user_email=GITHUB_EMAIL,
dest_user_name="GOG Galaxy SDK Team"
)

1
jenkins/requirements.txt Normal file
View File

@@ -0,0 +1 @@
git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1

View File

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

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.28",
version="0.31.3",
description="Galaxy python plugin API",
author='Galaxy team',
author_email='galaxy@gog.com',

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,32 @@ 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_task(game_ids):
try:
await self.import_games_achievements(game_ids)
finally:
self.achievements_import_finished()
self._achievements_import_in_progress = False
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()
@@ -315,6 +397,36 @@ 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_task(game_ids):
try:
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_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")
@@ -338,6 +450,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

@@ -7,15 +7,15 @@ import certifi
from galaxy.api.errors import (
AccessDenied, AuthenticationRequired,
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownError
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, 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)
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()
@@ -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:

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