Compare commits

...

16 Commits
0.4 ... 0.13

Author SHA1 Message Date
Pawel Kierski
6885cdc439 Increment version 2019-03-06 14:13:45 +01:00
Pawel Kierski
88e25a93be Ensure log folder exists 2019-03-06 11:15:33 +01:00
Pawel Kierski
67e7a4c0b2 Don't create log file if not specified 2019-03-05 12:19:48 +01:00
Paweł Kierski
788d2550e6 SDK-2552 optional achievement id or name 2019-03-05 12:18:33 +01:00
Rafal Makagon
059a1ea343 update logging facility in plugin API 2019-03-05 09:36:44 +01:00
Pawel Kierski
300ade5d43 Fix handling unknown notification 2019-03-04 11:52:08 +01:00
Paweł Kierski
43556a0470 SDK-2586 Return "None" instead of "Unknown" state for local game for Origin 2019-03-01 14:10:48 +01:00
Romuald Bierbasz
e244d3bb44 SDK-2577: Add UnknownBackendResponse 2019-02-28 15:20:03 +01:00
Romuald Bierbasz
d6e6efc633 SDK-2571: Refactor logging 2019-02-28 10:31:12 +01:00
Paweł Kierski
a114c9721c Add Unknown for all enums 2019-02-22 11:26:17 +01:00
Romuald Juchnowicz-Bierbasz
6c0389834b Increment version 2019-02-21 15:29:39 +01:00
Romuald Juchnowicz-Bierbasz
bc7d1c2914 SDK-2538: Use Optional 2019-02-21 15:17:38 +01:00
Romuald Juchnowicz-Bierbasz
d69e1aaa08 SDK-2538: Add LicenseType enum 2019-02-21 15:11:49 +01:00
Romuald Juchnowicz-Bierbasz
c2a0534162 Deploy only from master 2019-02-20 16:44:53 +01:00
Paweł Kierski
1614fd6eb2 Fix end of stream detecting 2019-02-20 16:41:44 +01:00
Paweł Kierski
48e54a8460 Revert "Make galaxy namespace package" 2019-02-20 14:09:34 +01:00
10 changed files with 112 additions and 32 deletions

View File

@@ -20,5 +20,7 @@ deploy_package:
- curl -X POST --silent --show-error --fail - curl -X POST --silent --show-error --fail
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}" "https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
when: manual when: manual
only:
- master
except: except:
- tags - tags

0
galaxy/__init__.py Normal file
View File

View File

@@ -12,6 +12,7 @@ class Platform(Enum):
Battlenet = "battlenet" Battlenet = "battlenet"
class Feature(Enum): class Feature(Enum):
Unknown = "Unknown"
ImportInstalledGames = "ImportInstalledGames" ImportInstalledGames = "ImportInstalledGames"
ImportOwnedGames = "ImportOwnedGames" ImportOwnedGames = "ImportOwnedGames"
LaunchGame = "LaunchGame" LaunchGame = "LaunchGame"
@@ -23,11 +24,19 @@ class Feature(Enum):
ImportUsers = "ImportUsers" ImportUsers = "ImportUsers"
VerifyGame = "VerifyGame" VerifyGame = "VerifyGame"
class LicenseType(Enum):
Unknown = "Unknown"
SinglePurchase = "SinglePurchase"
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Enum): class LocalGameState(Enum):
None_ = "None"
Installed = "Installed" Installed = "Installed"
Running = "Running" Running = "Running"
class PresenceState(Enum): class PresenceState(Enum):
Unknown = "Unknown"
Online = "online" Online = "online"
Offline = "offline" Offline = "offline"
Away = "away" Away = "away"

View File

@@ -20,6 +20,10 @@ class BackendError(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):
super().__init__(4, "Backend error", data) super().__init__(4, "Backend error", data)
class UnknownBackendResponse(ApplicationError):
def __init__(self, data=None):
super().__init__(4, "Backend responded in uknown way", data)
class InvalidCredentials(ApplicationError): class InvalidCredentials(ApplicationError):
def __init__(self, data=None): def __init__(self, data=None):
super().__init__(100, "Invalid credentials", data) super().__init__(100, "Invalid credentials", data)

View File

@@ -64,10 +64,12 @@ class Server():
async def run(self): async def run(self):
while self._active: while self._active:
data = await self._reader.readline() try:
if not data: data = await self._reader.readline()
# on windows rederecting a pipe to stdin result on continues if not data:
# not-blocking return of empty line on EOF self._eof()
continue
except:
self._eof() self._eof()
continue continue
data = data.strip() data = data.strip()
@@ -102,6 +104,7 @@ class Server():
method = self._notifications.get(request.method) method = self._notifications.get(request.method)
if not method: if not method:
logging.error("Received uknown notification: %s", request.method) logging.error("Received uknown notification: %s", request.method)
return
callback, internal = method callback, internal = method
if internal: if internal:
@@ -141,8 +144,8 @@ class Server():
self._send_error(request.id, MethodNotFound()) self._send_error(request.id, MethodNotFound())
except JsonRpcError as error: except JsonRpcError as error:
self._send_error(request.id, error) self._send_error(request.id, error)
except Exception as error: #pylint: disable=broad-except except Exception: #pylint: disable=broad-except
logging.error("Unexpected exception raised in plugin handler: %s", repr(error)) logging.exception("Unexpected exception raised in plugin handler")
asyncio.create_task(handle()) asyncio.create_task(handle())

View File

@@ -1,9 +1,12 @@
import asyncio import asyncio
import json import json
import logging import logging
import logging.handlers
import dataclasses import dataclasses
from enum import Enum from enum import Enum
from collections import OrderedDict from collections import OrderedDict
import sys
import os
from galaxy.api.jsonrpc import Server, NotificationClient from galaxy.api.jsonrpc import Server, NotificationClient
from galaxy.api.consts import Feature from galaxy.api.consts import Feature
@@ -21,6 +24,7 @@ class JSONEncoder(json.JSONEncoder):
class Plugin(): class Plugin():
def __init__(self, platform, reader, writer, handshake_token): def __init__(self, platform, reader, writer, handshake_token):
logging.info("Creating plugin for platform %s", platform.value)
self._platform = platform self._platform = platform
self._feature_methods = OrderedDict() self._feature_methods = OrderedDict()
@@ -167,7 +171,10 @@ class Plugin():
async def pass_control(): async def pass_control():
while self._active: while self._active:
logging.debug("Passing control to plugin") logging.debug("Passing control to plugin")
self.tick() try:
self.tick()
except Exception:
logging.exception("Unexpected exception raised in plugin tick")
await asyncio.sleep(1) await asyncio.sleep(1)
await asyncio.gather(pass_control(), self._server.run()) await asyncio.gather(pass_control(), self._server.run())
@@ -309,22 +316,56 @@ class Plugin():
async def get_game_times(self): async def get_game_times(self):
raise NotImplementedError() raise NotImplementedError()
def _prepare_logging(logger_file):
root = logging.getLogger()
root.setLevel(logging.DEBUG)
if logger_file:
# ensure destination folder exists
os.makedirs(os.path.dirname(os.path.abspath(logger_file)), exist_ok=True)
handler = logging.handlers.RotatingFileHandler(
logger_file,
mode="a",
maxBytes=10000000,
backupCount=10,
encoding="utf-8"
)
else:
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)
def create_and_run_plugin(plugin_class, argv): def create_and_run_plugin(plugin_class, argv):
if not issubclass(plugin_class, Plugin): logger_file = argv[3] if len(argv) >= 4 else None
raise TypeError("plugin_class must be subclass of Plugin") _prepare_logging(logger_file)
if len(argv) < 3: if len(argv) < 3:
raise ValueError("Not enough parameters, required: token, port") logging.critical("Not enough parameters, required: token, port")
sys.exit(1)
token = argv[1] token = argv[1]
try: try:
port = int(argv[2]) port = int(argv[2])
except ValueError as e: except ValueError:
raise ValueError("Failed to parse port value, {}".format(e)) logging.critical("Failed to parse port value: %s", argv[2])
sys.exit(2)
if not (1 <= port <= 65535): if not (1 <= port <= 65535):
raise ValueError("Port value out of range (1, 65535)") logging.critical("Port value out of range (1, 65535)")
sys.exit(3)
if not issubclass(plugin_class, Plugin):
logging.critical("plugin_class must be subclass of Plugin")
sys.exit(4)
async def coroutine(): async def coroutine():
reader, writer = await asyncio.open_connection("127.0.0.1", port) reader, writer = await asyncio.open_connection("127.0.0.1", port)
plugin = plugin_class(reader, writer, token) plugin = plugin_class(reader, writer, token)
await plugin.run() await plugin.run()
asyncio.run(coroutine())
try:
asyncio.run(coroutine())
except Exception:
logging.exception("Error while running plugin")
sys.exit(5)

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import List from typing import List, Optional
from galaxy.api.consts import LocalGameState, PresenceState from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@dataclass @dataclass
class Authentication(): class Authentication():
@@ -10,8 +10,8 @@ class Authentication():
@dataclass @dataclass
class LicenseInfo(): class LicenseInfo():
license_type: str license_type: LicenseType
owner: str = None owner: Optional[str] = None
@dataclass @dataclass
class Dlc(): class Dlc():
@@ -28,8 +28,13 @@ class Game():
@dataclass @dataclass
class Achievement(): class Achievement():
achievement_id: str
unlock_time: int unlock_time: int
achievement_id: Optional[str] = None
achievement_name: Optional[str] = None
def __post_init__(self):
assert self.achievement_id or self.achievement_name, \
"One of achievement_id or achievement_name is required"
@dataclass @dataclass
class LocalGame(): class LocalGame():
@@ -39,8 +44,8 @@ class LocalGame():
@dataclass @dataclass
class Presence(): class Presence():
presence_state: PresenceState presence_state: PresenceState
game_id: str = None game_id: Optional[str] = None
presence_status: str = None presence_status: Optional[str] = None
@dataclass @dataclass
class UserInfo(): class UserInfo():

View File

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

View File

@@ -1,9 +1,18 @@
import asyncio import asyncio
import json import json
from pytest import raises
from galaxy.api.types import Achievement from galaxy.api.types import Achievement
from galaxy.api.errors import UnknownError from galaxy.api.errors import UnknownError
def test_initialization_no_unlock_time():
with raises(Exception):
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
def test_initialization_no_id_nor_name():
with raises(AssertionError):
Achievement(unlock_time=1234567890)
def test_success(plugin, readline, write): def test_success(plugin, readline, write):
request = { request = {
"jsonrpc": "2.0", "jsonrpc": "2.0",
@@ -15,8 +24,9 @@ def test_success(plugin, readline, write):
} }
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.return_value = [ plugin.get_unlocked_achievements.return_value = [
Achievement("lvl10", 1548421241), Achievement(achievement_id="lvl10", unlock_time=1548421241),
Achievement("lvl20", 1548422395) Achievement(achievement_name="Got level 20", unlock_time=1548422395),
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called_with(game_id="14") plugin.get_unlocked_achievements.assert_called_with(game_id="14")
@@ -32,8 +42,13 @@ def test_success(plugin, readline, write):
"unlock_time": 1548421241 "unlock_time": 1548421241
}, },
{ {
"achievement_id": "lvl20", "achievement_name": "Got level 20",
"unlock_time": 1548422395 "unlock_time": 1548422395
},
{
"achievement_id": "lvl30",
"achievement_name": "Got level 30",
"unlock_time": 1548495633
} }
] ]
} }
@@ -65,7 +80,7 @@ def test_failure(plugin, readline, write):
} }
def test_unlock_achievement(plugin, write): def test_unlock_achievement(plugin, write):
achievement = Achievement("lvl20", 1548422395) achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
async def couritine(): async def couritine():
plugin.unlock_achievement("14", achievement) plugin.unlock_achievement("14", achievement)

View File

@@ -2,6 +2,7 @@ import asyncio
import json import json
from galaxy.api.types import Game, Dlc, LicenseInfo from galaxy.api.types import Game, Dlc, LicenseInfo
from galaxy.api.consts import LicenseType
from galaxy.api.errors import UnknownError from galaxy.api.errors import UnknownError
def test_success(plugin, readline, write): def test_success(plugin, readline, write):
@@ -13,15 +14,15 @@ def test_success(plugin, readline, write):
readline.side_effect = [json.dumps(request), ""] readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.return_value = [ plugin.get_owned_games.return_value = [
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)), Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
Game( Game(
"5", "5",
"Witcher 3", "Witcher 3",
[ [
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)), Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)),
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)), Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
], ],
LicenseInfo("SinglePurchase", None)) LicenseInfo(LicenseType.SinglePurchase, None))
] ]
asyncio.run(plugin.run()) asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with() plugin.get_owned_games.assert_called_with()
@@ -89,7 +90,7 @@ def test_failure(plugin, readline, write):
} }
def test_add_game(plugin, write): def test_add_game(plugin, write):
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
async def couritine(): async def couritine():
plugin.add_game(game) plugin.add_game(game)
@@ -127,7 +128,7 @@ def test_remove_game(plugin, write):
} }
def test_update_game(plugin, write): def test_update_game(plugin, write):
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)) game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
async def couritine(): async def couritine():
plugin.update_game(game) plugin.update_game(game)