mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-04-17 20:56:53 -04:00
SDK-2520: Move files from desktop-galaxy-client
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# pytest
|
||||
__pycache__/
|
||||
0
galaxy/__init__.py
Normal file
0
galaxy/__init__.py
Normal file
1
galaxy/api/__init__.py
Normal file
1
galaxy/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from galaxy.api.plugin import Plugin
|
||||
33
galaxy/api/consts.py
Normal file
33
galaxy/api/consts.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from enum import Enum
|
||||
|
||||
class Platform(Enum):
|
||||
Unknown = "unknown"
|
||||
Gog = "gog"
|
||||
Steam = "steam"
|
||||
Psn = "psn"
|
||||
XBoxOne = "xboxone"
|
||||
Generic = "generic"
|
||||
Origin = "origin"
|
||||
Uplay = "uplay"
|
||||
Battlenet = "battlenet"
|
||||
|
||||
class Feature(Enum):
|
||||
ImportInstalledGames = "ImportInstalledGames"
|
||||
ImportOwnedGames = "ImportOwnedGames"
|
||||
LaunchGame = "LaunchGame"
|
||||
InstallGame = "InstallGame"
|
||||
UninstallGame = "UninstallGame"
|
||||
ImportAchievements = "ImportAchievements"
|
||||
ImportGameTime = "ImportGameTime"
|
||||
Chat = "Chat"
|
||||
ImportUsers = "ImportUsers"
|
||||
VerifyGame = "VerifyGame"
|
||||
|
||||
class LocalGameState(Enum):
|
||||
Installed = "Installed"
|
||||
Running = "Running"
|
||||
|
||||
class PresenceState(Enum):
|
||||
Online = "online"
|
||||
Offline = "offline"
|
||||
Away = "away"
|
||||
202
galaxy/api/jsonrpc.py
Normal file
202
galaxy/api/jsonrpc.py
Normal file
@@ -0,0 +1,202 @@
|
||||
import asyncio
|
||||
from collections import namedtuple
|
||||
import logging
|
||||
import json
|
||||
|
||||
class JsonRpcError(Exception):
|
||||
def __init__(self, code, message, data=None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.data = data
|
||||
super().__init__()
|
||||
|
||||
class ParseError(JsonRpcError):
|
||||
def __init__(self):
|
||||
super().__init__(-32700, "Parse error")
|
||||
|
||||
class InvalidRequest(JsonRpcError):
|
||||
def __init__(self):
|
||||
super().__init__(-32600, "Invalid Request")
|
||||
|
||||
class MethodNotFound(JsonRpcError):
|
||||
def __init__(self):
|
||||
super().__init__(-32601, "Method not found")
|
||||
|
||||
class InvalidParams(JsonRpcError):
|
||||
def __init__(self):
|
||||
super().__init__(-32601, "Invalid params")
|
||||
|
||||
class ApplicationError(JsonRpcError):
|
||||
def __init__(self, data):
|
||||
super().__init__(-32003, "Custom error", data)
|
||||
|
||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||
Method = namedtuple("Method", ["callback", "internal"])
|
||||
|
||||
class Server():
|
||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||
self._active = True
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self._encoder = encoder
|
||||
self._methods = {}
|
||||
self._notifications = {}
|
||||
self._eof_listeners = []
|
||||
|
||||
def register_method(self, name, callback, internal):
|
||||
self._methods[name] = Method(callback, internal)
|
||||
|
||||
def register_notification(self, name, callback, internal):
|
||||
self._notifications[name] = Method(callback, internal)
|
||||
|
||||
def register_eof(self, callback):
|
||||
self._eof_listeners.append(callback)
|
||||
|
||||
async def run(self):
|
||||
while self._active:
|
||||
data = await self._reader.readline()
|
||||
if not data:
|
||||
# on windows rederecting a pipe to stdin result on continues
|
||||
# not-blocking return of empty line on EOF
|
||||
self._eof()
|
||||
continue
|
||||
data = data.strip()
|
||||
logging.debug("Received data: %s", data)
|
||||
self._handle_input(data)
|
||||
|
||||
def stop(self):
|
||||
self._active = False
|
||||
|
||||
def _eof(self):
|
||||
logging.info("Received EOF")
|
||||
self.stop()
|
||||
for listener in self._eof_listeners:
|
||||
listener()
|
||||
|
||||
def _handle_input(self, data):
|
||||
try:
|
||||
request = self._parse_request(data)
|
||||
except JsonRpcError as error:
|
||||
self._send_error(None, error)
|
||||
return
|
||||
|
||||
logging.debug("Parsed input: %s", request)
|
||||
|
||||
if request.id is not None:
|
||||
self._handle_request(request)
|
||||
else:
|
||||
self._handle_notification(request)
|
||||
|
||||
def _handle_notification(self, request):
|
||||
logging.debug("Handling notification %s", request)
|
||||
method = self._notifications.get(request.method)
|
||||
if not method:
|
||||
logging.error("Received uknown notification: %s", request.method)
|
||||
|
||||
callback, internal = method
|
||||
if internal:
|
||||
# internal requests are handled immediately
|
||||
callback(**request.params)
|
||||
else:
|
||||
try:
|
||||
asyncio.create_task(callback(**request.params))
|
||||
except Exception as error: #pylint: disable=broad-except
|
||||
logging.error(
|
||||
"Unexpected exception raised in notification handler: %s",
|
||||
repr(error)
|
||||
)
|
||||
|
||||
def _handle_request(self, request):
|
||||
logging.debug("Handling request %s", request)
|
||||
method = self._methods.get(request.method)
|
||||
|
||||
if not method:
|
||||
logging.error("Received uknown request: %s", request.method)
|
||||
self._send_error(request.id, MethodNotFound())
|
||||
return
|
||||
|
||||
callback, internal = method
|
||||
if internal:
|
||||
# internal requests are handled immediately
|
||||
response = callback(request.params)
|
||||
self._send_response(request.id, response)
|
||||
else:
|
||||
async def handle():
|
||||
try:
|
||||
result = await callback(request.params)
|
||||
self._send_response(request.id, result)
|
||||
except TypeError:
|
||||
self._send_error(request.id, InvalidParams())
|
||||
except NotImplementedError:
|
||||
self._send_error(request.id, MethodNotFound())
|
||||
except JsonRpcError as error:
|
||||
self._send_error(request.id, error)
|
||||
except Exception as error: #pylint: disable=broad-except
|
||||
logging.error("Unexpected exception raised in plugin handler: %s", repr(error))
|
||||
|
||||
asyncio.create_task(handle())
|
||||
|
||||
@staticmethod
|
||||
def _parse_request(data):
|
||||
try:
|
||||
jsonrpc_request = json.loads(data)
|
||||
if jsonrpc_request.get("jsonrpc") != "2.0":
|
||||
raise InvalidRequest()
|
||||
del jsonrpc_request["jsonrpc"]
|
||||
return Request(**jsonrpc_request)
|
||||
except json.JSONDecodeError:
|
||||
raise ParseError()
|
||||
except TypeError:
|
||||
raise InvalidRequest()
|
||||
|
||||
def _send(self, data):
|
||||
try:
|
||||
line = self._encoder.encode(data)
|
||||
logging.debug("Sending data: %s", line)
|
||||
self._writer.write(line + "\n")
|
||||
asyncio.create_task(self._writer.drain())
|
||||
except TypeError as error:
|
||||
logging.error(str(error))
|
||||
|
||||
def _send_response(self, request_id, result):
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"result": result
|
||||
}
|
||||
self._send(response)
|
||||
|
||||
def _send_error(self, request_id, error):
|
||||
response = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": error.code,
|
||||
"message": error.message,
|
||||
"data": error.data
|
||||
}
|
||||
}
|
||||
self._send(response)
|
||||
|
||||
class NotificationClient():
|
||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
||||
self._writer = writer
|
||||
self._encoder = encoder
|
||||
self._methods = {}
|
||||
|
||||
def notify(self, method, params):
|
||||
notification = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params
|
||||
}
|
||||
self._send(notification)
|
||||
|
||||
def _send(self, data):
|
||||
try:
|
||||
line = self._encoder.encode(data)
|
||||
logging.debug("Sending data: %s", line)
|
||||
self._writer.write(line + "\n")
|
||||
asyncio.create_task(self._writer.drain())
|
||||
except TypeError as error:
|
||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
||||
308
galaxy/api/plugin.py
Normal file
308
galaxy/api/plugin.py
Normal file
@@ -0,0 +1,308 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from collections import OrderedDict
|
||||
|
||||
from galaxy.api.jsonrpc import Server, NotificationClient
|
||||
from galaxy.api.stream import stdio
|
||||
from galaxy.api.consts import Feature
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
def default(self, o): # pylint: disable=method-hidden
|
||||
if dataclasses.is_dataclass(o):
|
||||
# filter None values
|
||||
def dict_factory(elements):
|
||||
return {k: v for k, v in elements if v is not None}
|
||||
return dataclasses.asdict(o, dict_factory=dict_factory)
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
return super().default(o)
|
||||
|
||||
class Plugin():
|
||||
def __init__(self, platform):
|
||||
self._platform = platform
|
||||
|
||||
self._feature_methods = OrderedDict()
|
||||
self._active = True
|
||||
|
||||
self._reader, self._writer = stdio()
|
||||
|
||||
encoder = JSONEncoder()
|
||||
self._server = Server(self._reader, self._writer, encoder)
|
||||
self._notification_client = NotificationClient(self._writer, encoder)
|
||||
|
||||
def eof_handler():
|
||||
self._active = False
|
||||
self._server.register_eof(eof_handler)
|
||||
|
||||
# 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)
|
||||
self._register_method("pass_login_credentials", self.pass_login_credentials)
|
||||
self._register_method(
|
||||
"import_owned_games",
|
||||
self.get_owned_games,
|
||||
result_name="owned_games",
|
||||
feature=Feature.ImportOwnedGames
|
||||
)
|
||||
self._register_method(
|
||||
"import_unlocked_achievements",
|
||||
self.get_unlocked_achievements,
|
||||
result_name="unlocked_achievements",
|
||||
feature=Feature.ImportAchievements
|
||||
)
|
||||
self._register_method(
|
||||
"import_local_games",
|
||||
self.get_local_games,
|
||||
result_name="local_games",
|
||||
feature=Feature.ImportInstalledGames
|
||||
)
|
||||
self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame)
|
||||
self._register_notification("install_game", self.install_game, feature=Feature.InstallGame)
|
||||
self._register_notification(
|
||||
"uninstall_game",
|
||||
self.uninstall_game,
|
||||
feature=Feature.UninstallGame
|
||||
)
|
||||
self._register_method(
|
||||
"import_friends",
|
||||
self.get_friends,
|
||||
result_name="user_info_list",
|
||||
feature=Feature.ImportUsers
|
||||
)
|
||||
self._register_method(
|
||||
"import_user_infos",
|
||||
self.get_users,
|
||||
result_name="user_info_list",
|
||||
feature=Feature.ImportUsers
|
||||
)
|
||||
self._register_method(
|
||||
"send_message",
|
||||
self.send_message,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"mark_as_read",
|
||||
self.mark_as_read,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_rooms",
|
||||
self.get_rooms,
|
||||
result_name="rooms",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_message",
|
||||
self.get_room_history_from_message,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_timestamp",
|
||||
self.get_room_history_from_timestamp,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
|
||||
self._register_method(
|
||||
"import_game_times",
|
||||
self.get_game_times,
|
||||
result_name="game_times",
|
||||
feature=Feature.ImportGameTime
|
||||
)
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
features = []
|
||||
if self.__class__ != Plugin:
|
||||
for feature, handlers in self._feature_methods.items():
|
||||
if self._implements(handlers):
|
||||
features.append(feature)
|
||||
|
||||
return features
|
||||
|
||||
def _implements(self, handlers):
|
||||
for handler in handlers:
|
||||
if handler.__name__ not in self.__class__.__dict__:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _register_method(self, name, handler, result_name=None, internal=False, feature=None):
|
||||
if internal:
|
||||
def method(params):
|
||||
result = handler(**params)
|
||||
if result_name:
|
||||
result = {
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
self._server.register_method(name, method, True)
|
||||
else:
|
||||
async def method(params):
|
||||
result = await handler(**params)
|
||||
if result_name:
|
||||
result = {
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
self._server.register_method(name, method, False)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
def _register_notification(self, name, handler, internal=False, feature=None):
|
||||
self._server.register_notification(name, handler, internal)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
async def run(self):
|
||||
"""Plugin main coorutine"""
|
||||
async def pass_control():
|
||||
while self._active:
|
||||
logging.debug("Passing control to plugin")
|
||||
self.tick()
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await asyncio.gather(pass_control(), self._server.run())
|
||||
|
||||
def _shutdown(self):
|
||||
logging.info("Shuting down")
|
||||
self._server.stop()
|
||||
self._active = False
|
||||
self.shutdown()
|
||||
|
||||
def _get_capabilities(self):
|
||||
return {
|
||||
"platform_name": self._platform,
|
||||
"features": self.features
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _ping():
|
||||
pass
|
||||
|
||||
# notifications
|
||||
def store_credentials(self, credentials):
|
||||
"""Notify client to store plugin credentials.
|
||||
They will be pass to next authencicate calls.
|
||||
"""
|
||||
self._notification_client.notify("store_credentials", credentials)
|
||||
|
||||
def add_game(self, game):
|
||||
params = {"owned_game" : game}
|
||||
self._notification_client.notify("owned_game_added", params)
|
||||
|
||||
def remove_game(self, game_id):
|
||||
params = {"game_id" : game_id}
|
||||
self._notification_client.notify("owned_game_removed", params)
|
||||
|
||||
def update_game(self, game):
|
||||
params = {"owned_game" : game}
|
||||
self._notification_client.notify("owned_game_updated", params)
|
||||
|
||||
def unlock_achievement(self, achievement):
|
||||
self._notification_client.notify("achievement_unlocked", achievement)
|
||||
|
||||
def update_local_game_status(self, local_game):
|
||||
params = {"local_game" : local_game}
|
||||
self._notification_client.notify("local_game_status_changed", params)
|
||||
|
||||
def add_friend(self, user):
|
||||
params = {"user_info" : user}
|
||||
self._notification_client.notify("friend_added", params)
|
||||
|
||||
def remove_friend(self, user_id):
|
||||
params = {"user_id" : user_id}
|
||||
self._notification_client.notify("friend_removed", params)
|
||||
|
||||
def update_friend(self, user):
|
||||
params = {"user_info" : user}
|
||||
self._notification_client.notify("friend_updated", params)
|
||||
|
||||
def update_room(self, room_id, unread_message_count=None, new_messages=None):
|
||||
params = {"room_id": room_id}
|
||||
if unread_message_count is not None:
|
||||
params["unread_message_count"] = unread_message_count
|
||||
if new_messages is not None:
|
||||
params["messages"] = new_messages
|
||||
self._notification_client.notify("chat_room_updated", params)
|
||||
|
||||
def update_game_time(self, game_time):
|
||||
params = {"game_time" : game_time}
|
||||
self._notification_client.notify("game_time_updated", params)
|
||||
|
||||
# handlers
|
||||
def tick(self):
|
||||
"""This method is called periodicaly.
|
||||
Override it to implement periodical tasks like refreshing cache.
|
||||
This method should not be blocking - any longer actions should be
|
||||
handled by asycio tasks.
|
||||
"""
|
||||
|
||||
def shutdown(self):
|
||||
"""This method is called on plugin shutdown.
|
||||
Override it to implement tear down.
|
||||
"""
|
||||
|
||||
# methods
|
||||
async def authenticate(self, stored_credentials=None):
|
||||
"""Overide this method to handle plugin authentication.
|
||||
The method should return one of:
|
||||
- galaxy.api.types.AuthenticationSuccess - on successful authencication
|
||||
- galaxy.api.types.NextStep - when more authentication steps are required
|
||||
Or raise galaxy.api.types.LoginError on authentication failure.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def pass_login_credentials(self, step, credentials):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_owned_games(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_unlocked_achievements(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_local_games(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def launch_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def install_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def uninstall_game(self, game_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_friends(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_users(self, user_id_list):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def send_message(self, room_id, message):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def mark_as_read(self, room_id, last_message_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_rooms(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_room_history_from_message(self, room_id, message_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_room_history_from_timestamp(self, room_id, from_timestamp):
|
||||
raise NotImplementedError()
|
||||
|
||||
async def get_game_times(self):
|
||||
raise NotImplementedError()
|
||||
35
galaxy/api/stream.py
Normal file
35
galaxy/api/stream.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
class StdinReader():
|
||||
def __init__(self):
|
||||
self._stdin = sys.stdin.buffer
|
||||
|
||||
async def readline(self):
|
||||
# a single call to sys.stdin.readline() is thread-safe
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, self._stdin.readline)
|
||||
|
||||
class StdoutWriter():
|
||||
def __init__(self):
|
||||
self._buffer = []
|
||||
self._stdout = sys.stdout.buffer
|
||||
|
||||
def write(self, data):
|
||||
self._buffer.append(data)
|
||||
|
||||
async def drain(self):
|
||||
data, self._buffer = self._buffer, []
|
||||
# a single call to sys.stdout.writelines() is thread-safe
|
||||
def write(data):
|
||||
sys.stdout.writelines(data)
|
||||
sys.stdout.flush()
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
return await loop.run_in_executor(None, write, data)
|
||||
|
||||
def stdio():
|
||||
# no support for asyncio stdio yet on Windows, see https://bugs.python.org/issue26832
|
||||
# use an executor to read from stdio and write to stdout
|
||||
# note: if nothing ever drains the writer explicitly, no flushing ever takes place!
|
||||
return StdinReader(), StdoutWriter()
|
||||
154
galaxy/api/types.py
Normal file
154
galaxy/api/types.py
Normal file
@@ -0,0 +1,154 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from galaxy.api.jsonrpc import ApplicationError
|
||||
from galaxy.api.consts import LocalGameState, PresenceState
|
||||
|
||||
@dataclass
|
||||
class AuthenticationSuccess():
|
||||
user_id: str
|
||||
user_name: str
|
||||
|
||||
@dataclass
|
||||
class NextStep():
|
||||
next_step: str
|
||||
auth_params: dict
|
||||
|
||||
class LoginError(ApplicationError):
|
||||
def __init__(self, current_step, reason):
|
||||
data = {
|
||||
"current_step": current_step,
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class LicenseInfo():
|
||||
license_type: str
|
||||
owner: str = None
|
||||
|
||||
@dataclass
|
||||
class Dlc():
|
||||
dlc_id: str
|
||||
dlc_title: str
|
||||
license_info: LicenseInfo
|
||||
|
||||
@dataclass
|
||||
class Game():
|
||||
game_id: str
|
||||
game_title: str
|
||||
dlcs: List[Dlc]
|
||||
license_info: LicenseInfo
|
||||
|
||||
class GetGamesError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class Achievement():
|
||||
achievement_id: str
|
||||
unlock_time: int
|
||||
|
||||
class GetAchievementsError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class LocalGame():
|
||||
game_id: str
|
||||
local_game_state: LocalGameState
|
||||
|
||||
class GetLocalGamesError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class Presence():
|
||||
presence_state: PresenceState
|
||||
game_id: str = None
|
||||
presence_status: str = None
|
||||
|
||||
@dataclass
|
||||
class UserInfo():
|
||||
user_id: str
|
||||
is_friend: bool
|
||||
user_name: str
|
||||
avatar_url: str
|
||||
presence: Presence
|
||||
|
||||
class GetFriendsError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
class GetUsersError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
class SendMessageError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
class MarkAsReadError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class Room():
|
||||
room_id: str
|
||||
unread_message_count: int
|
||||
last_message_id: str
|
||||
|
||||
class GetRoomsError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class Message():
|
||||
message_id: str
|
||||
sender_id: str
|
||||
sent_time: int
|
||||
message_text: str
|
||||
|
||||
class GetRoomHistoryError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
|
||||
@dataclass
|
||||
class GameTime():
|
||||
game_id: str
|
||||
time_played: int
|
||||
last_played_time: int
|
||||
|
||||
class GetGameTimeError(ApplicationError):
|
||||
def __init__(self, reason):
|
||||
data = {
|
||||
"reason": reason
|
||||
}
|
||||
super().__init__(data)
|
||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
pytest==4.2.0
|
||||
10
setup.py
Normal file
10
setup.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="galaxy.python.api",
|
||||
version="0.1",
|
||||
description="Galaxy python plugin API",
|
||||
author='Galaxy team',
|
||||
author_email='galaxy@gog.com',
|
||||
packages=find_packages()
|
||||
)
|
||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
6
tests/async_mock.py
Normal file
6
tests/async_mock.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
class AsyncMock(MagicMock):
|
||||
async def __call__(self, *args, **kwargs):
|
||||
# pylint: disable=useless-super-delegation
|
||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||
58
tests/conftest.py
Normal file
58
tests/conftest.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from contextlib import ExitStack
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.stream import StdinReader, StdoutWriter
|
||||
from galaxy.api.consts import Platform
|
||||
from tests.async_mock import AsyncMock
|
||||
|
||||
@pytest.fixture()
|
||||
def plugin():
|
||||
"""Return plugin instance with all feature methods mocked"""
|
||||
async_methods = (
|
||||
"authenticate",
|
||||
"pass_login_credentials",
|
||||
"get_owned_games",
|
||||
"get_unlocked_achievements",
|
||||
"get_local_games",
|
||||
"launch_game",
|
||||
"install_game",
|
||||
"uninstall_game",
|
||||
"get_friends",
|
||||
"get_users",
|
||||
"send_message",
|
||||
"mark_as_read",
|
||||
"get_rooms",
|
||||
"get_room_history_from_message",
|
||||
"get_room_history_from_timestamp",
|
||||
"get_game_times"
|
||||
)
|
||||
|
||||
methods = (
|
||||
"shutdown",
|
||||
"tick"
|
||||
)
|
||||
|
||||
with ExitStack() as stack:
|
||||
for method in async_methods:
|
||||
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
||||
for method in methods:
|
||||
stack.enter_context(patch.object(Plugin, method))
|
||||
yield Plugin(Platform.Generic)
|
||||
|
||||
@pytest.fixture()
|
||||
def readline():
|
||||
with patch.object(StdinReader, "readline", new_callable=AsyncMock) as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture()
|
||||
def write():
|
||||
with patch.object(StdoutWriter, "write") as mock:
|
||||
yield mock
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def my_caplog(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
85
tests/test_achievements.py
Normal file
85
tests/test_achievements.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import Achievement, GetAchievementsError
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_unlocked_achievements",
|
||||
"params": {
|
||||
"game_id": "14"
|
||||
}
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_unlocked_achievements.return_value = [
|
||||
Achievement("lvl10", 1548421241),
|
||||
Achievement("lvl20", 1548422395)
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"unlocked_achievements": [
|
||||
{
|
||||
"achievement_id": "lvl10",
|
||||
"unlock_time": 1548421241
|
||||
},
|
||||
{
|
||||
"achievement_id": "lvl20",
|
||||
"unlock_time": 1548422395
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_unlocked_achievements",
|
||||
"params": {
|
||||
"game_id": "14"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_unlocked_achievements.side_effect = GetAchievementsError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_unlocked_achievements.assert_called()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_unlock_achievement(plugin, write):
|
||||
achievement = Achievement("lvl20", 1548422395)
|
||||
|
||||
async def couritine():
|
||||
plugin.unlock_achievement(achievement)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "achievement_unlocked",
|
||||
"params": {
|
||||
"achievement_id": "lvl20",
|
||||
"unlock_time": 1548422395
|
||||
}
|
||||
}
|
||||
86
tests/test_authenticate.py
Normal file
86
tests/test_authenticate.py
Normal file
@@ -0,0 +1,86 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import AuthenticationSuccess, LoginError
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "init_authentication"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.authenticate.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"user_id": "132",
|
||||
"user_name": "Zenek"
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "init_authentication"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.authenticate.side_effect = LoginError("step", "reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.authenticate.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"current_step": "step",
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_stored_credentials(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "init_authentication",
|
||||
"params": {
|
||||
"stored_credentials": {
|
||||
"token": "ABC"
|
||||
}
|
||||
}
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.authenticate.return_value = AuthenticationSuccess("132", "Zenek")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||
write.assert_called()
|
||||
|
||||
def test_store_credentials(plugin, write):
|
||||
credentials = {
|
||||
"token": "ABC"
|
||||
}
|
||||
|
||||
async def couritine():
|
||||
plugin.store_credentials(credentials)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "store_credentials",
|
||||
"params": credentials
|
||||
}
|
||||
336
tests/test_chat.py
Normal file
336
tests/test_chat.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import (
|
||||
SendMessageError, MarkAsReadError, Room, GetRoomsError, Message, GetRoomHistoryError
|
||||
)
|
||||
|
||||
def test_send_message_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"message": "Hello!"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.send_message.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
}
|
||||
|
||||
def test_send_message_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "15",
|
||||
"message": "Bye"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.send_message.side_effect = SendMessageError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_mark_as_read_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"last_message_id": "67"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.mark_as_read.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
|
||||
def test_mark_as_read_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "18",
|
||||
"last_message_id": "7"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.mark_as_read.side_effect = MarkAsReadError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_rooms.return_value = [
|
||||
Room("13", 0, None),
|
||||
Room("15", 34, "8")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"rooms": [
|
||||
{
|
||||
"room_id": "13",
|
||||
"unread_message_count": 0,
|
||||
},
|
||||
{
|
||||
"room_id": "15",
|
||||
"unread_message_count": 34,
|
||||
"last_message_id": "8"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_rooms.side_effect = GetRoomsError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_message_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "34",
|
||||
"message_id": "66"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_message.return_value = [
|
||||
Message("13", "149", 1549454837, "Hello"),
|
||||
Message("14", "812", 1549454899, "Hi")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "13",
|
||||
"sender_id": "149",
|
||||
"sent_time": 1549454837,
|
||||
"message_text": "Hello"
|
||||
},
|
||||
{
|
||||
"message_id": "14",
|
||||
"sender_id": "812",
|
||||
"sent_time": 1549454899,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_message_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "33",
|
||||
"message_id": "88"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_message.side_effect = GetRoomHistoryError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "12",
|
||||
"from_timestamp": 1549454835
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_timestamp.return_value = [
|
||||
Message("12", "155", 1549454836, "Bye")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="12",
|
||||
from_timestamp=1549454835
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "12",
|
||||
"sender_id": "155",
|
||||
"sent_time": 1549454836,
|
||||
"message_text": "Bye"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "10",
|
||||
"from_timestamp": 1549454800
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_room_history_from_timestamp.side_effect = GetRoomHistoryError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="10",
|
||||
from_timestamp=1549454800
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_room(plugin, write):
|
||||
messages = [
|
||||
Message("10", "898", 1549454832, "Hi")
|
||||
]
|
||||
|
||||
async def couritine():
|
||||
plugin.update_room("14", 15, messages)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "chat_room_updated",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"unread_message_count": 15,
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "10",
|
||||
"sender_id": "898",
|
||||
"sent_time": 1549454832,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
45
tests/test_features.py
Normal file
45
tests/test_features.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform, Feature
|
||||
|
||||
def test_base_class():
|
||||
plugin = Plugin(Platform.Generic)
|
||||
assert plugin.features == []
|
||||
|
||||
def test_no_overloads():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic)
|
||||
assert plugin.features == []
|
||||
|
||||
def test_one_method_feature():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic)
|
||||
assert plugin.features == [Feature.ImportOwnedGames]
|
||||
|
||||
def test_multiple_methods_feature_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
pass
|
||||
async def mark_as_read(self, room_id, last_message_id):
|
||||
pass
|
||||
async def get_rooms(self):
|
||||
pass
|
||||
async def get_room_history_from_message(self, room_id, message_id):
|
||||
pass
|
||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic)
|
||||
assert plugin.features == [Feature.Chat]
|
||||
|
||||
def test_multiple_methods_feature_not_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic)
|
||||
assert plugin.features == []
|
||||
85
tests/test_game_times.py
Normal file
85
tests/test_game_times.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import GameTime, GetGameTimeError
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_game_times"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_game_times.return_value = [
|
||||
GameTime("3", 60, 1549550504),
|
||||
GameTime("5", 10, 1549550502)
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_game_times.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"game_times": [
|
||||
{
|
||||
"game_id": "3",
|
||||
"time_played": 60,
|
||||
"last_played_time": 1549550504
|
||||
},
|
||||
{
|
||||
"game_id": "5",
|
||||
"time_played": 10,
|
||||
"last_played_time": 1549550502
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_game_times"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_game_times.side_effect = GetGameTimeError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_game_times.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_game(plugin, write):
|
||||
game_time = GameTime("3", 60, 1549550504)
|
||||
|
||||
async def couritine():
|
||||
plugin.update_game_time(game_time)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "game_time_updated",
|
||||
"params": {
|
||||
"game_time": {
|
||||
"game_id": "3",
|
||||
"time_played": 60,
|
||||
"last_played_time": 1549550504
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/test_install_game.py
Normal file
16
tests/test_install_game.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
def test_success(plugin, readline):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "install_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.install_game.assert_called_with(game_id="3")
|
||||
66
tests/test_internal.py
Normal file
66
tests/test_internal.py
Normal file
@@ -0,0 +1,66 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform
|
||||
|
||||
def test_get_capabilites(readline, write):
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "get_capabilities"
|
||||
}
|
||||
plugin = PluginImpl(Platform.Generic)
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
asyncio.run(plugin.run())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"platform_name": "generic",
|
||||
"features": [
|
||||
"ImportOwnedGames"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_shutdown(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "5",
|
||||
"method": "shutdown"
|
||||
}
|
||||
readline.side_effect = [json.dumps(request)]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.shutdown.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "5",
|
||||
"result": None
|
||||
}
|
||||
|
||||
def test_ping(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "ping"
|
||||
}
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
asyncio.run(plugin.run())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
|
||||
def test_tick(plugin, readline):
|
||||
readline.side_effect = [""]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.tick.assert_called_with()
|
||||
16
tests/test_launch_game.py
Normal file
16
tests/test_launch_game.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
def test_success(plugin, readline):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "launch_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.launch_game.assert_called_with(game_id="3")
|
||||
84
tests/test_local_games.py
Normal file
84
tests/test_local_games.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import GetLocalGamesError, LocalGame
|
||||
from galaxy.api.consts import LocalGameState
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_local_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
|
||||
plugin.get_local_games.return_value = [
|
||||
LocalGame("1", "Running"),
|
||||
LocalGame("2", "Installed")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_local_games.assert_called_with()
|
||||
|
||||
response = json.loads(write.call_args[0][0])
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"local_games" : [
|
||||
{
|
||||
"game_id": "1",
|
||||
"local_game_state": "Running"
|
||||
},
|
||||
{
|
||||
"game_id": "2",
|
||||
"local_game_state": "Installed"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_local_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_local_games.side_effect = GetLocalGamesError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_local_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_local_game_state_update(plugin, write):
|
||||
game = LocalGame("1", LocalGameState.Running)
|
||||
|
||||
async def couritine():
|
||||
plugin.update_local_game_status(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "local_game_status_changed",
|
||||
"params": {
|
||||
"local_game": {
|
||||
"game_id": "1",
|
||||
"local_game_state": "Running"
|
||||
}
|
||||
}
|
||||
}
|
||||
152
tests/test_owned_games.py
Normal file
152
tests/test_owned_games.py
Normal file
@@ -0,0 +1,152 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import Game, Dlc, LicenseInfo, GetGamesError
|
||||
|
||||
def test_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_owned_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = [
|
||||
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)),
|
||||
Game(
|
||||
"5",
|
||||
"Witcher 3",
|
||||
[
|
||||
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)),
|
||||
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)),
|
||||
],
|
||||
LicenseInfo("SinglePurchase", None))
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_owned_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"owned_games": [
|
||||
{
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"game_id": "5",
|
||||
"game_title": "Witcher 3",
|
||||
"dlcs": [
|
||||
{
|
||||
"dlc_id": "7",
|
||||
"dlc_title": "Hearts of Stone",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
},
|
||||
{
|
||||
"dlc_id": "8",
|
||||
"dlc_title": "Temerian Armor Set",
|
||||
"license_info": {
|
||||
"license_type": "FreeToPlay"
|
||||
}
|
||||
}
|
||||
],
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_owned_games"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.side_effect = GetGamesError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_owned_games.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_add_game(plugin, write):
|
||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
||||
|
||||
async def couritine():
|
||||
plugin.add_game(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_added",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_remove_game(plugin, write):
|
||||
async def couritine():
|
||||
plugin.remove_game("5")
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_removed",
|
||||
"params": {
|
||||
"game_id": "5"
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_game(plugin, write):
|
||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
||||
|
||||
async def couritine():
|
||||
plugin.update_game(game)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "owned_game_updated",
|
||||
"params": {
|
||||
"owned_game": {
|
||||
"game_id": "3",
|
||||
"game_title": "Doom",
|
||||
"license_info": {
|
||||
"license_type": "SinglePurchase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
tests/test_uninstall_game.py
Normal file
16
tests/test_uninstall_game.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
def test_success(plugin, readline):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "uninstall_game",
|
||||
"params": {
|
||||
"game_id": "3"
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_owned_games.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||
220
tests/test_users.py
Normal file
220
tests/test_users.py
Normal file
@@ -0,0 +1,220 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import UserInfo, Presence, GetFriendsError, GetUsersError
|
||||
from galaxy.api.consts import PresenceState
|
||||
|
||||
def test_get_friends_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_friends.return_value = [
|
||||
UserInfo(
|
||||
"3",
|
||||
True,
|
||||
"Jan",
|
||||
"http://avatar1.png",
|
||||
Presence(
|
||||
PresenceState.Online,
|
||||
"123",
|
||||
"Main menu"
|
||||
)
|
||||
),
|
||||
UserInfo(
|
||||
"5",
|
||||
True,
|
||||
"Ola",
|
||||
"http://avatar2.png",
|
||||
Presence(PresenceState.Offline)
|
||||
)
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_friends.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"user_info_list": [
|
||||
{
|
||||
"user_id": "3",
|
||||
"is_friend": True,
|
||||
"user_name": "Jan",
|
||||
"avatar_url": "http://avatar1.png",
|
||||
"presence": {
|
||||
"presence_state": "online",
|
||||
"game_id": "123",
|
||||
"presence_status": "Main menu"
|
||||
}
|
||||
},
|
||||
{
|
||||
"user_id": "5",
|
||||
"is_friend": True,
|
||||
"user_name": "Ola",
|
||||
"avatar_url": "http://avatar2.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_friends_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_friends"
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_friends.side_effect = GetFriendsError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_friends.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_add_friend(plugin, write):
|
||||
friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
|
||||
async def couritine():
|
||||
plugin.add_friend(friend)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_added",
|
||||
"params": {
|
||||
"user_info": {
|
||||
"user_id": "7",
|
||||
"is_friend": True,
|
||||
"user_name": "Kuba",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_remove_friend(plugin, write):
|
||||
async def couritine():
|
||||
plugin.remove_friend("5")
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_removed",
|
||||
"params": {
|
||||
"user_id": "5"
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_friend(plugin, write):
|
||||
friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
|
||||
async def couritine():
|
||||
plugin.update_friend(friend)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "friend_updated",
|
||||
"params": {
|
||||
"user_info": {
|
||||
"user_id": "9",
|
||||
"is_friend": True,
|
||||
"user_name": "Anna",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_users_success(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["13"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_users.return_value = [
|
||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["13"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"result": {
|
||||
"user_info_list": [
|
||||
{
|
||||
"user_id": "5",
|
||||
"is_friend": False,
|
||||
"user_name": "Ula",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_users_failure(plugin, readline, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["10", "11", "12"]
|
||||
}
|
||||
}
|
||||
|
||||
readline.side_effect = [json.dumps(request), ""]
|
||||
plugin.get_users.side_effect = GetUsersError("reason")
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"error": {
|
||||
"code": -32003,
|
||||
"message": "Custom error",
|
||||
"data": {
|
||||
"reason": "reason"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user