mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52273e2f8c | ||
|
|
bda867473c | ||
|
|
6885cdc439 | ||
|
|
88e25a93be | ||
|
|
67e7a4c0b2 | ||
|
|
788d2550e6 | ||
|
|
059a1ea343 | ||
|
|
300ade5d43 | ||
|
|
43556a0470 | ||
|
|
e244d3bb44 | ||
|
|
d6e6efc633 |
@@ -30,4 +30,9 @@ pip install -r requirements.txt
|
|||||||
Run tests:
|
Run tests:
|
||||||
```bash
|
```bash
|
||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### 0.14
|
||||||
|
* Added required version parameter to Plugin constructor.
|
||||||
@@ -31,7 +31,7 @@ class LicenseType(Enum):
|
|||||||
OtherUserLicense = "OtherUserLicense"
|
OtherUserLicense = "OtherUserLicense"
|
||||||
|
|
||||||
class LocalGameState(Enum):
|
class LocalGameState(Enum):
|
||||||
Unknown = "Unknown"
|
None_ = "None"
|
||||||
Installed = "Installed"
|
Installed = "Installed"
|
||||||
Running = "Running"
|
Running = "Running"
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -104,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:
|
||||||
@@ -143,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())
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -20,8 +23,10 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
class Plugin():
|
class Plugin():
|
||||||
def __init__(self, platform, reader, writer, handshake_token):
|
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||||
|
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
|
self._version = version
|
||||||
|
|
||||||
self._feature_methods = OrderedDict()
|
self._feature_methods = OrderedDict()
|
||||||
self._active = True
|
self._active = True
|
||||||
@@ -167,7 +172,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 +317,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)
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.8",
|
version="0.14",
|
||||||
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',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def plugin(reader, writer):
|
|||||||
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
yield Plugin(Platform.Generic, reader, writer, "token")
|
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def my_caplog(caplog):
|
def my_caplog(caplog):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin
|
|||||||
from galaxy.api.consts import Platform, Feature
|
from galaxy.api.consts import Platform, Feature
|
||||||
|
|
||||||
def test_base_class():
|
def test_base_class():
|
||||||
plugin = Plugin(Platform.Generic, None, None, None)
|
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_no_overloads():
|
def test_no_overloads():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_one_method_feature():
|
def test_one_method_feature():
|
||||||
@@ -17,7 +17,7 @@ def test_one_method_feature():
|
|||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.ImportOwnedGames]
|
assert plugin.features == [Feature.ImportOwnedGames]
|
||||||
|
|
||||||
def test_multiple_methods_feature_all():
|
def test_multiple_methods_feature_all():
|
||||||
@@ -33,7 +33,7 @@ def test_multiple_methods_feature_all():
|
|||||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.Chat]
|
assert plugin.features == [Feature.Chat]
|
||||||
|
|
||||||
def test_multiple_methods_feature_not_all():
|
def test_multiple_methods_feature_not_all():
|
||||||
@@ -41,5 +41,5 @@ def test_multiple_methods_feature_not_all():
|
|||||||
async def send_message(self, room_id, message):
|
async def send_message(self, room_id, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_get_capabilites(reader, writer, readline, write):
|
|||||||
"method": "get_capabilities"
|
"method": "get_capabilities"
|
||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|||||||
Reference in New Issue
Block a user