mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-04 12:58:24 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d73d048ff7 | ||
|
|
e06e40f845 | ||
|
|
833e6999d7 | ||
|
|
ca778e2cdb | ||
|
|
9a06428fc0 | ||
|
|
f9eaeaf726 | ||
|
|
f09171672f | ||
|
|
ca8d0dfaf4 | ||
|
|
73bc9aa8ec |
@@ -34,5 +34,10 @@ pytest
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 0.16
|
||||||
|
* Do not log sensitive data.
|
||||||
|
* Return `LocalGameState` as int (possible combination of flags).
|
||||||
|
### 0.15
|
||||||
|
* `shutdown()` is called on socket disconnection.
|
||||||
### 0.14
|
### 0.14
|
||||||
* Added required version parameter to Plugin constructor.
|
* Added required version parameter to Plugin constructor.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum, Flag
|
||||||
|
|
||||||
class Platform(Enum):
|
class Platform(Enum):
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
@@ -30,10 +30,10 @@ class LicenseType(Enum):
|
|||||||
FreeToPlay = "FreeToPlay"
|
FreeToPlay = "FreeToPlay"
|
||||||
OtherUserLicense = "OtherUserLicense"
|
OtherUserLicense = "OtherUserLicense"
|
||||||
|
|
||||||
class LocalGameState(Enum):
|
class LocalGameState(Flag):
|
||||||
None_ = "None"
|
None_ = 0
|
||||||
Installed = "Installed"
|
Installed = 1
|
||||||
Running = "Running"
|
Running = 2
|
||||||
|
|
||||||
class PresenceState(Enum):
|
class PresenceState(Enum):
|
||||||
Unknown = "Unknown"
|
Unknown = "Unknown"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
class UnknownError(ApplicationError):
|
UnknownError = UnknownError
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(0, "Unknown error", data)
|
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from collections.abc import Iterable
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class MethodNotFound(JsonRpcError):
|
|||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
class InvalidParams(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(-32601, "Invalid params")
|
super().__init__(-32602, "Invalid params")
|
||||||
|
|
||||||
class Timeout(JsonRpcError):
|
class Timeout(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -40,8 +41,26 @@ class ApplicationError(JsonRpcError):
|
|||||||
raise ValueError("The error code in reserved range")
|
raise ValueError("The error code in reserved range")
|
||||||
super().__init__(code, message, data)
|
super().__init__(code, message, data)
|
||||||
|
|
||||||
|
class UnknownError(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(0, "Unknown error", data)
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
Method = namedtuple("Method", ["callback", "internal"])
|
Method = namedtuple("Method", ["callback", "internal", "sensitive_params"])
|
||||||
|
|
||||||
|
def anonymise_sensitive_params(params, sensitive_params):
|
||||||
|
anomized_data = "****"
|
||||||
|
if not sensitive_params:
|
||||||
|
return params
|
||||||
|
|
||||||
|
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 anomized_data
|
||||||
|
|
||||||
class Server():
|
class Server():
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
@@ -53,11 +72,27 @@ class Server():
|
|||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._eof_listeners = []
|
self._eof_listeners = []
|
||||||
|
|
||||||
def register_method(self, name, callback, internal):
|
def register_method(self, name, callback, internal, sensitive_params=False):
|
||||||
self._methods[name] = Method(callback, internal)
|
"""
|
||||||
|
Register method
|
||||||
|
:param name:
|
||||||
|
:param callback:
|
||||||
|
:param internal: if True the callback will be processed immediately (synchronously)
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
self._methods[name] = Method(callback, internal, sensitive_params)
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal):
|
def register_notification(self, name, callback, internal, sensitive_params=False):
|
||||||
self._notifications[name] = Method(callback, internal)
|
"""
|
||||||
|
Register notification
|
||||||
|
:param name:
|
||||||
|
:param callback:
|
||||||
|
:param internal: if True the callback will be processed immediately (synchronously)
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
self._notifications[name] = Method(callback, internal, sensitive_params)
|
||||||
|
|
||||||
def register_eof(self, callback):
|
def register_eof(self, callback):
|
||||||
self._eof_listeners.append(callback)
|
self._eof_listeners.append(callback)
|
||||||
@@ -73,7 +108,7 @@ class Server():
|
|||||||
self._eof()
|
self._eof()
|
||||||
continue
|
continue
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received data: %s", data)
|
logging.debug("Received %d bytes of data", len(data))
|
||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -92,43 +127,39 @@ class Server():
|
|||||||
self._send_error(None, error)
|
self._send_error(None, error)
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.debug("Parsed input: %s", request)
|
|
||||||
|
|
||||||
if request.id is not None:
|
if request.id is not None:
|
||||||
self._handle_request(request)
|
self._handle_request(request)
|
||||||
else:
|
else:
|
||||||
self._handle_notification(request)
|
self._handle_notification(request)
|
||||||
|
|
||||||
def _handle_notification(self, request):
|
def _handle_notification(self, request):
|
||||||
logging.debug("Handling notification %s", request)
|
|
||||||
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 unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, internal = method
|
callback, internal, sensitive_params = method
|
||||||
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
if internal:
|
if internal:
|
||||||
# internal requests are handled immediately
|
# internal requests are handled immediately
|
||||||
callback(**request.params)
|
callback(**request.params)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(callback(**request.params))
|
asyncio.create_task(callback(**request.params))
|
||||||
except Exception as error: #pylint: disable=broad-except
|
except Exception:
|
||||||
logging.error(
|
logging.exception("Unexpected exception raised in notification handler")
|
||||||
"Unexpected exception raised in notification handler: %s",
|
|
||||||
repr(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_request(self, request):
|
def _handle_request(self, request):
|
||||||
logging.debug("Handling request %s", request)
|
|
||||||
method = self._methods.get(request.method)
|
method = self._methods.get(request.method)
|
||||||
|
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received uknown request: %s", request.method)
|
logging.error("Received unknown request: %s", request.method)
|
||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, internal = method
|
callback, internal, sensitive_params = method
|
||||||
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
if internal:
|
if internal:
|
||||||
# internal requests are handled immediately
|
# internal requests are handled immediately
|
||||||
response = callback(request.params)
|
response = callback(request.params)
|
||||||
@@ -144,8 +175,9 @@ 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: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.exception("Unexpected exception raised in plugin handler")
|
logging.exception("Unexpected exception raised in plugin handler")
|
||||||
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
asyncio.create_task(handle())
|
||||||
|
|
||||||
@@ -166,7 +198,8 @@ class Server():
|
|||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
logging.debug("Sending data: %s", line)
|
||||||
self._writer.write((line + "\n").encode("utf-8"))
|
data = (line + "\n").encode("utf-8")
|
||||||
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
asyncio.create_task(self._writer.drain())
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logging.error(str(error))
|
||||||
@@ -194,25 +227,47 @@ class Server():
|
|||||||
|
|
||||||
self._send(response)
|
self._send(response)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_request(request, sensitive_params):
|
||||||
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
|
if request.id is not None:
|
||||||
|
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
|
else:
|
||||||
|
logging.info("Handling notification: method=%s, params=%s", request.method, params)
|
||||||
|
|
||||||
class NotificationClient():
|
class NotificationClient():
|
||||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
def __init__(self, writer, encoder=json.JSONEncoder()):
|
||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
|
|
||||||
def notify(self, method, params):
|
def notify(self, method, params, sensitive_params=False):
|
||||||
|
"""
|
||||||
|
Send notification
|
||||||
|
:param method:
|
||||||
|
:param params:
|
||||||
|
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
|
||||||
|
are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
notification = {
|
notification = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
|
self._log(method, params, sensitive_params)
|
||||||
self._send(notification)
|
self._send(notification)
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write((line + "\n").encode("utf-8"))
|
logging.debug("Sending %d byte of data", len(data))
|
||||||
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
asyncio.create_task(self._writer.drain())
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
logging.error("Failed to parse outgoing message: %s", str(error))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log(method, params, sensitive_params):
|
||||||
|
params = anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
logging.info("Sending notification: method=%s, params=%s", method, params)
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class Plugin():
|
|||||||
self._notification_client = NotificationClient(self._writer, encoder)
|
self._notification_client = NotificationClient(self._writer, encoder)
|
||||||
|
|
||||||
def eof_handler():
|
def eof_handler():
|
||||||
self._active = False
|
self._shutdown()
|
||||||
self._server.register_eof(eof_handler)
|
self._server.register_eof(eof_handler)
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
@@ -48,7 +48,7 @@ class Plugin():
|
|||||||
self._register_method("ping", self._ping, internal=True)
|
self._register_method("ping", self._ping, internal=True)
|
||||||
|
|
||||||
# implemented by developer
|
# implemented by developer
|
||||||
self._register_method("init_authentication", self.authenticate)
|
self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"])
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_owned_games",
|
"import_owned_games",
|
||||||
self.get_owned_games,
|
self.get_owned_games,
|
||||||
@@ -138,7 +138,7 @@ class Plugin():
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _register_method(self, name, handler, result_name=None, internal=False, feature=None):
|
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
|
||||||
if internal:
|
if internal:
|
||||||
def method(params):
|
def method(params):
|
||||||
result = handler(**params)
|
result = handler(**params)
|
||||||
@@ -147,7 +147,7 @@ class Plugin():
|
|||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, True)
|
self._server.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(params):
|
async def method(params):
|
||||||
result = await handler(**params)
|
result = await handler(**params)
|
||||||
@@ -156,13 +156,13 @@ class Plugin():
|
|||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, False)
|
self._server.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, feature=None):
|
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
|
||||||
self._server.register_notification(name, handler, internal)
|
self._server.register_notification(name, handler, internal, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
@@ -171,7 +171,6 @@ class Plugin():
|
|||||||
"""Plugin main coorutine"""
|
"""Plugin main coorutine"""
|
||||||
async def pass_control():
|
async def pass_control():
|
||||||
while self._active:
|
while self._active:
|
||||||
logging.debug("Passing control to plugin")
|
|
||||||
try:
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -202,7 +201,7 @@ class Plugin():
|
|||||||
"""Notify client to store plugin credentials.
|
"""Notify client to store plugin credentials.
|
||||||
They will be pass to next authencicate calls.
|
They will be pass to next authencicate calls.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify("store_credentials", credentials)
|
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
||||||
|
|
||||||
def add_game(self, game):
|
def add_game(self, game):
|
||||||
params = {"owned_game" : game}
|
params = {"owned_game" : game}
|
||||||
|
|||||||
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.14",
|
version="0.17",
|
||||||
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',
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ def test_success(plugin, readline, write):
|
|||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
|
||||||
plugin.get_local_games.return_value = [
|
plugin.get_local_games.return_value = [
|
||||||
LocalGame("1", "Running"),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", "Installed")
|
LocalGame("2", LocalGameState.Installed),
|
||||||
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
@@ -31,11 +32,15 @@ def test_success(plugin, readline, write):
|
|||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": "Installed"
|
"local_game_state": LocalGameState.Installed.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": "3",
|
||||||
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -85,7 +90,7 @@ def test_local_game_state_update(plugin, write):
|
|||||||
"params": {
|
"params": {
|
||||||
"local_game": {
|
"local_game": {
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user