mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-20 04:38:24 -05:00
prepare interfaces for subscriptions
This commit is contained in:
@@ -115,6 +115,7 @@ class Feature(Enum):
|
||||
ImportOSCompatibility = "ImportOSCompatibility"
|
||||
ImportUserPresence = "ImportUserPresence"
|
||||
ImportLocalSize = "ImportLocalSize"
|
||||
ImportSubscriptions = "ImportSubscriptions"
|
||||
|
||||
|
||||
class LicenseType(Enum):
|
||||
|
||||
@@ -10,7 +10,8 @@ from galaxy.api.consts import Feature, OSCompatibility
|
||||
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||
from galaxy.api.jsonrpc import ApplicationError, Connection
|
||||
from galaxy.api.types import (
|
||||
Achievement, Authentication, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserInfo, UserPresence
|
||||
Achievement, Authentication, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserInfo, UserPresence,
|
||||
Subscription, SubscriptionGame
|
||||
)
|
||||
from galaxy.task_manager import TaskManager
|
||||
|
||||
@@ -176,6 +177,16 @@ class Plugin:
|
||||
self._local_size_import_finished,
|
||||
self.local_size_import_complete
|
||||
)
|
||||
self._subscription_games_importer = Importer(
|
||||
self._external_task_manager,
|
||||
"subscription games",
|
||||
self.get_subscription_games,
|
||||
self.prepare_subscription_games_context,
|
||||
self._subscription_games_import_success,
|
||||
self._subscription_games_import_failure,
|
||||
self._subscription_games_import_finished,
|
||||
self.subscription_games_import_complete
|
||||
)
|
||||
|
||||
# internal
|
||||
self._register_method("shutdown", self._shutdown, internal=True)
|
||||
@@ -246,6 +257,10 @@ class Plugin:
|
||||
self._register_method("start_local_size_import", self._start_local_size_import)
|
||||
self._detect_feature(Feature.ImportLocalSize, ["get_local_size"])
|
||||
|
||||
self._register_method("import_subscriptions", self.get_subscriptions, result_name="subscriptions")
|
||||
self._register_method("start_subscription_games_import", self._start_subscription_games_import)
|
||||
self._detect_feature(Feature.ImportSubscriptions, ["get_subscriptions", "get_subscription_games"])
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
@@ -647,6 +662,27 @@ class Plugin:
|
||||
def _local_size_import_finished(self) -> None:
|
||||
self._connection.send_notification("local_size_import_finished", None)
|
||||
|
||||
def _subscription_games_import_success(self, subscription_name: str, subscription_games: Optional[List[SubscriptionGame]]) -> None:
|
||||
self._connection.send_notification(
|
||||
"subscription_games_import_success",
|
||||
{
|
||||
"subscription_name": subscription_name,
|
||||
"subscription_games": subscription_games
|
||||
}
|
||||
)
|
||||
|
||||
def _subscription_games_import_failure(self, subscription_name: str, error: ApplicationError) -> None:
|
||||
self._connection.send_notification(
|
||||
"subscription_games_import_failure",
|
||||
{
|
||||
"subscription_name": subscription_name,
|
||||
"error": error.json()
|
||||
}
|
||||
)
|
||||
|
||||
def _subscription_games_import_finished(self) -> None:
|
||||
self._connection.send_notification("subscription_games_import_finished", None)
|
||||
|
||||
def lost_authentication(self) -> None:
|
||||
"""Notify the client that integration has lost authentication for the
|
||||
current user and is unable to perform actions which would require it.
|
||||
@@ -1051,6 +1087,54 @@ class Plugin:
|
||||
def local_size_import_complete(self) -> None:
|
||||
"""Override this method to handle operations after local game size import is finished (like updating cache)."""
|
||||
|
||||
async def get_subscriptions(self) -> List[Subscription]:
|
||||
"""Override this method to return a list of available
|
||||
Subscriptions available on platform.
|
||||
This method is called by the GOG Galaxy Client.
|
||||
|
||||
Both this method and get_subscription_games are required to be overridden
|
||||
for the Subscriptions feature to be recognized
|
||||
|
||||
Example of possible override of the method:
|
||||
|
||||
.. code-block:: python
|
||||
:linenos:
|
||||
|
||||
async def get_subscriptions(self, game_id):
|
||||
subs = []
|
||||
platform_subs_info = await self.retrieve_platform_subs_info()
|
||||
for sub_info in platform_subs_info:
|
||||
subs.append(Subscription(subscription_name=sub_info['name'], owned=sub_info['is_owned']))
|
||||
return subs
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
async def _start_subscription_games_import(self, subscription_names: List[str]) -> None:
|
||||
await self._subscription_games_importer.start(subscription_names)
|
||||
|
||||
async def prepare_subscription_games_context(self, subscription_names: List[str]) -> Any:
|
||||
"""Override this method to prepare context for :meth:`get_subscription_games`
|
||||
Default implementation returns None.
|
||||
|
||||
:param subscription_names: the names of the subscriptions for which subscriptions games are imported
|
||||
:return: context
|
||||
"""
|
||||
return None
|
||||
|
||||
async def get_subscription_games(self, subscription_name: str, context: Any) -> Optional[List[SubscriptionGame]]:
|
||||
"""Override this method to return list of subscription games in a given subscription.
|
||||
|
||||
:param context: the value returned from :meth:`prepare_subscription_games_context`
|
||||
:return: List of subscription games or `None` if list cannot be determined.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def subscription_games_import_complete(self) -> None:
|
||||
"""Override this method to handle operations after
|
||||
subscription games import is finished (like updating cache).
|
||||
"""
|
||||
|
||||
|
||||
def create_and_run_plugin(plugin_class, argv):
|
||||
"""Call this method as an entry point for the implemented integration.
|
||||
|
||||
@@ -216,3 +216,34 @@ class UserPresence:
|
||||
game_title: Optional[str] = None
|
||||
in_game_status: Optional[str] = None
|
||||
full_status: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class Subscription:
|
||||
"""Information about a subscription. If a subscription is not owned no end_time should be specified.
|
||||
|
||||
:param subscription_name: name of the subscription, will also be used as its identifier.
|
||||
:param owned: whether the subscription is owned or not, None if unknown.
|
||||
:param end_time: unix timestamp of when the subscription ends, None if unknown.
|
||||
"""
|
||||
subscription_name: str
|
||||
owned: Optional[bool] = None
|
||||
end_time: Optional[int] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.owned:
|
||||
assert self.end_time is None, "Subscriptions not owned but end time specified." \
|
||||
"Specify end time for owned subscriptions only"
|
||||
|
||||
@dataclass
|
||||
class SubscriptionGame:
|
||||
"""Information about a game from a subscription.
|
||||
|
||||
:param game_title: title of the game
|
||||
:param game_id: id of the game
|
||||
:param start_time: unix timestamp of when the game has been added to subscription
|
||||
:param end_time: unix timestamp of when the game is removed from subscription.
|
||||
"""
|
||||
game_title: str
|
||||
game_id: str
|
||||
start_time: Optional[int] = None
|
||||
end_time: Optional[int] = None
|
||||
|
||||
@@ -67,6 +67,10 @@ async def plugin(reader, writer):
|
||||
"get_local_size",
|
||||
"prepare_local_size_context",
|
||||
"local_size_import_complete",
|
||||
"get_subscriptions",
|
||||
"get_subscription_games",
|
||||
"prepare_subscription_games_context",
|
||||
"subscription_games_import_complete"
|
||||
)
|
||||
|
||||
with ExitStack() as stack:
|
||||
|
||||
@@ -18,7 +18,8 @@ def test_base_class():
|
||||
Feature.ImportGameLibrarySettings,
|
||||
Feature.ImportOSCompatibility,
|
||||
Feature.ImportUserPresence,
|
||||
Feature.ImportLocalSize
|
||||
Feature.ImportLocalSize,
|
||||
Feature.ImportSubscriptions
|
||||
}
|
||||
|
||||
|
||||
|
||||
271
tests/test_subscriptions.py
Normal file
271
tests/test_subscriptions.py
Normal file
@@ -0,0 +1,271 @@
|
||||
import pytest
|
||||
|
||||
from galaxy.api.types import Subscription, SubscriptionGame
|
||||
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
|
||||
from galaxy.unittest.mock import async_return_value
|
||||
|
||||
from tests import create_message, get_messages
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_subscriptions_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_subscriptions"
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
|
||||
plugin.get_subscriptions.return_value = async_return_value([
|
||||
Subscription("1"),
|
||||
Subscription("2", False),
|
||||
Subscription("3", True, 1580899100)
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.get_subscriptions.assert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": {
|
||||
"subscriptions": [
|
||||
{
|
||||
"subscription_name": "1"
|
||||
},
|
||||
{
|
||||
"subscription_name": "2",
|
||||
"owned": False
|
||||
},
|
||||
{
|
||||
"subscription_name": "3",
|
||||
"owned": True,
|
||||
"end_time": 1580899100
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"error,code,message",
|
||||
[
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
||||
],
|
||||
)
|
||||
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_subscriptions"
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_subscriptions.side_effect = error()
|
||||
await plugin.run()
|
||||
plugin.get_subscriptions.assert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_subscription_assert_failure():
|
||||
with pytest.raises(AssertionError):
|
||||
Subscription("test", False, 123)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_subscription_games_success(plugin, read, write):
|
||||
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_subscription_games_import",
|
||||
"params": {
|
||||
"subscription_names": ["sub_a"]
|
||||
}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_subscription_games.return_value = async_return_value([
|
||||
SubscriptionGame(game_title="game A", game_id="game_A"),
|
||||
SubscriptionGame(game_title="game B", game_id="game_B", start_time=1548495632),
|
||||
SubscriptionGame(game_title="game C", game_id="game_C", end_time=1548495633),
|
||||
SubscriptionGame(game_title="game D", game_id="game_D", start_time=1548495632, end_time=1548495633),
|
||||
])
|
||||
await plugin.run()
|
||||
plugin.prepare_subscription_games_context.assert_called_with(["sub_a"])
|
||||
plugin.get_subscription_games.assert_called_with("sub_a", 5)
|
||||
plugin.subscription_games_import_complete.asert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscription_games_import_success",
|
||||
"params": {
|
||||
"subscription_name": "sub_a",
|
||||
"subscription_games": [
|
||||
{
|
||||
"game_title": "game A",
|
||||
"game_id": "game_A"
|
||||
},
|
||||
{
|
||||
"game_title": "game B",
|
||||
"game_id": "game_B",
|
||||
"start_time": 1548495632
|
||||
},
|
||||
{
|
||||
"game_title": "game C",
|
||||
"game_id": "game_C",
|
||||
"end_time": 1548495633
|
||||
},
|
||||
{
|
||||
"game_title": "game D",
|
||||
"game_id": "game_D",
|
||||
"start_time": 1548495632,
|
||||
"end_time": 1548495633
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscription_games_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("exception,code,message", [
|
||||
(BackendError, 4, "Backend error"),
|
||||
(KeyError, 0, "Unknown error")
|
||||
])
|
||||
async def test_get_subscription_games_error(exception, code, message, plugin, read, write):
|
||||
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_subscription_games_import",
|
||||
"params": {
|
||||
"subscription_names": ["sub_a"]
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
plugin.get_subscription_games.side_effect = exception
|
||||
await plugin.run()
|
||||
plugin.get_subscription_games.assert_called()
|
||||
plugin.subscription_games_import_complete.asert_called_with()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscription_games_import_failure",
|
||||
"params": {
|
||||
"subscription_name": "sub_a",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "subscription_games_import_finished",
|
||||
"params": None
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_get_subscription_games_context_error(plugin, read, write):
|
||||
request_id = "31415"
|
||||
error_details = "Unexpected syntax"
|
||||
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
||||
plugin.prepare_subscription_games_context.side_effect = FailedParsingManifest(error_details)
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": "start_subscription_games_import",
|
||||
"params": {"subscription_names": ["sub_a", "sub_b"]}
|
||||
}
|
||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||
await plugin.run()
|
||||
|
||||
assert get_messages(write) == [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"error": {
|
||||
"code": error_code,
|
||||
"message": error_message,
|
||||
"data": error_details
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_import_already_in_progress_error(plugin, read, write):
|
||||
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
|
||||
requests = [
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "start_subscription_games_import",
|
||||
"params": {
|
||||
"subscription_names": ["sub_a"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "start_subscription_games_import",
|
||||
"params": {
|
||||
"subscription_names": ["sub_a","sub_b"]
|
||||
}
|
||||
}
|
||||
]
|
||||
read.side_effect = [
|
||||
async_return_value(create_message(requests[0])),
|
||||
async_return_value(create_message(requests[1])),
|
||||
async_return_value(b"", 10)
|
||||
]
|
||||
|
||||
await plugin.run()
|
||||
|
||||
responses = get_messages(write)
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
} in responses
|
||||
assert {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": 600,
|
||||
"message": "Import already in progress"
|
||||
}
|
||||
} in responses
|
||||
|
||||
Reference in New Issue
Block a user