mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2025-12-31 19:08:16 -05:00
Compare commits
5 Commits
deployed_0
...
0.33
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909cc10762 | ||
|
|
9b4537c54f | ||
|
|
2e90c66390 | ||
|
|
8647b06ca2 | ||
|
|
f6522be74d |
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="galaxy.plugin.api",
|
||||
version="0.32.1",
|
||||
version="0.33",
|
||||
description="GOG Galaxy Integrations Python API",
|
||||
author='Galaxy team',
|
||||
author_email='galaxy@gog.com',
|
||||
|
||||
@@ -22,6 +22,10 @@ class UnknownBackendResponse(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(4, "Backend responded in uknown way", data)
|
||||
|
||||
class TooManyRequests(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(5, "Too many requests. Try again later", data)
|
||||
|
||||
class InvalidCredentials(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(100, "Invalid credentials", data)
|
||||
@@ -50,18 +54,6 @@ class AccessDenied(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(106, "Access denied", data)
|
||||
|
||||
class ParentalControlBlock(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(107, "Parental control block", data)
|
||||
|
||||
class DeviceBlocked(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(108, "Device blocked", data)
|
||||
|
||||
class RegionBlocked(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(109, "Region blocked", data)
|
||||
|
||||
class FailedParsingManifest(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(200, "Failed parsing manifest", data)
|
||||
@@ -80,4 +72,4 @@ class MessageNotFound(ApplicationError):
|
||||
|
||||
class ImportInProgress(ApplicationError):
|
||||
def __init__(self, data=None):
|
||||
super().__init__(600, "Import already in progress", data)
|
||||
super().__init__(600, "Import already in progress", data)
|
||||
|
||||
@@ -54,17 +54,15 @@ Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_p
|
||||
|
||||
def anonymise_sensitive_params(params, sensitive_params):
|
||||
anomized_data = "****"
|
||||
if not sensitive_params:
|
||||
return params
|
||||
|
||||
if isinstance(sensitive_params, bool):
|
||||
if sensitive_params:
|
||||
return {k:anomized_data for k,v in params.items()}
|
||||
|
||||
if isinstance(sensitive_params, Iterable):
|
||||
anomized_params = params.copy()
|
||||
for key in anomized_params.keys():
|
||||
if key in sensitive_params:
|
||||
anomized_params[key] = anomized_data
|
||||
return anomized_params
|
||||
return {k: anomized_data if k in sensitive_params else v for k, v in params.items()}
|
||||
|
||||
return anomized_data
|
||||
return params
|
||||
|
||||
class Server():
|
||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||
|
||||
@@ -15,8 +15,9 @@ from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError
|
||||
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
|
||||
def default(self, o): # pylint: disable=method-hidden
|
||||
if dataclasses.is_dataclass(o):
|
||||
# filter None values
|
||||
def dict_factory(elements):
|
||||
@@ -26,7 +27,8 @@ class JSONEncoder(json.JSONEncoder):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
class Plugin():
|
||||
|
||||
class Plugin:
|
||||
"""Use and override methods of this class to create a new platform integration."""
|
||||
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||
@@ -50,9 +52,12 @@ class Plugin():
|
||||
self._achievements_import_in_progress = False
|
||||
self._game_times_import_in_progress = False
|
||||
|
||||
self._persistent_cache = dict()
|
||||
|
||||
# internal
|
||||
self._register_method("shutdown", self._shutdown, internal=True)
|
||||
self._register_method("get_capabilities", self._get_capabilities, internal=True)
|
||||
self._register_method("initialize_cache", self._initialize_cache, internal=True)
|
||||
self._register_method("ping", self._ping, internal=True)
|
||||
|
||||
# implemented by developer
|
||||
@@ -156,6 +161,12 @@ class Plugin():
|
||||
|
||||
return features
|
||||
|
||||
@property
|
||||
def persistent_cache(self) -> Dict:
|
||||
"""The cache is only available after the :meth:`~.handshake_complete()` is called.
|
||||
"""
|
||||
return self._persistent_cache
|
||||
|
||||
def _implements(self, handlers):
|
||||
for handler in handlers:
|
||||
if handler.__name__ not in self.__class__.__dict__:
|
||||
@@ -192,7 +203,7 @@ class Plugin():
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
async def run(self):
|
||||
"""Plugin main coroutine."""
|
||||
"""Plugin's main coroutine."""
|
||||
async def pass_control():
|
||||
while self._active:
|
||||
try:
|
||||
@@ -216,6 +227,10 @@ class Plugin():
|
||||
"token": self._handshake_token
|
||||
}
|
||||
|
||||
def _initialize_cache(self, data: Dict):
|
||||
self._persistent_cache = data
|
||||
self.handshake_complete()
|
||||
|
||||
@staticmethod
|
||||
def _ping():
|
||||
pass
|
||||
@@ -264,7 +279,7 @@ class Plugin():
|
||||
self.add_game(game)
|
||||
|
||||
"""
|
||||
params = {"owned_game" : game}
|
||||
params = {"owned_game": game}
|
||||
self._notification_client.notify("owned_game_added", params)
|
||||
|
||||
def remove_game(self, game_id: str):
|
||||
@@ -286,7 +301,7 @@ class Plugin():
|
||||
self.remove_game(game.game_id)
|
||||
|
||||
"""
|
||||
params = {"game_id" : game_id}
|
||||
params = {"game_id": game_id}
|
||||
self._notification_client.notify("owned_game_removed", params)
|
||||
|
||||
def update_game(self, game: Game):
|
||||
@@ -295,7 +310,7 @@ class Plugin():
|
||||
|
||||
:param game: Game to update
|
||||
"""
|
||||
params = {"owned_game" : game}
|
||||
params = {"owned_game": game}
|
||||
self._notification_client.notify("owned_game_updated", params)
|
||||
|
||||
def unlock_achievement(self, game_id: str, achievement: Achievement):
|
||||
@@ -367,7 +382,7 @@ class Plugin():
|
||||
if self._check_statuses_task is None or self._check_statuses_task.done():
|
||||
self._check_statuses_task = asyncio.create_task(self._check_statuses())
|
||||
"""
|
||||
params = {"local_game" : local_game}
|
||||
params = {"local_game": local_game}
|
||||
self._notification_client.notify("local_game_status_changed", params)
|
||||
|
||||
def add_friend(self, user: FriendInfo):
|
||||
@@ -375,7 +390,7 @@ class Plugin():
|
||||
|
||||
:param user: FriendInfo of a user that the client will add to friends list
|
||||
"""
|
||||
params = {"friend_info" : user}
|
||||
params = {"friend_info": user}
|
||||
self._notification_client.notify("friend_added", params)
|
||||
|
||||
def remove_friend(self, user_id: str):
|
||||
@@ -383,7 +398,7 @@ class Plugin():
|
||||
|
||||
:param user_id: id of the user to remove from friends list
|
||||
"""
|
||||
params = {"user_id" : user_id}
|
||||
params = {"user_id": user_id}
|
||||
self._notification_client.notify("friend_removed", params)
|
||||
|
||||
def update_room(self, room_id: str, unread_message_count=None, new_messages=None):
|
||||
@@ -406,7 +421,7 @@ class Plugin():
|
||||
|
||||
:param game_time: game time to update
|
||||
"""
|
||||
params = {"game_time" : game_time}
|
||||
params = {"game_time": game_time}
|
||||
self._notification_client.notify("game_time_updated", params)
|
||||
|
||||
def game_time_import_success(self, game_time: GameTime):
|
||||
@@ -415,7 +430,7 @@ class Plugin():
|
||||
|
||||
:param game_time: game_time which was imported
|
||||
"""
|
||||
params = {"game_time" : game_time}
|
||||
params = {"game_time": game_time}
|
||||
self._notification_client.notify("game_time_import_success", params)
|
||||
|
||||
def game_time_import_failure(self, game_id: str, error: ApplicationError):
|
||||
@@ -446,7 +461,22 @@ class Plugin():
|
||||
"""
|
||||
self._notification_client.notify("authentication_lost", None)
|
||||
|
||||
def push_cache(self):
|
||||
"""Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
|
||||
"""
|
||||
self._notification_client.notify(
|
||||
"push_cache",
|
||||
params={"data": self._persistent_cache}
|
||||
)
|
||||
|
||||
# handlers
|
||||
def handshake_complete(self):
|
||||
"""This method is called right after the handshake with the GOG Galaxy Client is complete and
|
||||
before any other operations are called by the GOG Galaxy Client.
|
||||
Persistent cache is available when this method is called.
|
||||
Override it if you need to do additional plugin initializations.
|
||||
This method is called internally."""
|
||||
|
||||
def tick(self):
|
||||
"""This method is called periodically.
|
||||
Override it to implement periodical non-blocking tasks.
|
||||
@@ -470,14 +500,14 @@ class Plugin():
|
||||
def shutdown(self):
|
||||
"""This method is called on integration shutdown.
|
||||
Override it to implement tear down.
|
||||
This method is called by the GOG Galaxy client."""
|
||||
This method is called by the GOG Galaxy Client."""
|
||||
|
||||
# methods
|
||||
async def authenticate(self, stored_credentials:dict=None):
|
||||
async def authenticate(self, stored_credentials: dict = None):
|
||||
"""Override this method to handle user authentication.
|
||||
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
|
||||
or :class:`~galaxy.api.types.NextStep` if it requires going to another url.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param stored_credentials: If the client received any credentials to store locally
|
||||
in the previous session they will be passed here as a parameter.
|
||||
@@ -506,7 +536,7 @@ class Plugin():
|
||||
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
||||
This method should either return galaxy.api.types.Authentication if the authentication is finished
|
||||
or galaxy.api.types.NextStep if it requires going to another cef url.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param step: deprecated.
|
||||
:param credentials: end_uri previous NextStep finished on.
|
||||
@@ -531,8 +561,8 @@ class Plugin():
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_owned_games(self) -> List[Game]:
|
||||
"""Override this method to return owned games for currenly logged in user.
|
||||
This method is called by the GOG Galaxy client.
|
||||
"""Override this method to return owned games for currently logged in user.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
Example of possible override of the method:
|
||||
|
||||
@@ -558,7 +588,7 @@ class Plugin():
|
||||
|
||||
async def start_achievements_import(self, game_ids: List[str]):
|
||||
"""Starts the task of importing achievements.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param game_ids: ids of the games for which the achievements are imported
|
||||
"""
|
||||
@@ -580,9 +610,9 @@ class Plugin():
|
||||
Override this method to return the unlocked achievements
|
||||
of the user that is currently logged in to the plugin.
|
||||
Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param game_id: ids of the games for which to import unlocked achievements
|
||||
:param game_ids: ids of the games for which to import unlocked achievements
|
||||
"""
|
||||
async def import_game_achievements(game_id):
|
||||
try:
|
||||
@@ -597,7 +627,7 @@ class Plugin():
|
||||
async def get_local_games(self) -> List[LocalGame]:
|
||||
"""Override this method to return the list of
|
||||
games present locally on the users pc.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
Example of possible override of the method:
|
||||
|
||||
@@ -619,7 +649,7 @@ class Plugin():
|
||||
async def launch_game(self, game_id: str):
|
||||
"""Override this method to launch the game
|
||||
identified by the provided game_id.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param str game_id: id of the game to launch
|
||||
|
||||
@@ -637,7 +667,7 @@ class Plugin():
|
||||
async def install_game(self, game_id: str):
|
||||
"""Override this method to install the game
|
||||
identified by the provided game_id.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param str game_id: id of the game to install
|
||||
|
||||
@@ -655,7 +685,7 @@ class Plugin():
|
||||
async def uninstall_game(self, game_id: str):
|
||||
"""Override this method to uninstall the game
|
||||
identified by the provided game_id.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param str game_id: id of the game to uninstall
|
||||
|
||||
@@ -673,7 +703,7 @@ class Plugin():
|
||||
async def get_friends(self) -> List[FriendInfo]:
|
||||
"""Override this method to return the friends list
|
||||
of the currently authenticated user.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
Example of possible override of the method:
|
||||
|
||||
@@ -692,7 +722,7 @@ class Plugin():
|
||||
|
||||
async def get_users(self, user_id_list: List[str]) -> List[UserInfo]:
|
||||
"""WIP, Override this method to return the list of users matching the provided ids.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param user_id_list: list of user ids
|
||||
"""
|
||||
@@ -700,7 +730,7 @@ class Plugin():
|
||||
|
||||
async def send_message(self, room_id: str, message_text: str):
|
||||
"""WIP, Override this method to send message to a chat room.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param room_id: id of the room to which the message should be sent
|
||||
:param message_text: text which should be sent in the message
|
||||
@@ -709,22 +739,23 @@ class Plugin():
|
||||
|
||||
async def mark_as_read(self, room_id: str, last_message_id: str):
|
||||
"""WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param room_id: id of the room
|
||||
:param last_message_id: id of the last message; room is marked as read only if this id matches the last message id known to the client
|
||||
:param last_message_id: id of the last message; room is marked as read only if this id matches
|
||||
the last message id known to the client
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_rooms(self) -> List[Room]:
|
||||
"""WIP, Override this method to return the chat rooms in which the user is currently in.
|
||||
This method is called by the GOG Galaxy client
|
||||
This method is called by the GOG Galaxy Client
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_room_history_from_message(self, room_id: str, message_id: str):
|
||||
"""WIP, Override this method to return the chat room history since the message provided in parameter.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param room_id: id of the room
|
||||
:param message_id: id of the message since which the history should be retrieved
|
||||
@@ -733,7 +764,7 @@ class Plugin():
|
||||
|
||||
async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int):
|
||||
"""WIP, Override this method to return the chat room history since the timestamp provided in parameter.
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param room_id: id of the room
|
||||
:param from_timestamp: timestamp since which the history should be retrieved
|
||||
@@ -749,7 +780,7 @@ class Plugin():
|
||||
|
||||
async def start_game_times_import(self, game_ids: List[str]):
|
||||
"""Starts the task of importing game times
|
||||
This method is called by the GOG Galaxy client.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
:param game_ids: ids of the games for which the game time is imported
|
||||
"""
|
||||
@@ -829,7 +860,7 @@ 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')
|
||||
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()
|
||||
|
||||
@@ -6,10 +6,11 @@ import aiohttp
|
||||
import certifi
|
||||
|
||||
from galaxy.api.errors import (
|
||||
AccessDenied, AuthenticationRequired,
|
||||
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError
|
||||
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||
TooManyRequests, 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)
|
||||
@@ -39,6 +40,8 @@ class HttpClient:
|
||||
raise AccessDenied()
|
||||
if response.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||
raise BackendNotAvailable()
|
||||
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise TooManyRequests()
|
||||
if response.status >= 500:
|
||||
raise BackendError()
|
||||
if response.status >= 400:
|
||||
|
||||
@@ -33,6 +33,7 @@ def write(writer):
|
||||
def plugin(reader, writer):
|
||||
"""Return plugin instance with all feature methods mocked"""
|
||||
async_methods = (
|
||||
"handshake_complete",
|
||||
"authenticate",
|
||||
"get_owned_games",
|
||||
"get_unlocked_achievements",
|
||||
|
||||
@@ -6,8 +6,7 @@ import pytest
|
||||
from galaxy.api.types import Authentication
|
||||
from galaxy.api.errors import (
|
||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied,
|
||||
ParentalControlBlock, DeviceBlocked, RegionBlocked
|
||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
||||
)
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
@@ -44,9 +43,6 @@ def test_success(plugin, readline, write):
|
||||
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
|
||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
||||
pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"),
|
||||
pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"),
|
||||
pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked")
|
||||
])
|
||||
def test_failure(plugin, readline, write, error, code, message):
|
||||
request = {
|
||||
|
||||
71
tests/test_persistent_cache.py
Normal file
71
tests/test_persistent_cache.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def assert_rpc_response(write, response_id, result=None):
|
||||
assert json.loads(write.call_args[0][0]) == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(response_id),
|
||||
"result": result
|
||||
}
|
||||
|
||||
|
||||
def assert_rpc_request(write, method, params=None):
|
||||
assert json.loads(write.call_args[0][0]) == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": {"data": params}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache_data():
|
||||
return {
|
||||
"persistent key": "persistent value",
|
||||
"persistent object": {"answer to everything": 42}
|
||||
}
|
||||
|
||||
|
||||
def test_initialize_cache(plugin, readline, write, cache_data):
|
||||
request_id = 3
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": str(request_id),
|
||||
"method": "initialize_cache",
|
||||
"params": {"data": cache_data}
|
||||
}
|
||||
readline.side_effect = [json.dumps(request)]
|
||||
|
||||
assert {} == plugin.persistent_cache
|
||||
asyncio.run(plugin.run())
|
||||
plugin.handshake_complete.assert_called_once_with()
|
||||
assert cache_data == plugin.persistent_cache
|
||||
assert_rpc_response(write, response_id=request_id)
|
||||
|
||||
|
||||
def test_set_cache(plugin, write, cache_data):
|
||||
async def runner():
|
||||
assert {} == plugin.persistent_cache
|
||||
|
||||
plugin.persistent_cache.update(cache_data)
|
||||
plugin.push_cache()
|
||||
|
||||
assert_rpc_request(write, "push_cache", cache_data)
|
||||
assert cache_data == plugin.persistent_cache
|
||||
|
||||
asyncio.run(runner())
|
||||
|
||||
|
||||
def test_clear_cache(plugin, write, cache_data):
|
||||
async def runner():
|
||||
plugin._persistent_cache = cache_data
|
||||
|
||||
plugin.persistent_cache.clear()
|
||||
plugin.push_cache()
|
||||
|
||||
assert_rpc_request(write, "push_cache", {})
|
||||
assert {} == plugin.persistent_cache
|
||||
|
||||
asyncio.run(runner())
|
||||
Reference in New Issue
Block a user