mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0498caf7 | ||
|
|
f4ded58c28 | ||
|
|
d2f34349b8 | ||
|
|
f062387ddb | ||
|
|
5a699100d6 | ||
|
|
5daa386f6e | ||
|
|
761598de54 | ||
|
|
ab44e137c3 | ||
|
|
948bfcd971 | ||
|
|
6196e751c6 | ||
|
|
a5b2a0890e | ||
|
|
46cda7d61a | ||
|
|
f0f6210c3e | ||
|
|
468dfcc60d | ||
|
|
8f91f705ee | ||
|
|
46588c321e | ||
|
|
947c578121 | ||
|
|
aba9b0ed6b | ||
|
|
f0d65a72ff | ||
|
|
96cb48fcaf | ||
|
|
17b0542fdf | ||
|
|
0cf447bdcf | ||
|
|
259702e0de | ||
|
|
b96c55397e | ||
|
|
f82cab2770 | ||
|
|
1e7c284035 | ||
|
|
0c49ee315e | ||
|
|
aaeca6b47e | ||
|
|
fe8f7e929a | ||
|
|
49da4d4d37 | ||
|
|
9745dcd8ef | ||
|
|
ad758b0da9 | ||
|
|
9062944d4f | ||
|
|
2251747281 | ||
|
|
0245e47a74 | ||
|
|
0c51ff2cc9 | ||
|
|
cd452b881d | ||
|
|
19c9f14ca9 | ||
|
|
f5683d222a | ||
|
|
44ea89ef63 | ||
|
|
325cf66c7d | ||
|
|
cd8aecac8f | ||
|
|
3aa37907fc | ||
|
|
01e844009b | ||
|
|
4a7febfa37 | ||
|
|
f9eb9ab6cb | ||
|
|
134fbe2752 | ||
|
|
bd8e6703e0 | ||
|
|
74e3825f10 | ||
|
|
62206318bd | ||
|
|
083b9f869f | ||
|
|
617dbdfee7 | ||
|
|
65f4334c03 | ||
|
|
26102dd832 | ||
|
|
cdcebda529 | ||
|
|
a83f348d7d | ||
|
|
1c196d60d5 | ||
|
|
deb125ec48 | ||
|
|
4cc0055119 | ||
|
|
00164fab67 | ||
|
|
453cd1cc70 | ||
|
|
1f55253fd7 | ||
|
|
7aa3b01abd | ||
|
|
bd14d58bad | ||
|
|
274b9a2c18 | ||
|
|
75e5a66fbe | ||
|
|
2a9ec3067d | ||
|
|
69532a5ba9 | ||
|
|
f5d47b0167 | ||
|
|
02f4faa432 | ||
|
|
3d3922c965 | ||
|
|
b695cdfc78 | ||
|
|
66ab1809b8 | ||
|
|
8bf367d0f9 | ||
|
|
2cf83395fa | ||
|
|
4aa76b6e3d | ||
|
|
c03465e8f2 | ||
|
|
810a87718d | ||
|
|
e32abe11b7 | ||
|
|
d79f183826 | ||
|
|
78f1d5a4cc | ||
|
|
9041dbd98c | ||
|
|
e57ecc489c | ||
|
|
0a20629459 | ||
|
|
1585bab203 | ||
|
|
92caf682d8 | ||
|
|
062d6a9428 | ||
|
|
c874bc1d6e | ||
|
|
2dc56571d6 | ||
|
|
eb216a50a8 | ||
|
|
c9b1c8fcae | ||
|
|
a19a6cf11f | ||
|
|
98cff9cfb8 | ||
|
|
2e2aa8c4a0 | ||
|
|
f57e03db2d | ||
|
|
66085e2239 | ||
|
|
4d3c9b78c4 | ||
|
|
392e4c5f68 | ||
|
|
4d6d3b8eb2 | ||
|
|
d5610221a9 | ||
|
|
aa7b398d3b | ||
|
|
8d6ec500f9 |
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: GOG GALAXY 2.0 issue
|
||||||
|
url: https://mantis2.gog.com/
|
||||||
|
about: Report issues related to GOG GALAXY 2.0, official integrations or the whole ecosystem
|
||||||
|
- name: Platform ID request
|
||||||
|
url: https://github.com/gogcom/galaxy-integrations-python-api/issues/160
|
||||||
|
about: Report missing platform id
|
||||||
|
- name: Community integrations
|
||||||
|
url: https://github.com/Mixaill/awesome-gog-galaxy
|
||||||
|
about: Find integrations and their maintainers, request new integrations or report issues related to unofficial integrations.
|
||||||
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: API issue
|
||||||
|
about: Report a bug or problem with current API architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Problem**
|
||||||
|
<!-- Describe the problem you faced. -->
|
||||||
|
|
||||||
|
**Solution**
|
||||||
|
<!-- Describe the solution you'd like. -->
|
||||||
|
|
||||||
|
**Alternatives**
|
||||||
|
<!-- Optionally describe possible alternatives or current workarounds if any. -->
|
||||||
20
.github/workflows/ci.yml
vendored
Normal file
20
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,3 +7,5 @@ docs/build/
|
|||||||
Pipfile
|
Pipfile
|
||||||
.idea
|
.idea
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
|
.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
|
||||||
@@ -7,7 +7,7 @@ stages:
|
|||||||
test_package:
|
test_package:
|
||||||
stage: test
|
stage: test
|
||||||
script:
|
script:
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements-dev.txt
|
||||||
- pytest
|
- pytest
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
dist: xenial # required for Python >= 3.7
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.7"
|
|
||||||
install:
|
|
||||||
- pip install -r requirements.txt
|
|
||||||
script:
|
|
||||||
- pytest
|
|
||||||
@@ -4,10 +4,10 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
|
|
||||||
| ID | Name |
|
| ID | Name |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| test | Testing purposes |
|
||||||
| steam | Steam |
|
| steam | Steam |
|
||||||
| psn | PlayStation Network |
|
| psn | PlayStation Network |
|
||||||
| xboxone | Xbox Live |
|
| xboxone | Xbox Live |
|
||||||
| generic | Manually added games |
|
|
||||||
| origin | Origin |
|
| origin | Origin |
|
||||||
| uplay | Uplay |
|
| uplay | Uplay |
|
||||||
| battlenet | Battle.net |
|
| battlenet | Battle.net |
|
||||||
@@ -80,3 +80,12 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
| nds | Nintendo DS |
|
| nds | Nintendo DS |
|
||||||
| 3ds | Nintendo 3DS |
|
| 3ds | Nintendo 3DS |
|
||||||
| pathofexile | Path of Exile |
|
| pathofexile | Path of Exile |
|
||||||
|
| twitch | Twitch |
|
||||||
|
| minecraft | Minecraft |
|
||||||
|
| gamesessions | GameSessions |
|
||||||
|
| nuuvem | Nuuvem |
|
||||||
|
| fxstore | FX Store |
|
||||||
|
| indiegala | IndieGala |
|
||||||
|
| playfire | Playfire |
|
||||||
|
| oculus | Oculus |
|
||||||
|
| rockstar | Rockstar |
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -36,20 +36,31 @@ Communication between an integration and the client is also possible with the us
|
|||||||
import sys
|
import sys
|
||||||
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.api.types import Authentication, Game, LicenseInfo, LicenseType
|
||||||
|
|
||||||
|
|
||||||
class PluginExample(Plugin):
|
class PluginExample(Plugin):
|
||||||
def __init__(self, reader, writer, token):
|
def __init__(self, reader, writer, token):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Platform.Generic, # Choose platform from available list
|
Platform.Test, # choose platform from available list
|
||||||
"0.1", # Version
|
"0.1", # version
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
||||||
# implement methods
|
# implement methods
|
||||||
|
|
||||||
|
# required
|
||||||
async def authenticate(self, stored_credentials=None):
|
async def authenticate(self, stored_credentials=None):
|
||||||
pass
|
return Authentication('test_user_id', 'Test User Name')
|
||||||
|
|
||||||
|
# required
|
||||||
|
async def get_owned_games(self):
|
||||||
|
return [
|
||||||
|
Game('test', 'The Test', None, LicenseInfo(LicenseType.SinglePurchase))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
create_and_run_plugin(PluginExample, sys.argv)
|
create_and_run_plugin(PluginExample, sys.argv)
|
||||||
@@ -76,6 +87,20 @@ In order to be found by GOG Galaxy 2.0 an integration folder should be placed in
|
|||||||
|
|
||||||
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
<a href='https://docs.python.org/3.7/howto/logging.html'>Root logger</a> is already setup by GOG Galaxy to store rotated log files in:
|
||||||
|
|
||||||
|
- Windows:
|
||||||
|
|
||||||
|
`%programdata%\GOG.com\Galaxy\logs`
|
||||||
|
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
`/Users/Shared/GOG.com/Galaxy/Logs`
|
||||||
|
|
||||||
|
Plugin logs are kept in `plugin-<platform>-<guid>.log`.
|
||||||
|
When debugging, inspecting the other side of communication in the `GalaxyClient.log` can be helpful as well.
|
||||||
|
|
||||||
### Manifest
|
### Manifest
|
||||||
|
|
||||||
<a name="deploy-manifest"></a>
|
<a name="deploy-manifest"></a>
|
||||||
@@ -84,8 +109,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Example plugin",
|
"name": "Example plugin",
|
||||||
"platform": "generic",
|
"platform": "test",
|
||||||
"guid": "UNIQUE-GUID",
|
"guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"description": "Example plugin",
|
"description": "Example plugin",
|
||||||
"author": "Name",
|
"author": "Name",
|
||||||
@@ -97,9 +122,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
|
|
||||||
| property | description |
|
| property | description |
|
||||||
|---------------|---|
|
|---------------|---|
|
||||||
| `guid` | |
|
| `guid` | custom Globally Unique Identifier |
|
||||||
| `description` | |
|
| `version` | the same string as `version` in `Plugin` constructor |
|
||||||
| `url` | |
|
|
||||||
| `script` | path of the entry point module, relative to the integration folder |
|
| `script` | path of the entry point module, relative to the integration folder |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
Sphinx==2.0.1
|
Sphinx==4.2.0
|
||||||
sphinx-rtd-theme==0.4.3
|
sphinx-rtd-theme==1.0.0
|
||||||
sphinx-autodoc-typehints==1.6.0
|
sphinx-autodoc-typehints==1.12.0
|
||||||
sphinxcontrib-asyncio==0.2.0
|
sphinxcontrib-asyncio==0.3.0
|
||||||
m2r==0.2.1
|
m2r2==0.3.1
|
||||||
|
typing-extensions==3.10.0.2
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ extensions = [
|
|||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinxcontrib.asyncio',
|
'sphinxcontrib.asyncio',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'm2r' # mdinclude directive for makrdown files
|
'm2r2' # mdinclude directive for makrdown files
|
||||||
]
|
]
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
autodoc_inherit_docstrings = False
|
autodoc_inherit_docstrings = False
|
||||||
|
|||||||
7
requirements-dev.txt
Normal file
7
requirements-dev.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
pytest==5.2.2
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-mock==1.10.3
|
||||||
|
pytest-mypy==0.4.1
|
||||||
|
pytest-flakes==4.0.0
|
||||||
|
types-certifi==2020.4.0
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
-e .
|
-e .
|
||||||
pytest==4.2.0
|
# Copied from setup.py because of a pip bug
|
||||||
pytest-asyncio==0.10.0
|
# see https://github.com/pypa/pip/issues/4780
|
||||||
pytest-mock==1.10.3
|
|
||||||
pytest-mypy==0.3.2
|
|
||||||
pytest-flakes==4.0.0
|
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
psutil==5.6.3; sys_platform == 'darwin'
|
psutil==5.6.6; sys_platform == 'darwin'
|
||||||
|
# End of copy from setup.py
|
||||||
|
|||||||
7
setup.py
7
setup.py
@@ -2,14 +2,15 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.48",
|
version="0.69",
|
||||||
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',
|
||||||
packages=find_packages("src"),
|
packages=find_packages("src"),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp==3.5.4",
|
"aiohttp>=3.5.4",
|
||||||
"certifi==2019.3.9"
|
"certifi>=2019.3.9",
|
||||||
|
"psutil>=5.6.6; sys_platform == 'darwin'"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__path__: str = __import__('pkgutil').extend_path(__path__, __name__)
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger(__name__).setLevel(logging.INFO)
|
||||||
|
|
||||||
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class Platform(Enum):
|
|||||||
NintendoDs = "nds"
|
NintendoDs = "nds"
|
||||||
Nintendo3Ds = "3ds"
|
Nintendo3Ds = "3ds"
|
||||||
PathOfExile = "pathofexile"
|
PathOfExile = "pathofexile"
|
||||||
|
Twitch = "twitch"
|
||||||
|
Minecraft = "minecraft"
|
||||||
|
GameSessions = "gamesessions"
|
||||||
|
Nuuvem = "nuuvem"
|
||||||
|
FXStore = "fxstore"
|
||||||
|
IndieGala = "indiegala"
|
||||||
|
Playfire = "playfire"
|
||||||
|
Oculus = "oculus"
|
||||||
|
Test = "test"
|
||||||
|
Rockstar = "rockstar"
|
||||||
|
|
||||||
|
|
||||||
class Feature(Enum):
|
class Feature(Enum):
|
||||||
"""Possible features that can be implemented by an integration.
|
"""Possible features that can be implemented by an integration.
|
||||||
@@ -100,6 +111,12 @@ class Feature(Enum):
|
|||||||
ImportFriends = "ImportFriends"
|
ImportFriends = "ImportFriends"
|
||||||
ShutdownPlatformClient = "ShutdownPlatformClient"
|
ShutdownPlatformClient = "ShutdownPlatformClient"
|
||||||
LaunchPlatformClient = "LaunchPlatformClient"
|
LaunchPlatformClient = "LaunchPlatformClient"
|
||||||
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
|
ImportOSCompatibility = "ImportOSCompatibility"
|
||||||
|
ImportUserPresence = "ImportUserPresence"
|
||||||
|
ImportLocalSize = "ImportLocalSize"
|
||||||
|
ImportSubscriptions = "ImportSubscriptions"
|
||||||
|
ImportSubscriptionGames = "ImportSubscriptionGames"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -118,3 +135,30 @@ class LocalGameState(Flag):
|
|||||||
None_ = 0
|
None_ = 0
|
||||||
Installed = 1
|
Installed = 1
|
||||||
Running = 2
|
Running = 2
|
||||||
|
|
||||||
|
|
||||||
|
class OSCompatibility(Flag):
|
||||||
|
"""Possible game OS compatibility.
|
||||||
|
Use "bitwise or" to express multiple OSs compatibility, e.g. ``os=OSCompatibility.Windows|OSCompatibility.MacOS``
|
||||||
|
"""
|
||||||
|
Windows = 0b001
|
||||||
|
MacOS = 0b010
|
||||||
|
Linux = 0b100
|
||||||
|
|
||||||
|
|
||||||
|
class PresenceState(Enum):
|
||||||
|
""""Possible states of a user."""
|
||||||
|
Unknown = "unknown"
|
||||||
|
Online = "online"
|
||||||
|
Offline = "offline"
|
||||||
|
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
|
||||||
|
|||||||
@@ -2,74 +2,87 @@ from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
|||||||
|
|
||||||
assert UnknownError
|
assert UnknownError
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Authentication required", data=None):
|
||||||
super().__init__(1, "Authentication required", data)
|
super().__init__(1, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendNotAvailable(ApplicationError):
|
class BackendNotAvailable(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend not available", data=None):
|
||||||
super().__init__(2, "Backend not available", data)
|
super().__init__(2, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendTimeout(ApplicationError):
|
class BackendTimeout(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend timed out", data=None):
|
||||||
super().__init__(3, "Backend timed out", data)
|
super().__init__(3, message, data)
|
||||||
|
|
||||||
|
|
||||||
class BackendError(ApplicationError):
|
class BackendError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Backend error", data=None):
|
||||||
super().__init__(4, "Backend error", data)
|
super().__init__(4, message, data)
|
||||||
|
|
||||||
class UnknownBackendResponse(ApplicationError):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(4, "Backend responded in uknown way", data)
|
|
||||||
|
|
||||||
class TooManyRequests(ApplicationError):
|
class TooManyRequests(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Too many requests. Try again later", data=None):
|
||||||
super().__init__(5, "Too many requests. Try again later", data)
|
super().__init__(5, message, data)
|
||||||
|
|
||||||
|
|
||||||
|
class UnknownBackendResponse(ApplicationError):
|
||||||
|
def __init__(self, message="Backend responded in unknown way", data=None):
|
||||||
|
super().__init__(6, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidCredentials(ApplicationError):
|
class InvalidCredentials(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Invalid credentials", data=None):
|
||||||
super().__init__(100, "Invalid credentials", data)
|
super().__init__(100, message, data)
|
||||||
|
|
||||||
|
|
||||||
class NetworkError(ApplicationError):
|
class NetworkError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Network error", data=None):
|
||||||
super().__init__(101, "Network error", data)
|
super().__init__(101, message, data)
|
||||||
|
|
||||||
class LoggedInElsewhere(ApplicationError):
|
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(102, "Logged in elsewhere", data)
|
|
||||||
|
|
||||||
class ProtocolError(ApplicationError):
|
class ProtocolError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Protocol error", data=None):
|
||||||
super().__init__(103, "Protocol error", data)
|
super().__init__(103, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TemporaryBlocked(ApplicationError):
|
class TemporaryBlocked(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Temporary blocked", data=None):
|
||||||
super().__init__(104, "Temporary blocked", data)
|
super().__init__(104, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Banned(ApplicationError):
|
class Banned(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Banned", data=None):
|
||||||
super().__init__(105, "Banned", data)
|
super().__init__(105, message, data)
|
||||||
|
|
||||||
|
|
||||||
class AccessDenied(ApplicationError):
|
class AccessDenied(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Access denied", data=None):
|
||||||
super().__init__(106, "Access denied", data)
|
super().__init__(106, message, data)
|
||||||
|
|
||||||
|
|
||||||
class FailedParsingManifest(ApplicationError):
|
class FailedParsingManifest(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Failed parsing manifest", data=None):
|
||||||
super().__init__(200, "Failed parsing manifest", data)
|
super().__init__(200, message, data)
|
||||||
|
|
||||||
|
|
||||||
class TooManyMessagesSent(ApplicationError):
|
class TooManyMessagesSent(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Too many messages sent", data=None):
|
||||||
super().__init__(300, "Too many messages sent", data)
|
super().__init__(300, message, data)
|
||||||
|
|
||||||
|
|
||||||
class IncoherentLastMessage(ApplicationError):
|
class IncoherentLastMessage(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Different last message id on backend", data=None):
|
||||||
super().__init__(400, "Different last message id on backend", data)
|
super().__init__(400, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MessageNotFound(ApplicationError):
|
class MessageNotFound(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Message not found", data=None):
|
||||||
super().__init__(500, "Message not found", data)
|
super().__init__(500, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ImportInProgress(ApplicationError):
|
class ImportInProgress(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Import already in progress", data=None):
|
||||||
super().__init__(600, "Import already in progress", data)
|
super().__init__(600, message, data)
|
||||||
|
|||||||
102
src/galaxy/api/importer.py
Normal file
102
src/galaxy/api/importer.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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_)
|
||||||
|
|
||||||
|
|
||||||
|
class SynchroneousImporter(Importer):
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
for id_ in ids_:
|
||||||
|
await self._import_element(id_, context_)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from collections.abc import Iterable
|
from collections.abc import Iterable, Mapping
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
@@ -8,39 +8,64 @@ import json
|
|||||||
from galaxy.reader import StreamLineReader
|
from galaxy.reader import StreamLineReader
|
||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
def __init__(self, code, message, data=None):
|
def __init__(self, code, message, data=None):
|
||||||
self.code = code
|
self.code = code
|
||||||
self.message = message
|
self.message = str(message)
|
||||||
self.data = data
|
self.data = {}
|
||||||
|
if data is not None:
|
||||||
|
if not isinstance(data, Mapping):
|
||||||
|
raise TypeError(f"Data parameter should be a mapping, got this instead: {data}")
|
||||||
|
self.data = data
|
||||||
|
self.data.update({"internal_type": type(self).__name__})
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.code == other.code and self.message == other.message and self.data == other.data
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
obj = {
|
||||||
|
"code": self.code,
|
||||||
|
"message": self.message,
|
||||||
|
"data": self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Parse error", data=None):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidRequest(JsonRpcError):
|
class InvalidRequest(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid Request", data=None):
|
||||||
super().__init__(-32600, "Invalid Request")
|
super().__init__(-32600, message, data)
|
||||||
|
|
||||||
|
|
||||||
class MethodNotFound(JsonRpcError):
|
class MethodNotFound(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method not found", data=None):
|
||||||
super().__init__(-32601, "Method not found")
|
super().__init__(-32601, message, data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
class InvalidParams(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Invalid params", data=None):
|
||||||
super().__init__(-32602, "Invalid params")
|
super().__init__(-32602, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Timeout(JsonRpcError):
|
class Timeout(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method timed out", data=None):
|
||||||
super().__init__(-32000, "Method timed out")
|
super().__init__(-32000, message, data)
|
||||||
|
|
||||||
|
|
||||||
class Aborted(JsonRpcError):
|
class Aborted(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self, message="Method aborted", data=None):
|
||||||
super().__init__(-32001, "Method aborted")
|
super().__init__(-32001, message, data)
|
||||||
|
|
||||||
|
|
||||||
class ApplicationError(JsonRpcError):
|
class ApplicationError(JsonRpcError):
|
||||||
def __init__(self, code, message, data):
|
def __init__(self, code, message, data):
|
||||||
@@ -48,11 +73,14 @@ 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):
|
class UnknownError(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, message="Unknown error", data=None):
|
||||||
super().__init__(0, "Unknown error", data)
|
super().__init__(0, message, data)
|
||||||
|
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
|
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
||||||
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +96,7 @@ def anonymise_sensitive_params(params, sensitive_params):
|
|||||||
|
|
||||||
return params
|
return params
|
||||||
|
|
||||||
class Server():
|
class Connection():
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
self._active = True
|
self._active = True
|
||||||
self._reader = StreamLineReader(reader)
|
self._reader = StreamLineReader(reader)
|
||||||
@@ -77,6 +105,8 @@ class Server():
|
|||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._task_manager = TaskManager("jsonrpc server")
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
|
self._last_request_id = 0
|
||||||
|
self._requests_futures = {}
|
||||||
|
|
||||||
def register_method(self, name, callback, immediate, sensitive_params=False):
|
def register_method(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
@@ -102,6 +132,47 @@ class Server():
|
|||||||
"""
|
"""
|
||||||
self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
|
async def send_request(self, method, params, sensitive_params):
|
||||||
|
"""
|
||||||
|
Send request
|
||||||
|
|
||||||
|
:param method:
|
||||||
|
:param params:
|
||||||
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
self._last_request_id += 1
|
||||||
|
request_id = str(self._last_request_id)
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
future = loop.create_future()
|
||||||
|
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sending request: id=%s, method=%s, params=%s",
|
||||||
|
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._send_request(request_id, method, params)
|
||||||
|
return await future
|
||||||
|
|
||||||
|
def send_notification(self, method, params, sensitive_params=False):
|
||||||
|
"""
|
||||||
|
Send notification
|
||||||
|
|
||||||
|
:param method:
|
||||||
|
:param params:
|
||||||
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Sending notification: method=%s, params=%s",
|
||||||
|
method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
)
|
||||||
|
|
||||||
|
self._send_notification(method, params)
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while self._active:
|
while self._active:
|
||||||
try:
|
try:
|
||||||
@@ -113,37 +184,63 @@ class Server():
|
|||||||
self._eof()
|
self._eof()
|
||||||
continue
|
continue
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received %d bytes of data", len(data))
|
logger.debug("Received %d bytes of data", len(data))
|
||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
await asyncio.sleep(0) # To not starve task queue
|
await asyncio.sleep(0) # To not starve task queue
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
logging.info("Closing JSON-RPC server - not more messages will be read")
|
if self._active:
|
||||||
self._active = False
|
logger.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
|
self._active = False
|
||||||
|
|
||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
await self._task_manager.wait()
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _eof(self):
|
def _eof(self):
|
||||||
logging.info("Received EOF")
|
logger.info("Received EOF")
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _handle_input(self, data):
|
def _handle_input(self, data):
|
||||||
try:
|
try:
|
||||||
request = self._parse_request(data)
|
message = self._parse_message(data)
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
self._send_error(None, error)
|
self._send_error(None, error)
|
||||||
return
|
return
|
||||||
|
|
||||||
if request.id is not None:
|
if isinstance(message, Request):
|
||||||
self._handle_request(request)
|
if message.id is not None:
|
||||||
else:
|
self._handle_request(message)
|
||||||
self._handle_notification(request)
|
else:
|
||||||
|
self._handle_notification(message)
|
||||||
|
elif isinstance(message, Response):
|
||||||
|
self._handle_response(message)
|
||||||
|
|
||||||
|
def _handle_response(self, response):
|
||||||
|
request_future = self._requests_futures.get(int(response.id))
|
||||||
|
if request_future is None:
|
||||||
|
response_type = "response" if response.result is not None else "error"
|
||||||
|
logger.warning("Received %s for unknown request: %s", response_type, response.id)
|
||||||
|
return
|
||||||
|
|
||||||
|
future, sensitive_params = request_future
|
||||||
|
|
||||||
|
if response.error:
|
||||||
|
error = JsonRpcError(
|
||||||
|
response.error.setdefault("code", 0),
|
||||||
|
response.error.setdefault("message", ""),
|
||||||
|
response.error.setdefault("data", None)
|
||||||
|
)
|
||||||
|
self._log_error(response, error, sensitive_params)
|
||||||
|
future.set_exception(error)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log_response(response, sensitive_params)
|
||||||
|
future.set_result(response.result)
|
||||||
|
|
||||||
def _handle_notification(self, request):
|
def _handle_notification(self, request):
|
||||||
method = self._notifications.get(request.method)
|
method = self._notifications.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown notification: %s", request.method)
|
logger.error("Received unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, immediate, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
@@ -160,12 +257,12 @@ class Server():
|
|||||||
try:
|
try:
|
||||||
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in notification handler")
|
logger.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
def _handle_request(self, request):
|
def _handle_request(self, request):
|
||||||
method = self._methods.get(request.method)
|
method = self._methods.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown request: %s", request.method)
|
logger.error("Received unknown request: %s", request.method)
|
||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -192,33 +289,36 @@ class Server():
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self._send_error(request.id, Aborted())
|
self._send_error(request.id, Aborted())
|
||||||
except Exception as e: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.exception("Unexpected exception raised in plugin handler")
|
logger.exception("Unexpected exception raised in plugin handler")
|
||||||
self._send_error(request.id, UnknownError(str(e)))
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
self._task_manager.create_task(handle(), request.method)
|
self._task_manager.create_task(handle(), request.method)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_request(data):
|
def _parse_message(data):
|
||||||
try:
|
try:
|
||||||
jsonrpc_request = json.loads(data, encoding="utf-8")
|
jsonrpc_message = json.loads(data, encoding="utf-8")
|
||||||
if jsonrpc_request.get("jsonrpc") != "2.0":
|
if jsonrpc_message.get("jsonrpc") != "2.0":
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
del jsonrpc_request["jsonrpc"]
|
del jsonrpc_message["jsonrpc"]
|
||||||
return Request(**jsonrpc_request)
|
if "result" in jsonrpc_message.keys() or "error" in jsonrpc_message.keys():
|
||||||
|
return Response(**jsonrpc_message)
|
||||||
|
else:
|
||||||
|
return Request(**jsonrpc_message)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
raise ParseError()
|
raise ParseError()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data, log_level=logging.DEBUG):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
logger.log(log_level, "Sending data: %s", line)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
self._task_manager.create_task(self._writer.drain(), "drain")
|
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logger.error(str(error))
|
||||||
|
|
||||||
def _send_response(self, request_id, result):
|
def _send_response(self, request_id, result):
|
||||||
response = {
|
response = {
|
||||||
@@ -226,69 +326,50 @@ class Server():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"result": result
|
"result": result
|
||||||
}
|
}
|
||||||
self._send(response)
|
self._send(response, logging.INFO)
|
||||||
|
|
||||||
def _send_error(self, request_id, error):
|
def _send_error(self, request_id, error):
|
||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
self._send(response, logging.ERROR)
|
||||||
|
|
||||||
if error.data is not None:
|
def _send_request(self, request_id, method, params):
|
||||||
response["error"]["data"] = error.data
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": method,
|
||||||
|
"id": request_id,
|
||||||
|
"params": params
|
||||||
|
}
|
||||||
|
self._send(request, logging.NOTSET)
|
||||||
|
|
||||||
self._send(response)
|
def _send_notification(self, method, params):
|
||||||
|
|
||||||
@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():
|
|
||||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
|
||||||
self._writer = writer
|
|
||||||
self._encoder = encoder
|
|
||||||
self._methods = {}
|
|
||||||
self._task_manager = TaskManager("notification client")
|
|
||||||
|
|
||||||
def notify(self, method, params, sensitive_params=False):
|
|
||||||
"""
|
|
||||||
Send notification
|
|
||||||
|
|
||||||
:param method:
|
|
||||||
:param params:
|
|
||||||
:param sensitive_params: list of parameters that are 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, logging.NOTSET)
|
||||||
self._send(notification)
|
|
||||||
|
|
||||||
async def close(self):
|
|
||||||
await self._task_manager.wait()
|
|
||||||
|
|
||||||
def _send(self, data):
|
|
||||||
try:
|
|
||||||
line = self._encoder.encode(data)
|
|
||||||
data = (line + "\n").encode("utf-8")
|
|
||||||
logging.debug("Sending %d byte of data", len(data))
|
|
||||||
self._writer.write(data)
|
|
||||||
self._task_manager.create_task(self._writer.drain(), "drain")
|
|
||||||
except TypeError as error:
|
|
||||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log(method, params, sensitive_params):
|
def _log_request(request, sensitive_params):
|
||||||
params = anonymise_sensitive_params(params, sensitive_params)
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
logging.info("Sending notification: method=%s, params=%s", method, params)
|
if request.id is not None:
|
||||||
|
logger.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
|
else:
|
||||||
|
logger.info("Handling notification: method=%s, params=%s", request.method, params)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_response(response, sensitive_params):
|
||||||
|
result = anonymise_sensitive_params(response.result, sensitive_params)
|
||||||
|
logger.info("Handling response: id=%s, result=%s", response.id, result)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _log_error(response, error, sensitive_params):
|
||||||
|
params = error.data if error.data is not None else {}
|
||||||
|
data = anonymise_sensitive_params(params, sensitive_params)
|
||||||
|
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||||
|
response.id, error.code, error.message, data
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,16 +2,22 @@ import asyncio
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
|
||||||
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
|
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, NotificationClient, Server
|
from galaxy.api.types import (
|
||||||
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep
|
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, SynchroneousImporter
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=method-hidden
|
def default(self, o): # pylint: disable=method-hidden
|
||||||
@@ -30,7 +36,7 @@ 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."""
|
||||||
|
|
||||||
def __init__(self, platform, version, reader, writer, handshake_token):
|
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||||
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
logger.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
self._version = version
|
self._version = version
|
||||||
|
|
||||||
@@ -41,17 +47,85 @@ class Plugin:
|
|||||||
self._handshake_token = handshake_token
|
self._handshake_token = handshake_token
|
||||||
|
|
||||||
encoder = JSONEncoder()
|
encoder = JSONEncoder()
|
||||||
self._server = Server(self._reader, self._writer, encoder)
|
self._connection = Connection(self._reader, self._writer, encoder)
|
||||||
self._notification_client = NotificationClient(self._writer, encoder)
|
|
||||||
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
|
|
||||||
self._persistent_cache = dict()
|
self._persistent_cache = dict()
|
||||||
|
|
||||||
self._internal_task_manager = TaskManager("plugin internal")
|
self._internal_task_manager = TaskManager("plugin internal")
|
||||||
self._external_task_manager = TaskManager("plugin external")
|
self._external_task_manager = TaskManager("plugin external")
|
||||||
|
|
||||||
|
self._achievements_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"achievements",
|
||||||
|
self.get_unlocked_achievements,
|
||||||
|
self.prepare_achievements_context,
|
||||||
|
self._game_achievements_import_success,
|
||||||
|
self._game_achievements_import_failure,
|
||||||
|
self._achievements_import_finished,
|
||||||
|
self.achievements_import_complete
|
||||||
|
)
|
||||||
|
self._game_time_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"game times",
|
||||||
|
self.get_game_time,
|
||||||
|
self.prepare_game_times_context,
|
||||||
|
self._game_time_import_success,
|
||||||
|
self._game_time_import_failure,
|
||||||
|
self._game_times_import_finished,
|
||||||
|
self.game_times_import_complete
|
||||||
|
)
|
||||||
|
self._game_library_settings_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"game library settings",
|
||||||
|
self.get_game_library_settings,
|
||||||
|
self.prepare_game_library_settings_context,
|
||||||
|
self._game_library_settings_import_success,
|
||||||
|
self._game_library_settings_import_failure,
|
||||||
|
self._game_library_settings_import_finished,
|
||||||
|
self.game_library_settings_import_complete
|
||||||
|
)
|
||||||
|
self._os_compatibility_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"os compatibility",
|
||||||
|
self.get_os_compatibility,
|
||||||
|
self.prepare_os_compatibility_context,
|
||||||
|
self._os_compatibility_import_success,
|
||||||
|
self._os_compatibility_import_failure,
|
||||||
|
self._os_compatibility_import_finished,
|
||||||
|
self.os_compatibility_import_complete
|
||||||
|
)
|
||||||
|
self._user_presence_importer = Importer(
|
||||||
|
self._external_task_manager,
|
||||||
|
"users presence",
|
||||||
|
self.get_user_presence,
|
||||||
|
self.prepare_user_presence_context,
|
||||||
|
self._user_presence_import_success,
|
||||||
|
self._user_presence_import_failure,
|
||||||
|
self._user_presence_import_finished,
|
||||||
|
self.user_presence_import_complete
|
||||||
|
)
|
||||||
|
self._local_size_importer = SynchroneousImporter(
|
||||||
|
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)
|
||||||
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
||||||
@@ -109,6 +183,24 @@ class Plugin:
|
|||||||
self._register_method("start_game_times_import", self._start_game_times_import)
|
self._register_method("start_game_times_import", self._start_game_times_import)
|
||||||
self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
|
self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
|
||||||
|
|
||||||
|
self._register_method("start_game_library_settings_import", self._start_game_library_settings_import)
|
||||||
|
self._detect_feature(Feature.ImportGameLibrarySettings, ["get_game_library_settings"])
|
||||||
|
|
||||||
|
self._register_method("start_os_compatibility_import", self._start_os_compatibility_import)
|
||||||
|
self._detect_feature(Feature.ImportOSCompatibility, ["get_os_compatibility"])
|
||||||
|
|
||||||
|
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
||||||
|
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
|
||||||
|
|
||||||
@@ -136,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 = {
|
||||||
@@ -149,7 +242,7 @@ class Plugin:
|
|||||||
result = handler(*args, **kwargs)
|
result = handler(*args, **kwargs)
|
||||||
return wrap_result(result)
|
return wrap_result(result)
|
||||||
|
|
||||||
self._server.register_method(name, method, True, sensitive_params)
|
self._connection.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(*args, **kwargs):
|
async def method(*args, **kwargs):
|
||||||
if not internal:
|
if not internal:
|
||||||
@@ -159,38 +252,47 @@ class Plugin:
|
|||||||
result = await handler_(*args, **kwargs)
|
result = await handler_(*args, **kwargs)
|
||||||
return wrap_result(result)
|
return wrap_result(result)
|
||||||
|
|
||||||
self._server.register_method(name, method, False, sensitive_params)
|
self._connection.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, immediate=False, sensitive_params=False):
|
def _register_notification(self, name, handler, internal=False, immediate=False, sensitive_params=False):
|
||||||
if not internal and not immediate:
|
if not internal and not immediate:
|
||||||
handler = self._wrap_external_method(handler, name)
|
handler = self._wrap_external_method(handler, name)
|
||||||
self._server.register_notification(name, handler, immediate, sensitive_params)
|
self._connection.register_notification(name, handler, immediate, sensitive_params)
|
||||||
|
|
||||||
def _wrap_external_method(self, handler, name: str):
|
def _wrap_external_method(self, handler, name: str):
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
return await self._external_task_manager.create_task(handler(*args, **kwargs), name, False)
|
return await self._external_task_manager.create_task(handler(*args, **kwargs), name, False)
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""Plugin's main coroutine."""
|
"""Plugin's main coroutine."""
|
||||||
await self._server.run()
|
await self._connection.run()
|
||||||
await self._external_task_manager.wait()
|
logger.debug("Plugin run loop finished")
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if not self._active:
|
if not self._active:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info("Closing plugin")
|
logger.info("Closing plugin")
|
||||||
self._server.close()
|
self._connection.close()
|
||||||
self._external_task_manager.cancel()
|
self._external_task_manager.cancel()
|
||||||
self._internal_task_manager.create_task(self.shutdown(), "shutdown")
|
|
||||||
|
async def shutdown():
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.shutdown(), 30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logging.warning("Plugin shutdown timed out")
|
||||||
|
|
||||||
|
self._internal_task_manager.create_task(shutdown(), "shutdown")
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
async def wait_closed(self) -> None:
|
async def wait_closed(self) -> None:
|
||||||
|
logger.debug("Waiting for plugin to close")
|
||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
await self._server.wait_closed()
|
await self._connection.wait_closed()
|
||||||
await self._notification_client.close()
|
logger.info("Plugin closed")
|
||||||
|
|
||||||
def create_task(self, coro, description):
|
def create_task(self, coro, description):
|
||||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
@@ -201,11 +303,11 @@ class Plugin:
|
|||||||
try:
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in plugin tick")
|
logger.exception("Unexpected exception raised in plugin tick")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
async def _shutdown(self):
|
async def _shutdown(self):
|
||||||
logging.info("Shutting down")
|
logger.info("Shutting down")
|
||||||
self.close()
|
self.close()
|
||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
@@ -222,7 +324,7 @@ class Plugin:
|
|||||||
try:
|
try:
|
||||||
self.handshake_complete()
|
self.handshake_complete()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unhandled exception during `handshake_complete` step")
|
logger.exception("Unhandled exception during `handshake_complete` step")
|
||||||
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -253,9 +355,9 @@ class Plugin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
# temporary solution for persistent_cache vs credentials issue
|
# temporary solution for persistent_cache vs credentials issue
|
||||||
self.persistent_cache['credentials'] = credentials # type: ignore
|
self.persistent_cache["credentials"] = credentials # type: ignore
|
||||||
|
|
||||||
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
self._connection.send_notification("store_credentials", credentials, sensitive_params=True)
|
||||||
|
|
||||||
def add_game(self, game: Game) -> None:
|
def add_game(self, game: Game) -> None:
|
||||||
"""Notify the client to add game to the list of owned games
|
"""Notify the client to add game to the list of owned games
|
||||||
@@ -277,7 +379,7 @@ class Plugin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
params = {"owned_game": game}
|
params = {"owned_game": game}
|
||||||
self._notification_client.notify("owned_game_added", params)
|
self._connection.send_notification("owned_game_added", params)
|
||||||
|
|
||||||
def remove_game(self, game_id: str) -> None:
|
def remove_game(self, game_id: str) -> None:
|
||||||
"""Notify the client to remove game from the list of owned games
|
"""Notify the client to remove game from the list of owned games
|
||||||
@@ -299,7 +401,7 @@ class Plugin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
params = {"game_id": game_id}
|
params = {"game_id": game_id}
|
||||||
self._notification_client.notify("owned_game_removed", params)
|
self._connection.send_notification("owned_game_removed", params)
|
||||||
|
|
||||||
def update_game(self, game: Game) -> None:
|
def update_game(self, game: Game) -> None:
|
||||||
"""Notify the client to update the status of a game
|
"""Notify the client to update the status of a game
|
||||||
@@ -308,7 +410,7 @@ class Plugin:
|
|||||||
:param game: Game to update
|
:param game: Game to update
|
||||||
"""
|
"""
|
||||||
params = {"owned_game": game}
|
params = {"owned_game": game}
|
||||||
self._notification_client.notify("owned_game_updated", params)
|
self._connection.send_notification("owned_game_updated", params)
|
||||||
|
|
||||||
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
|
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
|
||||||
"""Notify the client to unlock an achievement for a specific game.
|
"""Notify the client to unlock an achievement for a specific game.
|
||||||
@@ -320,27 +422,24 @@ class Plugin:
|
|||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"achievement": achievement
|
"achievement": achievement
|
||||||
}
|
}
|
||||||
self._notification_client.notify("achievement_unlocked", params)
|
self._connection.send_notification("achievement_unlocked", params)
|
||||||
|
|
||||||
def _game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None:
|
def _game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None:
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"unlocked_achievements": achievements
|
"unlocked_achievements": achievements
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_success", params)
|
self._connection.send_notification("game_achievements_import_success", params)
|
||||||
|
|
||||||
def _game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
def _game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_failure", params)
|
self._connection.send_notification("game_achievements_import_failure", params)
|
||||||
|
|
||||||
def _achievements_import_finished(self) -> None:
|
def _achievements_import_finished(self) -> None:
|
||||||
self._notification_client.notify("achievements_import_finished", None)
|
self._connection.send_notification("achievements_import_finished", None)
|
||||||
|
|
||||||
def update_local_game_status(self, local_game: LocalGame) -> None:
|
def update_local_game_status(self, local_game: LocalGame) -> None:
|
||||||
"""Notify the client to update the status of a local game.
|
"""Notify the client to update the status of a local game.
|
||||||
@@ -366,15 +465,15 @@ class Plugin:
|
|||||||
self._check_statuses_task = asyncio.create_task(self._check_statuses())
|
self._check_statuses_task = asyncio.create_task(self._check_statuses())
|
||||||
"""
|
"""
|
||||||
params = {"local_game": local_game}
|
params = {"local_game": local_game}
|
||||||
self._notification_client.notify("local_game_status_changed", params)
|
self._connection.send_notification("local_game_status_changed", params)
|
||||||
|
|
||||||
def add_friend(self, user: FriendInfo) -> None:
|
def add_friend(self, user: UserInfo) -> None:
|
||||||
"""Notify the client to add a user to friends list of the currently authenticated user.
|
"""Notify the client to add a user to friends list of the currently authenticated user.
|
||||||
|
|
||||||
:param user: FriendInfo of a user that the client will add to friends list
|
:param user: UserInfo of a user that the client will add to friends list
|
||||||
"""
|
"""
|
||||||
params = {"friend_info": user}
|
params = {"friend_info": user}
|
||||||
self._notification_client.notify("friend_added", params)
|
self._connection.send_notification("friend_added", params)
|
||||||
|
|
||||||
def remove_friend(self, user_id: str) -> None:
|
def remove_friend(self, user_id: str) -> None:
|
||||||
"""Notify the client to remove a user from friends list of the currently authenticated user.
|
"""Notify the client to remove a user from friends list of the currently authenticated user.
|
||||||
@@ -382,7 +481,14 @@ class Plugin:
|
|||||||
:param user_id: id of the user to remove from friends list
|
:param user_id: id of the user to remove from friends list
|
||||||
"""
|
"""
|
||||||
params = {"user_id": user_id}
|
params = {"user_id": user_id}
|
||||||
self._notification_client.notify("friend_removed", params)
|
self._connection.send_notification("friend_removed", params)
|
||||||
|
|
||||||
|
def update_friend_info(self, user: UserInfo) -> None:
|
||||||
|
"""Notify the client about the updated friend information.
|
||||||
|
|
||||||
|
:param user: UserInfo of a friend whose info was updated
|
||||||
|
"""
|
||||||
|
self._connection.send_notification("friend_updated", params={"friend_info": user})
|
||||||
|
|
||||||
def update_game_time(self, game_time: GameTime) -> None:
|
def update_game_time(self, game_time: GameTime) -> None:
|
||||||
"""Notify the client to update game time for a game.
|
"""Notify the client to update game time for a game.
|
||||||
@@ -390,40 +496,161 @@ class Plugin:
|
|||||||
:param game_time: game time to update
|
:param game_time: game time to update
|
||||||
"""
|
"""
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_updated", params)
|
self._connection.send_notification("game_time_updated", params)
|
||||||
|
|
||||||
def _game_time_import_success(self, game_time: GameTime) -> None:
|
def update_user_presence(self, user_id: str, user_presence: UserPresence) -> None:
|
||||||
|
"""Notify the client about the updated user presence information.
|
||||||
|
|
||||||
|
:param user_id: the id of the user whose presence information is updated
|
||||||
|
:param user_presence: presence information of the specified user
|
||||||
|
"""
|
||||||
|
self._connection.send_notification(
|
||||||
|
"user_presence_updated",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"presence": user_presence
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _game_time_import_success(self, game_id: str, game_time: GameTime) -> None:
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_import_success", params)
|
self._connection.send_notification("game_time_import_success", params)
|
||||||
|
|
||||||
def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_time_import_failure", params)
|
self._connection.send_notification("game_time_import_failure", params)
|
||||||
|
|
||||||
def _game_times_import_finished(self) -> None:
|
def _game_times_import_finished(self) -> None:
|
||||||
self._notification_client.notify("game_times_import_finished", None)
|
self._connection.send_notification("game_times_import_finished", None)
|
||||||
|
|
||||||
|
def _game_library_settings_import_success(self, game_id: str, game_library_settings: GameLibrarySettings) -> None:
|
||||||
|
params = {"game_library_settings": game_library_settings}
|
||||||
|
self._connection.send_notification("game_library_settings_import_success", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
self._connection.send_notification("game_library_settings_import_failure", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_finished(self) -> None:
|
||||||
|
self._connection.send_notification("game_library_settings_import_finished", None)
|
||||||
|
|
||||||
|
def _os_compatibility_import_success(self, game_id: str, os_compatibility: Optional[OSCompatibility]) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"os_compatibility_import_success",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"os_compatibility": os_compatibility
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _os_compatibility_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"os_compatibility_import_failure",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _os_compatibility_import_finished(self) -> None:
|
||||||
|
self._connection.send_notification("os_compatibility_import_finished", None)
|
||||||
|
|
||||||
|
def _user_presence_import_success(self, user_id: str, user_presence: UserPresence) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"user_presence_import_success",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"presence": user_presence
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _user_presence_import_failure(self, user_id: str, error: ApplicationError) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"user_presence_import_failure",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _user_presence_import_finished(self) -> 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.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify("authentication_lost", None)
|
self._connection.send_notification("authentication_lost", None)
|
||||||
|
|
||||||
def push_cache(self) -> None:
|
def push_cache(self) -> None:
|
||||||
"""Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
|
"""Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify(
|
self._connection.send_notification(
|
||||||
"push_cache",
|
"push_cache",
|
||||||
params={"data": self._persistent_cache},
|
params={"data": self._persistent_cache},
|
||||||
sensitive_params="data"
|
sensitive_params="data"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def refresh_credentials(self, params: Dict[str, Any], sensitive_params) -> Dict[str, Any]:
|
||||||
|
return await self._connection.send_request("refresh_credentials", params, sensitive_params)
|
||||||
|
|
||||||
# handlers
|
# handlers
|
||||||
def handshake_complete(self) -> None:
|
def handshake_complete(self) -> None:
|
||||||
"""This method is called right after the handshake with the GOG Galaxy Client is complete and
|
"""This method is called right after the handshake with the GOG Galaxy Client is complete and
|
||||||
@@ -465,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:
|
||||||
@@ -487,11 +714,12 @@ 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 galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
|
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
||||||
|
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.
|
||||||
This method should either return galaxy.api.types.Authentication if the authentication is finished
|
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
|
||||||
or galaxy.api.types.NextStep if it requires going to another cef url.
|
or :class:`~galaxy.api.types.NextStep` if it requires going to another cef url.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param step: deprecated.
|
:param step: deprecated.
|
||||||
@@ -536,36 +764,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
||||||
if self._achievements_import_in_progress:
|
await self._achievements_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_achievements_context(game_ids)
|
|
||||||
|
|
||||||
async def import_game_achievements(game_id, context_):
|
|
||||||
try:
|
|
||||||
achievements = await self.get_unlocked_achievements(game_id, context_)
|
|
||||||
self._game_achievements_import_success(game_id, achievements)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._game_achievements_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_game_achievements")
|
|
||||||
self._game_achievements_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_games_achievements(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
imports = [import_game_achievements(game_id, context_) for game_id in game_ids_]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
finally:
|
|
||||||
self._achievements_import_finished()
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
self.achievements_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_games_achievements(game_ids, context),
|
|
||||||
"unlocked achievements import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._achievements_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
|
async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_unlocked_achievements.
|
"""Override this method to prepare context for get_unlocked_achievements.
|
||||||
@@ -679,7 +878,7 @@ class Plugin:
|
|||||||
This method is called by the GOG Galaxy Client."""
|
This method is called by the GOG Galaxy Client."""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_friends(self) -> List[FriendInfo]:
|
async def get_friends(self) -> List[UserInfo]:
|
||||||
"""Override this method to return the friends list
|
"""Override this method to return the friends list
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
@@ -700,36 +899,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
||||||
if self._game_times_import_in_progress:
|
await self._game_time_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_game_times_context(game_ids)
|
|
||||||
|
|
||||||
async def import_game_time(game_id, context_):
|
|
||||||
try:
|
|
||||||
game_time = await self.get_game_time(game_id, context_)
|
|
||||||
self._game_time_import_success(game_time)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._game_time_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_game_time")
|
|
||||||
self._game_time_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_game_times(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
imports = [import_game_time(game_id, context_) for game_id in game_ids_]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
finally:
|
|
||||||
self._game_times_import_finished()
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
self.game_times_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_game_times(game_ids, context),
|
|
||||||
"game times import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._game_times_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_game_times_context(self, game_ids: List[str]) -> Any:
|
async def prepare_game_times_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_game_time.
|
"""Override this method to prepare context for get_game_time.
|
||||||
@@ -757,6 +927,162 @@ class Plugin:
|
|||||||
(like updating cache).
|
(like updating cache).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
||||||
|
await self._game_library_settings_importer.start(game_ids)
|
||||||
|
|
||||||
|
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_game_library_settings.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which game library settings are imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_game_library_settings(self, game_id: str, context: Any) -> GameLibrarySettings:
|
||||||
|
"""Override this method to return the game library settings for the game
|
||||||
|
identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game library settings are imported
|
||||||
|
:param context: the value returned from :meth:`prepare_game_library_settings_context`
|
||||||
|
:return: GameLibrarySettings object
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def game_library_settings_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after game library settings import is finished
|
||||||
|
(like updating cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
||||||
|
await self._os_compatibility_importer.start(game_ids)
|
||||||
|
|
||||||
|
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for get_os_compatibility.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which game os compatibility is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_os_compatibility(self, game_id: str, context: Any) -> Optional[OSCompatibility]:
|
||||||
|
"""Override this method to return the OS compatibility for the game with the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game os compatibility is imported
|
||||||
|
:param context: the value returned from :meth:`prepare_os_compatibility_context`
|
||||||
|
:return: OSCompatibility flags indicating compatible OSs, or None if compatibility is not know
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def os_compatibility_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
||||||
|
|
||||||
|
async def _start_user_presence_import(self, user_id_list: List[str]) -> None:
|
||||||
|
await self._user_presence_importer.start(user_id_list)
|
||||||
|
|
||||||
|
async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for :meth:`get_user_presence`.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param user_id_list: the ids of the users for whom presence information is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_user_presence(self, user_id: str, context: Any) -> UserPresence:
|
||||||
|
"""Override this method to return presence information for the user with the provided user_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param user_id: the id of the user for whom presence information is imported
|
||||||
|
:param context: the value returned from :meth:`prepare_user_presence_context`
|
||||||
|
:return: UserPresence presence information of the provided user
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def user_presence_import_complete(self) -> None:
|
||||||
|
"""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 game_id: the id of the installed game
|
||||||
|
:param context: the value returned from :meth:`prepare_local_size_context`
|
||||||
|
:return: the size of the game on a user-owned storage device (in bytes) or `None` if the size cannot be determined
|
||||||
|
"""
|
||||||
|
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.
|
||||||
@@ -776,7 +1102,7 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
main()
|
main()
|
||||||
"""
|
"""
|
||||||
if len(argv) < 3:
|
if len(argv) < 3:
|
||||||
logging.critical("Not enough parameters, required: token, port")
|
logger.critical("Not enough parameters, required: token, port")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
token = argv[1]
|
token = argv[1]
|
||||||
@@ -784,23 +1110,30 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
try:
|
try:
|
||||||
port = int(argv[2])
|
port = int(argv[2])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.critical("Failed to parse port value: %s", argv[2])
|
logger.critical("Failed to parse port value: %s", argv[2])
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
if not (1 <= port <= 65535):
|
if not (1 <= port <= 65535):
|
||||||
logging.critical("Port value out of range (1, 65535)")
|
logger.critical("Port value out of range (1, 65535)")
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
if not issubclass(plugin_class, Plugin):
|
if not issubclass(plugin_class, Plugin):
|
||||||
logging.critical("plugin_class must be subclass of Plugin")
|
logger.critical("plugin_class must be subclass of Plugin")
|
||||||
sys.exit(4)
|
sys.exit(4)
|
||||||
|
|
||||||
async def coroutine():
|
async def coroutine():
|
||||||
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||||
extra_info = writer.get_extra_info("sockname")
|
try:
|
||||||
logging.info("Using local address: %s:%u", *extra_info)
|
extra_info = writer.get_extra_info("sockname")
|
||||||
async with plugin_class(reader, writer, token) as plugin:
|
logger.info("Using local address: %s:%u", *extra_info)
|
||||||
await plugin.run()
|
async with plugin_class(reader, writer, token) as plugin:
|
||||||
|
await plugin.run()
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
except (ConnectionAbortedError, ConnectionResetError):
|
||||||
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -808,5 +1141,5 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
|
|
||||||
asyncio.run(coroutine())
|
asyncio.run(coroutine())
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Error while running plugin")
|
logger.exception("Error while running plugin")
|
||||||
sys.exit(5)
|
sys.exit(5)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState, SubscriptionDiscovery
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authentication():
|
class Authentication:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
|
||||||
to inform the client that authentication has successfully finished.
|
to inform the client that authentication has successfully finished.
|
||||||
|
|
||||||
@@ -14,8 +15,9 @@ class Authentication():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Cookie():
|
class Cookie:
|
||||||
"""Cookie
|
"""Cookie
|
||||||
|
|
||||||
:param name: name of the cookie
|
:param name: name of the cookie
|
||||||
@@ -28,9 +30,10 @@ class Cookie():
|
|||||||
domain: Optional[str] = None
|
domain: Optional[str] = None
|
||||||
path: Optional[str] = None
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class NextStep():
|
class NextStep:
|
||||||
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
R"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
@@ -58,17 +61,20 @@ class NextStep():
|
|||||||
if not stored_credentials:
|
if not stored_credentials:
|
||||||
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`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :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`}
|
||||||
: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 on every document at given step of internal browser authentication.
|
: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.
|
||||||
"""
|
"""
|
||||||
next_step: str
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
cookies: Optional[List[Cookie]] = None
|
cookies: Optional[List[Cookie]] = None
|
||||||
js: Optional[Dict[str, List[str]]] = None
|
js: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LicenseInfo():
|
class LicenseInfo:
|
||||||
"""Information about the license of related product.
|
"""Information about the license of related product.
|
||||||
|
|
||||||
:param license_type: type of license
|
:param license_type: type of license
|
||||||
@@ -77,8 +83,9 @@ class LicenseInfo():
|
|||||||
license_type: LicenseType
|
license_type: LicenseType
|
||||||
owner: Optional[str] = None
|
owner: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Dlc():
|
class Dlc:
|
||||||
"""Downloadable content object.
|
"""Downloadable content object.
|
||||||
|
|
||||||
:param dlc_id: id of the dlc
|
:param dlc_id: id of the dlc
|
||||||
@@ -89,8 +96,9 @@ class Dlc():
|
|||||||
dlc_title: str
|
dlc_title: str
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Game():
|
class Game:
|
||||||
"""Game object.
|
"""Game object.
|
||||||
|
|
||||||
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
|
||||||
@@ -103,8 +111,9 @@ class Game():
|
|||||||
dlcs: Optional[List[Dlc]]
|
dlcs: Optional[List[Dlc]]
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Achievement():
|
class Achievement:
|
||||||
"""Achievement, has to be initialized with either id or name.
|
"""Achievement, has to be initialized with either id or name.
|
||||||
|
|
||||||
:param unlock_time: unlock time of the achievement
|
:param unlock_time: unlock time of the achievement
|
||||||
@@ -119,8 +128,9 @@ class Achievement():
|
|||||||
assert self.achievement_id or self.achievement_name, \
|
assert self.achievement_id or self.achievement_name, \
|
||||||
"One of achievement_id or achievement_name is required"
|
"One of achievement_id or achievement_name is required"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalGame():
|
class LocalGame:
|
||||||
"""Game locally present on the authenticated user's computer.
|
"""Game locally present on the authenticated user's computer.
|
||||||
|
|
||||||
:param game_id: id of the game
|
:param game_id: id of the game
|
||||||
@@ -129,9 +139,14 @@ class LocalGame():
|
|||||||
game_id: str
|
game_id: str
|
||||||
local_game_state: LocalGameState
|
local_game_state: LocalGameState
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FriendInfo():
|
class FriendInfo:
|
||||||
"""Information about a friend of the currently authenticated user.
|
"""
|
||||||
|
.. deprecated:: 0.56
|
||||||
|
Use :class:`UserInfo`.
|
||||||
|
|
||||||
|
Information about a friend of the currently authenticated user.
|
||||||
|
|
||||||
:param user_id: id of the user
|
:param user_id: id of the user
|
||||||
:param user_name: username of the user
|
:param user_name: username of the user
|
||||||
@@ -139,15 +154,104 @@ class FriendInfo():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GameTime():
|
class UserInfo:
|
||||||
|
"""Information about a user of related user.
|
||||||
|
|
||||||
|
:param user_id: id of the user
|
||||||
|
:param user_name: username of the user
|
||||||
|
:param avatar_url: the URL of the user avatar
|
||||||
|
:param profile_url: the URL of the user profile
|
||||||
|
"""
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
avatar_url: Optional[str] = None
|
||||||
|
profile_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameTime:
|
||||||
"""Game time of a game, defines the total time spent in the game
|
"""Game time of a game, defines the total time spent in the game
|
||||||
and the last time the game was played.
|
and the last time the game was played.
|
||||||
|
|
||||||
:param game_id: id of the related game
|
:param game_id: id of the related game
|
||||||
:param time_played: the total time spent in the game in **minutes**
|
:param time_played: the total time spent in the game in **minutes**
|
||||||
:param last_time_played: last time the game was played (**unix timestamp**)
|
:param last_played_time: last time the game was played (**unix timestamp**)
|
||||||
"""
|
"""
|
||||||
game_id: str
|
game_id: str
|
||||||
time_played: Optional[int]
|
time_played: Optional[int]
|
||||||
last_played_time: Optional[int]
|
last_played_time: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameLibrarySettings:
|
||||||
|
"""Library settings of a game, defines assigned tags and visibility flag.
|
||||||
|
|
||||||
|
:param game_id: id of the related game
|
||||||
|
:param tags: collection of tags assigned to the game
|
||||||
|
:param hidden: indicates if the game should be hidden in GOG Galaxy client
|
||||||
|
"""
|
||||||
|
game_id: str
|
||||||
|
tags: Optional[List[str]]
|
||||||
|
hidden: Optional[bool]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class UserPresence:
|
||||||
|
"""Presence information of a user.
|
||||||
|
|
||||||
|
The GOG Galaxy client will prefer to generate user status basing on `game_id` (or `game_title`)
|
||||||
|
and `in_game_status` fields but if plugin is not capable of delivering it then the `full_status` will be used if
|
||||||
|
available
|
||||||
|
|
||||||
|
:param presence_state: the state of the user
|
||||||
|
:param game_id: id of the game a user is currently in
|
||||||
|
:param game_title: name of the game a user is currently in
|
||||||
|
:param in_game_status: status set by the game itself e.x. "In Main Menu"
|
||||||
|
:param full_status: full user status e.x. "Playing <title_name>: <in_game_status>"
|
||||||
|
"""
|
||||||
|
presence_state: PresenceState
|
||||||
|
game_id: Optional[str] = None
|
||||||
|
game_title: Optional[str] = None
|
||||||
|
in_game_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,12 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
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
|
||||||
|
|
||||||
import logging
|
|
||||||
from galaxy.http import create_client_session, handle_exception
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
class BackendClient:
|
class BackendClient:
|
||||||
@@ -44,6 +43,8 @@ from galaxy.api.errors import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#: Default limit of the simultaneous connections for ssl connector.
|
#: Default limit of the simultaneous connections for ssl connector.
|
||||||
DEFAULT_LIMIT = 20
|
DEFAULT_LIMIT = 20
|
||||||
#: Default timeout in seconds used for client session.
|
#: Default timeout in seconds used for client session.
|
||||||
@@ -70,7 +71,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>`_
|
||||||
"""
|
"""
|
||||||
@@ -78,16 +79,17 @@ def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
|||||||
ssl_context.load_verify_locations(certifi.where())
|
ssl_context.load_verify_locations(certifi.where())
|
||||||
kwargs.setdefault("ssl", ssl_context)
|
kwargs.setdefault("ssl", ssl_context)
|
||||||
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
||||||
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore due to https://github.com/python/mypy/issues/4001
|
# due to https://github.com/python/mypy/issues/4001
|
||||||
|
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -103,7 +105,8 @@ def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
|||||||
kwargs.setdefault("connector", create_tcp_connector())
|
kwargs.setdefault("connector", create_tcp_connector())
|
||||||
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
||||||
kwargs.setdefault("raise_for_status", True)
|
kwargs.setdefault("raise_for_status", True)
|
||||||
return aiohttp.ClientSession(*args, **kwargs) # type: ignore due to https://github.com/python/mypy/issues/4001
|
# due to https://github.com/python/mypy/issues/4001
|
||||||
|
return aiohttp.ClientSession(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -120,25 +123,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:
|
||||||
logging.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:
|
||||||
logging.exception("Caught exception while performing request")
|
logger.exception("Caught exception while performing request")
|
||||||
raise UnknownError()
|
raise UnknownError(repr(e))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
|||||||
from typing import Iterable, NewType, Optional, List, cast
|
from typing import Iterable, NewType, Optional, List, cast
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ProcessId = NewType("ProcessId", int)
|
ProcessId = NewType("ProcessId", int)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class StreamLineReader:
|
|||||||
while True:
|
while True:
|
||||||
# check if there is no unprocessed data in the buffer
|
# check if there is no unprocessed data in the buffer
|
||||||
if not self._buffer or self._processed_buffer_it != 0:
|
if not self._buffer or self._processed_buffer_it != 0:
|
||||||
chunk = await self._reader.read(1024)
|
chunk = await self._reader.read(1024*1024)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
return bytes() # EOF
|
return bytes() # EOF
|
||||||
self._buffer += chunk
|
self._buffer += chunk
|
||||||
|
|||||||
99
src/galaxy/registry_monitor.py
Normal file
99
src/galaxy/registry_monitor.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import logging
|
||||||
|
import ctypes
|
||||||
|
from ctypes.wintypes import LONG, HKEY, LPCWSTR, DWORD, BOOL, HANDLE, LPVOID
|
||||||
|
|
||||||
|
LPSECURITY_ATTRIBUTES = LPVOID
|
||||||
|
|
||||||
|
RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
|
||||||
|
RegOpenKeyEx.restype = LONG
|
||||||
|
RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
|
||||||
|
|
||||||
|
RegCloseKey = ctypes.windll.advapi32.RegCloseKey
|
||||||
|
RegCloseKey.restype = LONG
|
||||||
|
RegCloseKey.argtypes = [HKEY]
|
||||||
|
|
||||||
|
RegNotifyChangeKeyValue = ctypes.windll.advapi32.RegNotifyChangeKeyValue
|
||||||
|
RegNotifyChangeKeyValue.restype = LONG
|
||||||
|
RegNotifyChangeKeyValue.argtypes = [HKEY, BOOL, DWORD, HANDLE, BOOL]
|
||||||
|
|
||||||
|
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||||
|
CloseHandle.restype = BOOL
|
||||||
|
CloseHandle.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
CreateEvent = ctypes.windll.kernel32.CreateEventW
|
||||||
|
CreateEvent.restype = BOOL
|
||||||
|
CreateEvent.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||||
|
|
||||||
|
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
|
||||||
|
WaitForSingleObject.restype = DWORD
|
||||||
|
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0x00000000
|
||||||
|
|
||||||
|
KEY_READ = 0x00020019
|
||||||
|
KEY_QUERY_VALUE = 0x00000001
|
||||||
|
|
||||||
|
REG_NOTIFY_CHANGE_NAME = 0x00000001
|
||||||
|
REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
|
||||||
|
|
||||||
|
WAIT_OBJECT_0 = 0x00000000
|
||||||
|
WAIT_TIMEOUT = 0x00000102
|
||||||
|
|
||||||
|
class RegistryMonitor:
|
||||||
|
|
||||||
|
def __init__(self, root, subkey):
|
||||||
|
self._root = root
|
||||||
|
self._subkey = subkey
|
||||||
|
self._event = CreateEvent(None, False, False, None)
|
||||||
|
|
||||||
|
self._key = None
|
||||||
|
self._open_key()
|
||||||
|
if self._key:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
CloseHandle(self._event)
|
||||||
|
if self._key:
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def is_updated(self):
|
||||||
|
wait_result = WaitForSingleObject(self._event, 0)
|
||||||
|
|
||||||
|
# previously watched
|
||||||
|
if wait_result == WAIT_OBJECT_0:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# no changes or no key before
|
||||||
|
if wait_result != WAIT_TIMEOUT:
|
||||||
|
# unexpected error
|
||||||
|
logging.warning("Unexpected WaitForSingleObject result %s", wait_result)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._key is None:
|
||||||
|
self._open_key()
|
||||||
|
|
||||||
|
if self._key is not None:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _set_key_update_notification(self):
|
||||||
|
filter_ = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
|
||||||
|
status = RegNotifyChangeKeyValue(self._key, True, filter_, self._event, True)
|
||||||
|
if status != ERROR_SUCCESS:
|
||||||
|
# key was deleted
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def _open_key(self):
|
||||||
|
access = KEY_QUERY_VALUE | KEY_READ
|
||||||
|
self._key = HKEY()
|
||||||
|
rc = RegOpenKeyEx(self._root, self._subkey, 0, access, ctypes.byref(self._key))
|
||||||
|
if rc != ERROR_SUCCESS:
|
||||||
|
self._key = None
|
||||||
@@ -3,6 +3,10 @@ import logging
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskManager:
|
class TaskManager:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self._name = name
|
self._name = name
|
||||||
@@ -15,23 +19,23 @@ class TaskManager:
|
|||||||
async def task_wrapper(task_id):
|
async def task_wrapper(task_id):
|
||||||
try:
|
try:
|
||||||
result = await coro
|
result = await coro
|
||||||
logging.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||||
return result
|
return result
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
if handle_exceptions:
|
if handle_exceptions:
|
||||||
logging.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
if handle_exceptions:
|
if handle_exceptions:
|
||||||
logging.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
logger.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
del self._tasks[task_id]
|
del self._tasks[task_id]
|
||||||
|
|
||||||
task_id = next(self._task_counter)
|
task_id = next(self._task_counter)
|
||||||
logging.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
||||||
task = asyncio.create_task(task_wrapper(task_id))
|
task = asyncio.create_task(task_wrapper(task_id))
|
||||||
self._tasks[task_id] = task
|
self._tasks[task_id] = task
|
||||||
return task
|
return task
|
||||||
|
|||||||
@@ -21,11 +21,19 @@ def coroutine_mock():
|
|||||||
corofunc.coro = coro
|
corofunc.coro = coro
|
||||||
return corofunc
|
return corofunc
|
||||||
|
|
||||||
|
|
||||||
async def skip_loop(iterations=1):
|
async def skip_loop(iterations=1):
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
async def async_return_value(return_value, loop_iterations_delay=0):
|
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||||
await skip_loop(loop_iterations_delay)
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
|
async def async_raise(error, loop_iterations_delay=0):
|
||||||
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
raise error
|
||||||
|
|||||||
@@ -1,33 +1,38 @@
|
|||||||
from contextlib import ExitStack
|
|
||||||
import logging
|
import logging
|
||||||
from unittest.mock import patch, MagicMock
|
from contextlib import ExitStack
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def reader():
|
def reader():
|
||||||
stream = MagicMock(name="stream_reader")
|
stream = MagicMock(name="stream_reader")
|
||||||
stream.read = MagicMock()
|
stream.read = MagicMock()
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
async def writer():
|
async def writer():
|
||||||
stream = MagicMock(name="stream_writer")
|
stream = MagicMock(name="stream_writer")
|
||||||
stream.drain.side_effect = lambda: async_return_value(None)
|
stream.drain.side_effect = lambda: async_return_value(None)
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def read(reader):
|
def read(reader):
|
||||||
yield reader.read
|
yield reader.read
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def write(writer):
|
def write(writer):
|
||||||
yield writer.write
|
yield writer.write
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
async def plugin(reader, writer):
|
async def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
@@ -49,7 +54,23 @@ async def plugin(reader, writer):
|
|||||||
"game_times_import_complete",
|
"game_times_import_complete",
|
||||||
"shutdown_platform_client",
|
"shutdown_platform_client",
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"tick"
|
"tick",
|
||||||
|
"get_game_library_settings",
|
||||||
|
"prepare_game_library_settings_context",
|
||||||
|
"game_library_settings_import_complete",
|
||||||
|
"get_os_compatibility",
|
||||||
|
"prepare_os_compatibility_context",
|
||||||
|
"os_compatibility_import_complete",
|
||||||
|
"get_user_presence",
|
||||||
|
"prepare_user_presence_context",
|
||||||
|
"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:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from pytest import raises
|
|||||||
|
|
||||||
from galaxy.api.types import Achievement
|
from galaxy.api.types import Achievement
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -79,11 +79,11 @@ async def test_get_unlocked_achievements_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write):
|
async def test_get_unlocked_achievements_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -113,7 +113,8 @@ async def test_get_unlocked_achievements_error(exception, code, message, plugin,
|
|||||||
"game_id": "14",
|
"game_id": "14",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,7 +136,7 @@ async def test_prepare_get_unlocked_achievements_context_error(plugin, read, wri
|
|||||||
"game_ids": ["14"]
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
@@ -145,7 +146,8 @@ async def test_prepare_get_unlocked_achievements_context_error(plugin, read, wri
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -176,7 +178,7 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
async_return_value(create_message(requests[0])),
|
||||||
async_return_value(create_message(requests[1])),
|
async_return_value(create_message(requests[1])),
|
||||||
async_return_value(b"")
|
async_return_value(b"", 10)
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
@@ -192,7 +194,8 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in messages
|
} in messages
|
||||||
|
|
||||||
@@ -201,6 +204,7 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
async def test_unlock_achievement(plugin, write):
|
async def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
plugin.unlock_achievement("14", achievement)
|
plugin.unlock_achievement("14", achievement)
|
||||||
|
await skip_loop()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
assert response == {
|
assert response == {
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import pytest
|
|||||||
|
|
||||||
from galaxy.api.types import Authentication
|
from galaxy.api.types import Authentication
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
UnknownError,
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
BackendNotAvailable,
|
||||||
|
BackendTimeout,
|
||||||
|
BackendError,
|
||||||
|
InvalidCredentials,
|
||||||
|
NetworkError,
|
||||||
|
ProtocolError,
|
||||||
|
TemporaryBlocked,
|
||||||
|
Banned,
|
||||||
|
AccessDenied,
|
||||||
)
|
)
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -17,7 +25,7 @@ async def test_success(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
@@ -35,27 +43,26 @@ async def test_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
@pytest.mark.parametrize("error,code,message, internal_type", [
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError"),
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
pytest.param(BackendNotAvailable, 2, "Backend not available", "BackendNotAvailable"),
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
pytest.param(BackendTimeout, 3, "Backend timed out", "BackendTimeout"),
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
pytest.param(BackendError, 4, "Backend error", "BackendError"),
|
||||||
pytest.param(InvalidCredentials, 100, "Invalid credentials", id="invalid_credentials"),
|
pytest.param(InvalidCredentials, 100, "Invalid credentials", "InvalidCredentials"),
|
||||||
pytest.param(NetworkError, 101, "Network error", id="network_error"),
|
pytest.param(NetworkError, 101, "Network error", "NetworkError"),
|
||||||
pytest.param(LoggedInElsewhere, 102, "Logged in elsewhere", id="logged_elsewhere"),
|
pytest.param(ProtocolError, 103, "Protocol error", "ProtocolError"),
|
||||||
pytest.param(ProtocolError, 103, "Protocol error", id="protocol_error"),
|
pytest.param(TemporaryBlocked, 104, "Temporary blocked", "TemporaryBlocked"),
|
||||||
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
|
pytest.param(Banned, 105, "Banned", "Banned"),
|
||||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
pytest.param(AccessDenied, 106, "Access denied", "AccessDenied"),
|
||||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
|
||||||
])
|
])
|
||||||
async def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.side_effect = error()
|
plugin.authenticate.side_effect = error()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
@@ -66,7 +73,8 @@ async def test_failure(plugin, read, write, error, code, message):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -84,7 +92,7 @@ async def test_stored_credentials(plugin, read, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
@@ -97,6 +105,7 @@ async def test_store_credentials(plugin, write):
|
|||||||
"token": "ABC"
|
"token": "ABC"
|
||||||
}
|
}
|
||||||
plugin.store_credentials(credentials)
|
plugin.store_credentials(credentials)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -110,6 +119,7 @@ async def test_store_credentials(plugin, write):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_lost_authentication(plugin, write):
|
async def test_lost_authentication(plugin, write):
|
||||||
plugin.lost_authentication()
|
plugin.lost_authentication()
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
|
|||||||
147
tests/test_errors.py
Normal file
147
tests/test_errors.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import pytest
|
||||||
|
import galaxy.api.errors as errors
|
||||||
|
import galaxy.api.jsonrpc as jsonrpc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
{"key1": "value", "key2": "value2"},
|
||||||
|
{},
|
||||||
|
{"key1": ["list", "of", "things"], "key2": None},
|
||||||
|
{"key1": ("tuple", Exception)},
|
||||||
|
])
|
||||||
|
def test_valid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
data.update({"internal_type": "JsonRpcError"})
|
||||||
|
expected_json = {"code": 1, "data": data, "message": "Test error message"}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_default_data():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message)
|
||||||
|
expected_json = {"code": test_code, "data": {"internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
123,
|
||||||
|
["not", "a", "mapping"],
|
||||||
|
"nor is this"
|
||||||
|
])
|
||||||
|
def test_invalid_error_data(data):
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
jsonrpc.JsonRpcError(code=test_code, message=test_message, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_override_internal_type():
|
||||||
|
test_message = "Test error message"
|
||||||
|
test_code = 1
|
||||||
|
test_data = {"internal_type": "SomeUserProvidedType", "details": "some more data"}
|
||||||
|
err_obj = jsonrpc.JsonRpcError(code=test_code, message=test_message, data=test_data)
|
||||||
|
expected_json = {"code": test_code, "data": {"details": "some more data", "internal_type": "JsonRpcError"}, "message": test_message}
|
||||||
|
assert err_obj.json() == expected_json
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error, expected_error_msg", [
|
||||||
|
(errors.AuthenticationRequired, "Authentication required"),
|
||||||
|
(errors.BackendNotAvailable, "Backend not available"),
|
||||||
|
(errors.BackendTimeout, "Backend timed out"),
|
||||||
|
(errors.BackendError, "Backend error"),
|
||||||
|
(errors.UnknownBackendResponse, "Backend responded in unknown way"),
|
||||||
|
(errors.TooManyRequests, "Too many requests. Try again later"),
|
||||||
|
(errors.InvalidCredentials, "Invalid credentials"),
|
||||||
|
(errors.NetworkError, "Network error"),
|
||||||
|
(errors.ProtocolError, "Protocol error"),
|
||||||
|
(errors.TemporaryBlocked, "Temporary blocked"),
|
||||||
|
(errors.Banned, "Banned"),
|
||||||
|
(errors.AccessDenied, "Access denied"),
|
||||||
|
(errors.FailedParsingManifest, "Failed parsing manifest"),
|
||||||
|
(errors.TooManyMessagesSent, "Too many messages sent"),
|
||||||
|
(errors.IncoherentLastMessage, "Different last message id on backend"),
|
||||||
|
(errors.MessageNotFound, "Message not found"),
|
||||||
|
(errors.ImportInProgress, "Import already in progress"),
|
||||||
|
(jsonrpc.UnknownError, "Unknown error"),
|
||||||
|
(jsonrpc.ParseError, "Parse error"),
|
||||||
|
(jsonrpc.InvalidRequest, "Invalid Request"),
|
||||||
|
(jsonrpc.MethodNotFound, "Method not found"),
|
||||||
|
(jsonrpc.InvalidParams, "Invalid params"),
|
||||||
|
(jsonrpc.Timeout, "Method timed out"),
|
||||||
|
(jsonrpc.Aborted, "Method aborted"),
|
||||||
|
])
|
||||||
|
def test_error_default_message(error, expected_error_msg):
|
||||||
|
error_json = error().json()
|
||||||
|
|
||||||
|
assert error_json["message"] == expected_error_msg
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_error_custom_message(error):
|
||||||
|
custom_message = "test message"
|
||||||
|
|
||||||
|
error_json = error(custom_message).json()
|
||||||
|
|
||||||
|
assert error_json["message"] == custom_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("error", [
|
||||||
|
errors.AuthenticationRequired,
|
||||||
|
errors.BackendNotAvailable,
|
||||||
|
errors.BackendTimeout,
|
||||||
|
errors.BackendError,
|
||||||
|
errors.UnknownBackendResponse,
|
||||||
|
errors.TooManyRequests,
|
||||||
|
errors.InvalidCredentials,
|
||||||
|
errors.NetworkError,
|
||||||
|
errors.ProtocolError,
|
||||||
|
errors.TemporaryBlocked,
|
||||||
|
errors.Banned,
|
||||||
|
errors.AccessDenied,
|
||||||
|
errors.FailedParsingManifest,
|
||||||
|
errors.TooManyMessagesSent,
|
||||||
|
errors.IncoherentLastMessage,
|
||||||
|
errors.MessageNotFound,
|
||||||
|
errors.ImportInProgress,
|
||||||
|
jsonrpc.UnknownError,
|
||||||
|
jsonrpc.ParseError,
|
||||||
|
jsonrpc.InvalidRequest,
|
||||||
|
jsonrpc.MethodNotFound,
|
||||||
|
jsonrpc.InvalidParams,
|
||||||
|
jsonrpc.Timeout,
|
||||||
|
jsonrpc.Aborted,
|
||||||
|
])
|
||||||
|
def test_set_arbitrary_error_message(error):
|
||||||
|
arbitrary_messages = [[], {}, (), 1, None]
|
||||||
|
|
||||||
|
for msg in arbitrary_messages:
|
||||||
|
error_json = error(msg).json()
|
||||||
|
assert error_json["message"] == str(msg)
|
||||||
@@ -14,7 +14,13 @@ def test_base_class():
|
|||||||
Feature.ImportGameTime,
|
Feature.ImportGameTime,
|
||||||
Feature.ImportFriends,
|
Feature.ImportFriends,
|
||||||
Feature.ShutdownPlatformClient,
|
Feature.ShutdownPlatformClient,
|
||||||
Feature.LaunchPlatformClient
|
Feature.LaunchPlatformClient,
|
||||||
|
Feature.ImportGameLibrarySettings,
|
||||||
|
Feature.ImportOSCompatibility,
|
||||||
|
Feature.ImportUserPresence,
|
||||||
|
Feature.ImportLocalSize,
|
||||||
|
Feature.ImportSubscriptions,
|
||||||
|
Feature.ImportSubscriptionGames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from galaxy.api.types import FriendInfo
|
from galaxy.api.types import UserInfo
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -15,10 +15,12 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.return_value = async_return_value([
|
plugin.get_friends.return_value = async_return_value([
|
||||||
FriendInfo("3", "Jan"),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
FriendInfo("5", "Ola")
|
UserInfo("5", "Ola", None, "https://profile.url/u5"),
|
||||||
|
UserInfo("6", "Ola2", None),
|
||||||
|
UserInfo("7", "Ola3"),
|
||||||
])
|
])
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
@@ -29,8 +31,10 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"friend_info_list": [
|
"friend_info_list": [
|
||||||
{"user_id": "3", "user_name": "Jan"},
|
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||||
{"user_id": "5", "user_name": "Ola"}
|
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"},
|
||||||
|
{"user_id": "6", "user_name": "Ola2"},
|
||||||
|
{"user_id": "7", "user_name": "Ola3"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +49,7 @@ async def test_get_friends_failure(plugin, read, write):
|
|||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.side_effect = UnknownError()
|
plugin.get_friends.side_effect = UnknownError()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
@@ -57,6 +61,7 @@ async def test_get_friends_failure(plugin, read, write):
|
|||||||
"error": {
|
"error": {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "Unknown error",
|
"message": "Unknown error",
|
||||||
|
"data": {"internal_type": "UnknownError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -64,16 +69,22 @@ async def test_get_friends_failure(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_friend(plugin, write):
|
async def test_add_friend(plugin, write):
|
||||||
friend = FriendInfo("7", "Kuba")
|
friend = UserInfo("7", "Kuba", avatar_url="https://avatar.url/kuba.jpg", profile_url="https://profile.url/kuba")
|
||||||
|
|
||||||
plugin.add_friend(friend)
|
plugin.add_friend(friend)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "friend_added",
|
"method": "friend_added",
|
||||||
"params": {
|
"params": {
|
||||||
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
"friend_info": {
|
||||||
|
"user_id": "7",
|
||||||
|
"user_name": "Kuba",
|
||||||
|
"avatar_url": "https://avatar.url/kuba.jpg",
|
||||||
|
"profile_url": "https://profile.url/kuba"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -82,6 +93,7 @@ async def test_add_friend(plugin, write):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_remove_friend(plugin, write):
|
async def test_remove_friend(plugin, write):
|
||||||
plugin.remove_friend("5")
|
plugin.remove_friend("5")
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -92,3 +104,26 @@ async def test_remove_friend(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_friend_info(plugin, write):
|
||||||
|
plugin.update_friend_info(
|
||||||
|
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
|
||||||
|
)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_updated",
|
||||||
|
"params": {
|
||||||
|
"friend_info": {
|
||||||
|
"user_id": "7",
|
||||||
|
"user_name": "Jakub",
|
||||||
|
"avatar_url": "https://new-avatar.url/kuba2.jpg",
|
||||||
|
"profile_url": "https://profile.url/kuba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
199
tests/test_game_library_settings.py
Normal file
199
tests/test_game_library_settings.py
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.types import GameLibrarySettings
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_library_settings_success(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = [
|
||||||
|
async_return_value(GameLibrarySettings("3", None, True)),
|
||||||
|
async_return_value(GameLibrarySettings("5", [], False)),
|
||||||
|
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_has_calls([
|
||||||
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "3",
|
||||||
|
"hidden": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "5",
|
||||||
|
"tags": [],
|
||||||
|
"hidden": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "7",
|
||||||
|
"tags": ["tag1", "tag2", "tag3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_game_library_settings_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_called()
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_game_library_settings_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_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": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ from unittest.mock import call
|
|||||||
import pytest
|
import pytest
|
||||||
from galaxy.api.types import GameTime
|
from galaxy.api.types import GameTime
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -79,11 +79,11 @@ async def test_get_game_time_success(plugin, read, write):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception,code,message", [
|
@pytest.mark.parametrize("exception,code,message, internal_type", [
|
||||||
(BackendError, 4, "Backend error"),
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
(KeyError, 0, "Unknown error")
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
])
|
])
|
||||||
async def test_get_game_time_error(exception, code, message, plugin, read, write):
|
async def test_get_game_time_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -112,7 +112,8 @@ async def test_get_game_time_error(exception, code, message, plugin, read, write
|
|||||||
"game_id": "6",
|
"game_id": "6",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,7 +136,7 @@ async def test_prepare_get_game_time_context_error(plugin, read, write):
|
|||||||
"game_ids": ["6"]
|
"game_ids": ["6"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -144,7 +145,8 @@ async def test_prepare_get_game_time_context_error(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -174,7 +176,7 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
async_return_value(create_message(requests[0])),
|
||||||
async_return_value(create_message(requests[1])),
|
async_return_value(create_message(requests[1])),
|
||||||
async_return_value(b"")
|
async_return_value(b"", 10)
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
@@ -190,7 +192,8 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 600,
|
"code": 600,
|
||||||
"message": "Import already in progress"
|
"message": "Import already in progress",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
}
|
}
|
||||||
} in messages
|
} in messages
|
||||||
|
|
||||||
@@ -199,6 +202,7 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
async def test_update_game(plugin, write):
|
async def test_update_game(plugin, write):
|
||||||
game_time = GameTime("3", 60, 1549550504)
|
game_time = GameTime("3", 60, 1549550504)
|
||||||
plugin.update_game_time(game_time)
|
plugin.update_game_time(game_time)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import pytest
|
|||||||
from galaxy.api.types import LocalGame
|
from galaxy.api.types import LocalGame
|
||||||
from galaxy.api.consts import LocalGameState
|
from galaxy.api.consts import LocalGameState
|
||||||
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
from galaxy.api.errors import UnknownError, FailedParsingManifest
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ async def test_success(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"method": "import_local_games"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
plugin.get_local_games.return_value = async_return_value([
|
plugin.get_local_games.return_value = async_return_value([
|
||||||
LocalGame("1", LocalGameState.Running),
|
LocalGame("1", LocalGameState.Running),
|
||||||
@@ -51,19 +51,19 @@ async def test_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message, internal_type",
|
||||||
[
|
[
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_failure(plugin, read, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message, internal_type):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_local_games"
|
"method": "import_local_games"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_local_games.side_effect = error()
|
plugin.get_local_games.side_effect = error()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
@@ -74,7 +74,8 @@ async def test_failure(plugin, read, write, error, code, message):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data" : {"internal_type" : internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -83,6 +84,7 @@ async def test_failure(plugin, read, write, error, code, message):
|
|||||||
async def test_local_game_state_update(plugin, write):
|
async def test_local_game_state_update(plugin, write):
|
||||||
game = LocalGame("1", LocalGameState.Running)
|
game = LocalGame("1", LocalGameState.Running)
|
||||||
plugin.update_local_game_status(game)
|
plugin.update_local_game_status(game)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
|
|||||||
197
tests/test_local_size.py
Normal file
197
tests/test_local_size.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
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, 1),
|
||||||
|
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,internal_type", [
|
||||||
|
(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_local_size_error(exception, code, message, internal_type, 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()
|
||||||
|
|
||||||
|
direct_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
}
|
||||||
|
responses = get_messages(write)
|
||||||
|
assert direct_response in responses
|
||||||
|
responses.remove(direct_response)
|
||||||
|
assert responses == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_size_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 = {"Details": "Unexpected syntax"}
|
||||||
|
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
|
||||||
|
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(data=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": {
|
||||||
|
"internal_type": "FailedParsingManifest",
|
||||||
|
"Details": "Unexpected syntax"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
plugin.get_local_size.return_value = async_return_value(100, 5)
|
||||||
|
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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
190
tests/test_os_compatibility.py
Normal file
190
tests/test_os_compatibility.py
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.consts import OSCompatibility
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_os_compatibility_success(plugin, read, write):
|
||||||
|
context = "abc"
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {"game_ids": ["666", "13", "42"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_os_compatibility.side_effect = [
|
||||||
|
async_return_value(OSCompatibility.Linux),
|
||||||
|
async_return_value(None),
|
||||||
|
async_return_value(OSCompatibility.Windows | OSCompatibility.MacOS),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_os_compatibility.assert_has_calls([
|
||||||
|
call("666", context),
|
||||||
|
call("13", context),
|
||||||
|
call("42", context),
|
||||||
|
])
|
||||||
|
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "666",
|
||||||
|
"os_compatibility": OSCompatibility.Linux.value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "13",
|
||||||
|
"os_compatibility": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "42",
|
||||||
|
"os_compatibility": (OSCompatibility.Windows | OSCompatibility.MacOS).value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_os_compatibility_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
game_id = "6"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {"game_ids": [game_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_os_compatibility.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_os_compatibility.assert_called()
|
||||||
|
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "os_compatibility_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
plugin.prepare_os_compatibility_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_os_compatibility_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": 4,
|
||||||
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_os_compatibility_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["666"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ import pytest
|
|||||||
from galaxy.api.types import Game, Dlc, LicenseInfo
|
from galaxy.api.types import Game, Dlc, LicenseInfo
|
||||||
from galaxy.api.consts import LicenseType
|
from galaxy.api.consts import LicenseType
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ async def test_success(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
plugin.get_owned_games.return_value = async_return_value([
|
plugin.get_owned_games.return_value = async_return_value([
|
||||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
@@ -80,7 +80,7 @@ async def test_failure(plugin, read, write):
|
|||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_owned_games.side_effect = UnknownError()
|
plugin.get_owned_games.side_effect = UnknownError()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
@@ -90,7 +90,8 @@ async def test_failure(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"message": "Unknown error"
|
"message": "Unknown error",
|
||||||
|
"data": {"internal_type": "UnknownError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -100,6 +101,7 @@ async def test_failure(plugin, read, write):
|
|||||||
async def test_add_game(plugin, write):
|
async def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
plugin.add_game(game)
|
plugin.add_game(game)
|
||||||
|
await skip_loop()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -120,6 +122,7 @@ async def test_add_game(plugin, write):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_remove_game(plugin, write):
|
async def test_remove_game(plugin, write):
|
||||||
plugin.remove_game("5")
|
plugin.remove_game("5")
|
||||||
|
await skip_loop()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -135,6 +138,7 @@ async def test_remove_game(plugin, write):
|
|||||||
async def test_update_game(plugin, write):
|
async def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
plugin.update_game(game)
|
plugin.update_game(game)
|
||||||
|
await skip_loop()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@ async def test_set_cache(plugin, write, cache_data):
|
|||||||
|
|
||||||
plugin.persistent_cache.update(cache_data)
|
plugin.persistent_cache.update(cache_data)
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", cache_data)
|
assert_rpc_request(write, "push_cache", cache_data)
|
||||||
assert cache_data == plugin.persistent_cache
|
assert cache_data == plugin.persistent_cache
|
||||||
@@ -68,6 +69,7 @@ async def test_clear_cache(plugin, write, cache_data):
|
|||||||
|
|
||||||
plugin.persistent_cache.clear()
|
plugin.persistent_cache.clear()
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", {})
|
assert_rpc_request(write, "push_cache", {})
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
|
|||||||
75
tests/test_refresh_credentials.py
Normal file
75
tests/test_refresh_credentials.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
|
)
|
||||||
|
from galaxy.api.jsonrpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_credentials_success(plugin, read, write):
|
||||||
|
|
||||||
|
run_task = asyncio.create_task(plugin.run())
|
||||||
|
|
||||||
|
refreshed_credentials = {
|
||||||
|
"access_token": "new_access_token"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"result": refreshed_credentials
|
||||||
|
}
|
||||||
|
# 2 loop iterations delay is to force sending response after request has been sent
|
||||||
|
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
|
||||||
|
|
||||||
|
result = await plugin.refresh_credentials({}, False)
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "refresh_credentials",
|
||||||
|
"params": {
|
||||||
|
},
|
||||||
|
"id": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert result == refreshed_credentials
|
||||||
|
await run_task
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception", [
|
||||||
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
|
])
|
||||||
|
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
||||||
|
|
||||||
|
run_task = asyncio.create_task(plugin.run())
|
||||||
|
error = exception()
|
||||||
|
response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "1",
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2 loop iterations delay is to force sending response after request has been sent
|
||||||
|
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
|
||||||
|
|
||||||
|
with pytest.raises(JsonRpcError) as e:
|
||||||
|
await plugin.refresh_credentials({}, False)
|
||||||
|
|
||||||
|
# Go back to comparing error == e.value, after fixing current always raising JsonRpcError when handling a response with an error
|
||||||
|
assert error.code == e.value.code
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "refresh_credentials",
|
||||||
|
"params": {
|
||||||
|
},
|
||||||
|
"id": "1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
await run_task
|
||||||
345
tests/test_subscriptions.py
Normal file
345
tests/test_subscriptions.py
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
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,internal_type",
|
||||||
|
[
|
||||||
|
pytest.param(UnknownError, 0, "Unknown error", "UnknownError", id="unknown_error"),
|
||||||
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", "FailedParsingManifest", id="failed_parsing")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message, internal_type):
|
||||||
|
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,
|
||||||
|
"data": {"internal_type": internal_type},
|
||||||
|
"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,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_subscription_games_error(exception, code, message, internal_type, 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,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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 = {"Details": "Unexpected backend error"}
|
||||||
|
error_message, error_code = BackendError().message, BackendError().code
|
||||||
|
plugin.prepare_subscription_games_context.side_effect = BackendError(data=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": {
|
||||||
|
"internal_type": "BackendError",
|
||||||
|
"Details": "Unexpected backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
283
tests/test_user_presence.py
Normal file
283
tests/test_user_presence.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.consts import PresenceState
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.api.types import UserPresence
|
||||||
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_user_presence_success(plugin, read, write):
|
||||||
|
context = "abc"
|
||||||
|
user_id_list = ["666", "13", "42", "69", "22"]
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_id_list": user_id_list}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_user_presence.side_effect = [
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Unknown,
|
||||||
|
"game-id1",
|
||||||
|
None,
|
||||||
|
"unknown state",
|
||||||
|
None
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Offline,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"Going to grandma's house",
|
||||||
|
None
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Online,
|
||||||
|
"game-id3",
|
||||||
|
"game-title3",
|
||||||
|
"Pew pew",
|
||||||
|
None
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Away,
|
||||||
|
None,
|
||||||
|
"game-title4",
|
||||||
|
"AFKKTHXBY",
|
||||||
|
None
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Away,
|
||||||
|
None,
|
||||||
|
"game-title5",
|
||||||
|
None,
|
||||||
|
"Playing game-title5: In Menu"
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_user_presence.assert_has_calls([
|
||||||
|
call(user_id, context) for user_id in user_id_list
|
||||||
|
])
|
||||||
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "666",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Unknown.value,
|
||||||
|
"game_id": "game-id1",
|
||||||
|
"in_game_status": "unknown state"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "13",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Offline.value,
|
||||||
|
"in_game_status": "Going to grandma's house"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "42",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Online.value,
|
||||||
|
"game_id": "game-id3",
|
||||||
|
"game_title": "game-title3",
|
||||||
|
"in_game_status": "Pew pew"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "69",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Away.value,
|
||||||
|
"game_title": "game-title4",
|
||||||
|
"in_game_status": "AFKKTHXBY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "22",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Away.value,
|
||||||
|
"game_title": "game-title5",
|
||||||
|
"full_status": "Playing game-title5: In Menu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message,internal_type", [
|
||||||
|
(BackendError, 4, "Backend error", "BackendError"),
|
||||||
|
(KeyError, 0, "Unknown error", "UnknownError")
|
||||||
|
])
|
||||||
|
async def test_get_user_presence_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
|
user_id = "69"
|
||||||
|
request_id = "55"
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_id_list": [user_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_user_presence.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_user_presence.assert_called()
|
||||||
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_failure",
|
||||||
|
"params": {
|
||||||
|
"user_id": user_id,
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
||||||
|
request_id = "31415"
|
||||||
|
plugin.prepare_user_presence_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {"user_id_list": ["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": 4,
|
||||||
|
"message": "Backend error",
|
||||||
|
"data": {
|
||||||
|
"internal_type": "BackendError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
|
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {
|
||||||
|
"user_id_list": ["42"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_user_presence_import",
|
||||||
|
"params": {
|
||||||
|
"user_id_list": ["666"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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",
|
||||||
|
"data": {"internal_type": "ImportInProgress"}
|
||||||
|
}
|
||||||
|
} in responses
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_user_presence(plugin, write):
|
||||||
|
plugin.update_user_presence("42", UserPresence(PresenceState.Online, "game-id", "game-title", "Pew pew"))
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_updated",
|
||||||
|
"params": {
|
||||||
|
"user_id": "42",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Online.value,
|
||||||
|
"game_id": "game-id",
|
||||||
|
"game_title": "game-title",
|
||||||
|
"in_game_status": "Pew pew"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user