mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 03:18:25 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad758b0da9 | ||
|
|
9062944d4f | ||
|
|
2251747281 | ||
|
|
0245e47a74 | ||
|
|
0c51ff2cc9 | ||
|
|
cd452b881d | ||
|
|
19c9f14ca9 | ||
|
|
f5683d222a | ||
|
|
44ea89ef63 | ||
|
|
325cf66c7d | ||
|
|
cd8aecac8f | ||
|
|
3aa37907fc | ||
|
|
01e844009b | ||
|
|
4a7febfa37 | ||
|
|
f9eb9ab6cb | ||
|
|
134fbe2752 | ||
|
|
bd8e6703e0 | ||
|
|
74e3825f10 | ||
|
|
62206318bd | ||
|
|
083b9f869f | ||
|
|
617dbdfee7 | ||
|
|
65f4334c03 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ Pipfile
|
|||||||
.idea
|
.idea
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest
|
image: registry-gitlab.gog.com/docker/python:3.7.3
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
|
|||||||
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.63",
|
version="0.65",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class Feature(Enum):
|
|||||||
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
ImportOSCompatibility = "ImportOSCompatibility"
|
ImportOSCompatibility = "ImportOSCompatibility"
|
||||||
ImportUserPresence = "ImportUserPresence"
|
ImportUserPresence = "ImportUserPresence"
|
||||||
|
ImportLocalSize = "ImportLocalSize"
|
||||||
|
ImportSubscriptions = "ImportSubscriptions"
|
||||||
|
ImportSubscriptionGames = "ImportSubscriptionGames"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -149,3 +152,13 @@ class PresenceState(Enum):
|
|||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionDiscovery(Flag):
|
||||||
|
"""Possible capabilities which inform what methods of subscriptions ownership detection are supported.
|
||||||
|
|
||||||
|
:param AUTOMATIC: integration can retrieve the proper status of subscription ownership.
|
||||||
|
:param USER_ENABLED: integration can handle override of ~class::`Subscription.owned` value to True
|
||||||
|
"""
|
||||||
|
AUTOMATIC = 1
|
||||||
|
USER_ENABLED = 2
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class BackendError(ApplicationError):
|
|||||||
|
|
||||||
class UnknownBackendResponse(ApplicationError):
|
class UnknownBackendResponse(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
super().__init__(4, "Backend responded in uknown way", data)
|
super().__init__(4, "Backend responded in unknown way", data)
|
||||||
|
|
||||||
class TooManyRequests(ApplicationError):
|
class TooManyRequests(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
|
|||||||
89
src/galaxy/api/importer.py
Normal file
89
src/galaxy/api/importer.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from galaxy.api.jsonrpc import ApplicationError
|
||||||
|
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Importer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_manger,
|
||||||
|
name,
|
||||||
|
get,
|
||||||
|
prepare_context,
|
||||||
|
notification_success,
|
||||||
|
notification_failure,
|
||||||
|
notification_finished,
|
||||||
|
complete,
|
||||||
|
):
|
||||||
|
self._task_manager = task_manger
|
||||||
|
self._name = name
|
||||||
|
self._get = get
|
||||||
|
self._prepare_context = prepare_context
|
||||||
|
self._notification_success = notification_success
|
||||||
|
self._notification_failure = notification_failure
|
||||||
|
self._notification_finished = notification_finished
|
||||||
|
self._complete = complete
|
||||||
|
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
async def _import_element(self, id_, context_):
|
||||||
|
try:
|
||||||
|
element = await self._get(id_, context_)
|
||||||
|
self._notification_success(id_, element)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._notification_failure(id_, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception raised in %s importer", self._name)
|
||||||
|
self._notification_failure(id_, UnknownError())
|
||||||
|
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [self._import_element(id_, context_) for id_ in ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
async def start(self, ids):
|
||||||
|
if self._import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
self._import_in_progress = True
|
||||||
|
try:
|
||||||
|
context = await self._prepare_context(ids)
|
||||||
|
self._task_manager.create_task(
|
||||||
|
self._import_elements(ids, context),
|
||||||
|
"{} import".format(self._name),
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
self._import_in_progress = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionImporter(Importer):
|
||||||
|
def __init__(self, notification_partially_finished, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self._notification_partially_finished = notification_partially_finished
|
||||||
|
|
||||||
|
async def _import_element(self, id_, context_):
|
||||||
|
try:
|
||||||
|
async for element in self._get(id_, context_):
|
||||||
|
self._notification_success(id_, element)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._notification_failure(id_, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception raised in %s importer", self._name)
|
||||||
|
self._notification_failure(id_, UnknownError())
|
||||||
|
finally:
|
||||||
|
self._notification_partially_finished(id_)
|
||||||
@@ -4,15 +4,16 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Set, Union
|
from typing import Any, Dict, List, Optional, Set, Union, AsyncGenerator
|
||||||
|
|
||||||
from galaxy.api.consts import Feature, OSCompatibility
|
from galaxy.api.consts import Feature, OSCompatibility
|
||||||
from galaxy.api.errors import ImportInProgress, UnknownError
|
|
||||||
from galaxy.api.jsonrpc import ApplicationError, Connection
|
from galaxy.api.jsonrpc import ApplicationError, Connection
|
||||||
from galaxy.api.types import (
|
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
|
from galaxy.task_manager import TaskManager
|
||||||
|
from galaxy.api.importer import Importer, CollectionImporter
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -31,69 +32,6 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
class Importer:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
task_manger,
|
|
||||||
name,
|
|
||||||
get,
|
|
||||||
prepare_context,
|
|
||||||
notification_success,
|
|
||||||
notification_failure,
|
|
||||||
notification_finished,
|
|
||||||
complete
|
|
||||||
):
|
|
||||||
self._task_manager = task_manger
|
|
||||||
self._name = name
|
|
||||||
self._get = get
|
|
||||||
self._prepare_context = prepare_context
|
|
||||||
self._notification_success = notification_success
|
|
||||||
self._notification_failure = notification_failure
|
|
||||||
self._notification_finished = notification_finished
|
|
||||||
self._complete = complete
|
|
||||||
|
|
||||||
self._import_in_progress = False
|
|
||||||
|
|
||||||
async def start(self, ids):
|
|
||||||
if self._import_in_progress:
|
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
async def import_element(id_, context_):
|
|
||||||
try:
|
|
||||||
element = await self._get(id_, context_)
|
|
||||||
self._notification_success(id_, element)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._notification_failure(id_, error)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Unexpected exception raised in %s importer", self._name)
|
|
||||||
self._notification_failure(id_, UnknownError())
|
|
||||||
|
|
||||||
async def import_elements(ids_, context_):
|
|
||||||
try:
|
|
||||||
imports = [import_element(id_, context_) for id_ in ids_]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
self._notification_finished()
|
|
||||||
self._complete()
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
logger.debug("Importing %s cancelled", self._name)
|
|
||||||
finally:
|
|
||||||
self._import_in_progress = False
|
|
||||||
|
|
||||||
self._import_in_progress = True
|
|
||||||
try:
|
|
||||||
context = await self._prepare_context(ids)
|
|
||||||
self._task_manager.create_task(
|
|
||||||
import_elements(ids, context),
|
|
||||||
"{} import".format(self._name),
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
self._import_in_progress = False
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""Use and override methods of this class to create a new platform integration."""
|
"""Use and override methods of this class to create a new platform integration."""
|
||||||
|
|
||||||
@@ -166,6 +104,27 @@ class Plugin:
|
|||||||
self._user_presence_import_finished,
|
self._user_presence_import_finished,
|
||||||
self.user_presence_import_complete
|
self.user_presence_import_complete
|
||||||
)
|
)
|
||||||
|
self._local_size_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"local size",
|
||||||
|
self.get_local_size,
|
||||||
|
self.prepare_local_size_context,
|
||||||
|
self._local_size_import_success,
|
||||||
|
self._local_size_import_failure,
|
||||||
|
self._local_size_import_finished,
|
||||||
|
self.local_size_import_complete
|
||||||
|
)
|
||||||
|
self._subscription_games_importer = CollectionImporter(
|
||||||
|
self._subscriptions_games_partial_import_finished,
|
||||||
|
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
|
# internal
|
||||||
self._register_method("shutdown", self._shutdown, internal=True)
|
self._register_method("shutdown", self._shutdown, internal=True)
|
||||||
@@ -233,6 +192,15 @@ class Plugin:
|
|||||||
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
||||||
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
|
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
|
||||||
|
|
||||||
|
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._detect_feature(Feature.ImportSubscriptions, ["get_subscriptions"])
|
||||||
|
|
||||||
|
self._register_method("start_subscription_games_import", self._start_subscription_games_import)
|
||||||
|
self._detect_feature(Feature.ImportSubscriptionGames, ["get_subscription_games"])
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -260,7 +228,8 @@ class Plugin:
|
|||||||
if self._implements(methods):
|
if self._implements(methods):
|
||||||
self._features.add(feature)
|
self._features.add(feature)
|
||||||
|
|
||||||
def _register_method(self, name, handler, result_name=None, internal=False, immediate=False, sensitive_params=False):
|
def _register_method(self, name, handler, result_name=None, internal=False, immediate=False,
|
||||||
|
sensitive_params=False):
|
||||||
def wrap_result(result):
|
def wrap_result(result):
|
||||||
if result_name:
|
if result_name:
|
||||||
result = {
|
result = {
|
||||||
@@ -613,6 +582,57 @@ class Plugin:
|
|||||||
def _user_presence_import_finished(self) -> None:
|
def _user_presence_import_finished(self) -> None:
|
||||||
self._connection.send_notification("user_presence_import_finished", None)
|
self._connection.send_notification("user_presence_import_finished", None)
|
||||||
|
|
||||||
|
def _local_size_import_success(self, game_id: str, size: Optional[int]) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"local_size_import_success",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"local_size": size
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _local_size_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"local_size_import_failure",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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 _subscriptions_games_partial_import_finished(self, subscription_name: str) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"subscription_games_partial_import_finished",
|
||||||
|
{
|
||||||
|
"subscription_name": subscription_name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _subscription_games_import_finished(self) -> None:
|
||||||
|
self._connection.send_notification("subscription_games_import_finished", None)
|
||||||
|
|
||||||
def lost_authentication(self) -> None:
|
def lost_authentication(self) -> None:
|
||||||
"""Notify the client that integration has lost authentication for the
|
"""Notify the client that integration has lost authentication for the
|
||||||
current user and is unable to perform actions which would require it.
|
current user and is unable to perform actions which would require it.
|
||||||
@@ -672,7 +692,7 @@ class Plugin:
|
|||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param stored_credentials: If the client received any credentials to store locally
|
:param stored_credentials: If the client received any credentials to store locally
|
||||||
in the previous session they will be passed here as a parameter.
|
in the previous session they will be passed here as a parameter.
|
||||||
|
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
@@ -694,7 +714,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
||||||
-> Union[NextStep, Authentication]:
|
-> Union[NextStep, Authentication]:
|
||||||
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
||||||
or :meth:`.pass_login_credentials`.
|
or :meth:`.pass_login_credentials`.
|
||||||
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
||||||
@@ -966,7 +986,7 @@ class Plugin:
|
|||||||
await self._user_presence_importer.start(user_id_list)
|
await self._user_presence_importer.start(user_id_list)
|
||||||
|
|
||||||
async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
|
async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_user_presence.
|
"""Override this method to prepare context for :meth:`get_user_presence`.
|
||||||
This allows for optimizations like batch requests to platform API.
|
This allows for optimizations like batch requests to platform API.
|
||||||
Default implementation returns None.
|
Default implementation returns None.
|
||||||
|
|
||||||
@@ -988,6 +1008,82 @@ class Plugin:
|
|||||||
def user_presence_import_complete(self) -> None:
|
def user_presence_import_complete(self) -> None:
|
||||||
"""Override this method to handle operations after presence import is finished (like updating cache)."""
|
"""Override this method to handle operations after presence import is finished (like updating cache)."""
|
||||||
|
|
||||||
|
async def _start_local_size_import(self, game_ids: List[str]) -> None:
|
||||||
|
await self._local_size_importer.start(game_ids)
|
||||||
|
|
||||||
|
async def prepare_local_size_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for :meth:`get_local_size`
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which information about size is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_local_size(self, game_id: str, context: Any) -> Optional[int]:
|
||||||
|
"""Override this method to return installed game size.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is preferable to avoid iterating over local game files when overriding this method.
|
||||||
|
If possible, please use a more efficient way of game size retrieval.
|
||||||
|
|
||||||
|
:param context: the value returned from :meth:`prepare_local_size_context`
|
||||||
|
:return: game size (in bytes) or `None` if game size cannot be determined;
|
||||||
|
'0' if the game is not installed, or if it is not present locally (e.g. installed
|
||||||
|
on another machine and accessible via remote connection, playable via web browser etc.)
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
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
|
||||||
|
Subscriptions available on platform.
|
||||||
|
This method is called by the GOG Galaxy Client.
|
||||||
|
"""
|
||||||
|
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) -> AsyncGenerator[
|
||||||
|
List[SubscriptionGame], None]:
|
||||||
|
"""Override this method to provide SubscriptionGames for a given subscription.
|
||||||
|
This method should `yield` a list of SubscriptionGames -> yield [sub_games]
|
||||||
|
|
||||||
|
This method will only be used if :meth:`get_subscriptions` has been implemented.
|
||||||
|
|
||||||
|
:param context: the value returned from :meth:`prepare_subscription_games_context`
|
||||||
|
:return a generator object that yields SubscriptionGames
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
:linenos:
|
||||||
|
|
||||||
|
async def get_subscription_games(subscription_name: str, context: Any):
|
||||||
|
while True:
|
||||||
|
games_page = await self._get_subscriptions_from_backend(subscription_name, i)
|
||||||
|
if not games_pages:
|
||||||
|
yield None
|
||||||
|
yield [SubGame(game['game_id'], game['game_title']) for game in games_page]
|
||||||
|
|
||||||
|
"""
|
||||||
|
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):
|
def create_and_run_plugin(plugin_class, argv):
|
||||||
"""Call this method as an entry point for the implemented integration.
|
"""Call this method as an entry point for the implemented integration.
|
||||||
@@ -1037,7 +1133,6 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
writer.close()
|
writer.close()
|
||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState, SubscriptionDiscovery
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -62,10 +62,10 @@ class NextStep:
|
|||||||
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
||||||
|
|
||||||
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
||||||
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
||||||
:param cookies: browser initial set of cookies
|
:param cookies: browser initial set of cookies
|
||||||
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
||||||
on every document at given step of internal browser authentication.
|
on every document at given step of internal browser authentication.
|
||||||
"""
|
"""
|
||||||
next_step: str
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
@@ -216,3 +216,42 @@ class UserPresence:
|
|||||||
game_title: Optional[str] = None
|
game_title: Optional[str] = None
|
||||||
in_game_status: Optional[str] = None
|
in_game_status: Optional[str] = None
|
||||||
full_status: Optional[str] = None
|
full_status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Subscription:
|
||||||
|
"""Information about a subscription.
|
||||||
|
|
||||||
|
: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.
|
||||||
|
:param subscription_discovery: combination of settings that can be manually
|
||||||
|
chosen by user to determine subscription handling behaviour. For example, if the integration cannot retrieve games
|
||||||
|
for subscription when user doesn't own it, then USER_ENABLED should not be used.
|
||||||
|
If the integration cannot determine subscription ownership for a user then AUTOMATIC should not be used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
subscription_name: str
|
||||||
|
owned: Optional[bool] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
subscription_discovery: SubscriptionDiscovery = SubscriptionDiscovery.AUTOMATIC | \
|
||||||
|
SubscriptionDiscovery.USER_ENABLED
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.subscription_discovery in [SubscriptionDiscovery.AUTOMATIC, SubscriptionDiscovery.USER_ENABLED,
|
||||||
|
SubscriptionDiscovery.AUTOMATIC | SubscriptionDiscovery.USER_ENABLED]
|
||||||
|
|
||||||
|
|
||||||
|
@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 will be removed from subscription.
|
||||||
|
"""
|
||||||
|
game_title: str
|
||||||
|
game_id: str
|
||||||
|
start_time: Optional[int] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
This module standarize http traffic and the error handling for further communication with the GOG Galaxy 2.0.
|
This module standardizes http traffic and the error handling for further communication with the GOG Galaxy 2.0.
|
||||||
|
|
||||||
It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
|
It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
|
||||||
Examplary simple web service could looks like:
|
Exemplary simple web service could looks like:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ class HttpClient:
|
|||||||
|
|
||||||
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||||
"""
|
"""
|
||||||
Creates TCP connector with resonable defaults.
|
Creates TCP connector with reasonable defaults.
|
||||||
For details about available parameters refer to
|
For details about available parameters refer to
|
||||||
`aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
|
`aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
|
||||||
"""
|
"""
|
||||||
@@ -86,11 +86,11 @@ def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
|||||||
|
|
||||||
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
||||||
"""
|
"""
|
||||||
Creates client session with resonable defaults.
|
Creates client session with reasonable defaults.
|
||||||
For details about available parameters refer to
|
For details about available parameters refer to
|
||||||
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||||
|
|
||||||
Examplary customization:
|
Exemplary customization:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -124,25 +124,25 @@ def handle_exception():
|
|||||||
raise BackendNotAvailable()
|
raise BackendNotAvailable()
|
||||||
except aiohttp.ClientConnectionError:
|
except aiohttp.ClientConnectionError:
|
||||||
raise NetworkError()
|
raise NetworkError()
|
||||||
except aiohttp.ContentTypeError:
|
except aiohttp.ContentTypeError as error:
|
||||||
raise UnknownBackendResponse()
|
raise UnknownBackendResponse(error.message)
|
||||||
except aiohttp.ClientResponseError as error:
|
except aiohttp.ClientResponseError as error:
|
||||||
if error.status == HTTPStatus.UNAUTHORIZED:
|
if error.status == HTTPStatus.UNAUTHORIZED:
|
||||||
raise AuthenticationRequired()
|
raise AuthenticationRequired(error.message)
|
||||||
if error.status == HTTPStatus.FORBIDDEN:
|
if error.status == HTTPStatus.FORBIDDEN:
|
||||||
raise AccessDenied()
|
raise AccessDenied(error.message)
|
||||||
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||||
raise BackendNotAvailable()
|
raise BackendNotAvailable(error.message)
|
||||||
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||||
raise TooManyRequests()
|
raise TooManyRequests(error.message)
|
||||||
if error.status >= 500:
|
if error.status >= 500:
|
||||||
raise BackendError()
|
raise BackendError(error.message)
|
||||||
if error.status >= 400:
|
if error.status >= 400:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Got status %d while performing %s request for %s",
|
"Got status %d while performing %s request for %s",
|
||||||
error.status, error.request_info.method, str(error.request_info.url)
|
error.status, error.request_info.method, str(error.request_info.url)
|
||||||
)
|
)
|
||||||
raise UnknownError()
|
raise UnknownError(error.message)
|
||||||
except aiohttp.ClientError:
|
except aiohttp.ClientError as e:
|
||||||
logger.exception("Caught exception while performing request")
|
logger.exception("Caught exception while performing request")
|
||||||
raise UnknownError()
|
raise UnknownError(repr(e))
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ async def plugin(reader, writer):
|
|||||||
"get_user_presence",
|
"get_user_presence",
|
||||||
"prepare_user_presence_context",
|
"prepare_user_presence_context",
|
||||||
"user_presence_import_complete",
|
"user_presence_import_complete",
|
||||||
|
"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:
|
with ExitStack() as stack:
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ def test_base_class():
|
|||||||
Feature.LaunchPlatformClient,
|
Feature.LaunchPlatformClient,
|
||||||
Feature.ImportGameLibrarySettings,
|
Feature.ImportGameLibrarySettings,
|
||||||
Feature.ImportOSCompatibility,
|
Feature.ImportOSCompatibility,
|
||||||
Feature.ImportUserPresence
|
Feature.ImportUserPresence,
|
||||||
|
Feature.ImportLocalSize,
|
||||||
|
Feature.ImportSubscriptions,
|
||||||
|
Feature.ImportSubscriptionGames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
188
tests/test_local_size.py
Normal file
188
tests/test_local_size.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.errors import FailedParsingManifest
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_local_size_success(plugin, read, write):
|
||||||
|
context = {'abc': 'def'}
|
||||||
|
plugin.prepare_local_size_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": ["777", "13", "42"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_local_size.side_effect = [
|
||||||
|
async_return_value(100000000000),
|
||||||
|
async_return_value(None),
|
||||||
|
async_return_value(3333333)
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_local_size.assert_has_calls([
|
||||||
|
call("777", context),
|
||||||
|
call("13", context),
|
||||||
|
call("42", context)
|
||||||
|
])
|
||||||
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "777",
|
||||||
|
"local_size": 100000000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "13",
|
||||||
|
"local_size": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "42",
|
||||||
|
"local_size": 3333333
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(FailedParsingManifest, 200, "Failed parsing manifest"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_local_size_error(exception, code, message, plugin, read, write):
|
||||||
|
game_id = "6"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": [game_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_local_size.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_local_size.assert_called()
|
||||||
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_local_size_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
error_details = "Unexpected syntax"
|
||||||
|
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
||||||
|
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(error_details)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": ["6"]}
|
||||||
|
}
|
||||||
|
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_local_size_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["13"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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
|
||||||
|
|
||||||
340
tests/test_subscriptions.py
Normal file
340
tests/test_subscriptions.py
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.types import Subscription, SubscriptionGame
|
||||||
|
from galaxy.api.consts import SubscriptionDiscovery
|
||||||
|
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
|
||||||
|
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_discovery=SubscriptionDiscovery.AUTOMATIC),
|
||||||
|
Subscription("3", True, 1580899100, SubscriptionDiscovery.USER_ENABLED)
|
||||||
|
])
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_subscriptions.assert_called_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": {
|
||||||
|
"subscriptions": [
|
||||||
|
{
|
||||||
|
"subscription_name": "1",
|
||||||
|
'subscription_discovery': 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subscription_name": "2",
|
||||||
|
"owned": False,
|
||||||
|
'subscription_discovery': 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"subscription_name": "3",
|
||||||
|
"owned": True,
|
||||||
|
"end_time": 1580899100,
|
||||||
|
'subscription_discovery': 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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_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)]
|
||||||
|
|
||||||
|
async def sub_games():
|
||||||
|
games = [
|
||||||
|
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),
|
||||||
|
]
|
||||||
|
yield [game for game in games]
|
||||||
|
|
||||||
|
plugin.get_subscription_games.return_value = sub_games()
|
||||||
|
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_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_subscription_games_success_empty(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)]
|
||||||
|
|
||||||
|
async def sub_games():
|
||||||
|
yield None
|
||||||
|
|
||||||
|
plugin.get_subscription_games.return_value = sub_games()
|
||||||
|
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": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method':
|
||||||
|
'subscription_games_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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_partial_import_finished',
|
||||||
|
'params': {
|
||||||
|
"subscription_name": "sub_a"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 backend error"
|
||||||
|
error_message, error_code = BackendError().message, BackendError().code
|
||||||
|
plugin.prepare_subscription_games_context.side_effect = BackendError(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