mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-30 01:21:18 -05:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e483edd92 | ||
|
|
8a39496a11 | ||
|
|
2c18ac4ffa | ||
|
|
6a1d2763c3 | ||
|
|
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 |
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.13
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
pytest
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ Pipfile
|
|||||||
.idea
|
.idea
|
||||||
docs/source/_build
|
docs/source/_build
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
|
.pytest_cache
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest
|
image: registry-gitlab.gog.com/docker/python:3.13
|
||||||
|
|
||||||
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
|
||||||
@@ -18,10 +18,10 @@ deploy_package:
|
|||||||
TWINE_USERNAME: $PYPI_USERNAME
|
TWINE_USERNAME: $PYPI_USERNAME
|
||||||
TWINE_PASSWORD: $PYPI_PASSWORD
|
TWINE_PASSWORD: $PYPI_PASSWORD
|
||||||
script:
|
script:
|
||||||
- pip install twine wheel
|
- pip install twine wheel build
|
||||||
- rm -rf dist
|
- rm -rf dist
|
||||||
- export VERSION=$(python setup.py --version)
|
- export VERSION=$(python setup.py --version)
|
||||||
- python setup.py sdist --formats=gztar bdist_wheel
|
- python -m build --sdist --wheel
|
||||||
- twine upload dist/*
|
- twine upload dist/*
|
||||||
- curl -X POST --silent --show-error --fail
|
- curl -X POST --silent --show-error --fail
|
||||||
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
"https://gitlab.gog.com/api/v4/projects/${CI_PROJECT_ID}/repository/tags?tag_name=${VERSION}&ref=${CI_COMMIT_REF_NAME}&private_token=${PACKAGE_DEPLOYER_API_TOKEN}"
|
||||||
|
|||||||
@@ -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 |
|
||||||
|
|||||||
70
README.md
70
README.md
@@ -1,17 +1,19 @@
|
|||||||
# GOG Galaxy Integrations Python API
|
# GOG GALAXY Integrations Python API
|
||||||
|
|
||||||
This Python library allows developers to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
|
This Python library allows developers to easily build community integrations for various gaming platforms with GOG GALAXY **2.1**.
|
||||||
|
|
||||||
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
|
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
|
||||||
|
|
||||||
|
Note: For integrations targeting GOG GALAXY the **below 2.1.0 version**, please refer to [this version](https://github.com/gogcom/galaxy-integrations-python-api/tree/0.69).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Each integration in GOG Galaxy 2.0 comes as a separate Python script and is launched as a separate process that needs to communicate with the main instance of GOG Galaxy 2.0.
|
Each integration in GOG GALAXY 2.1 comes as a separate Python script and is launched as a separate process that needs to communicate with the main instance of GOG GALAXY 2.1.
|
||||||
|
|
||||||
The provided features are:
|
The provided features are:
|
||||||
|
|
||||||
- multistep authorization using a browser built into GOG Galaxy 2.0
|
- multistep authorization using a browser built into GOG GALAXY 2.1
|
||||||
- support for GOG Galaxy 2.0 features:
|
- support for GOG GALAXY 2.1 features:
|
||||||
- importing owned and detecting installed games
|
- importing owned and detecting installed games
|
||||||
- installing and launching games
|
- installing and launching games
|
||||||
- importing achievements and game time
|
- importing achievements and game time
|
||||||
@@ -28,28 +30,41 @@ Each integration can implement only one platform. Each integration must declare
|
|||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
Each integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client at the appropriate times.
|
Each integration should inherit from the `galaxy.api.plugin.Plugin` class. Supported methods like `galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG GALAXY client at the appropriate times.
|
||||||
Each of those methods can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
|
Each of those methods can raise exceptions inherited from the `galaxy.api.jsonrpc.ApplicationError`.
|
||||||
Communication between an integration and the client is also possible with the use of notifications, for example: :meth:`~galaxy.api.plugin.Plugin.update_local_game_status`.
|
Communication between an integration and the client is also possible with the use of notifications, for example: `galaxy.api.plugin.Plugin.update_local_game_status`.
|
||||||
|
|
||||||
|
The minimum implementation requires to override `galaxy.api.plugin.Plugin.authenticate` and `galaxy.api.plugin.Plugin.get_owned_games` methods.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
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)
|
||||||
@@ -61,8 +76,8 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The client has a built-in Python 3.7 interpreter, so integrations are delivered as Python modules.
|
The client has a built-in Python 3.13 interpreter, so integrations are delivered as Python modules.
|
||||||
In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the Python files, the integration folder must contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [exemplary structure](#deploy-structure-example).
|
In order to be found by GOG GALAXY 2.1 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the Python files, the integration folder must contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [exemplary structure](#deploy-structure-example).
|
||||||
|
|
||||||
### Lookup directory
|
### Lookup directory
|
||||||
|
|
||||||
@@ -76,6 +91,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.13/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 +113,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,16 +126,15 @@ 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
|
||||||
|
|
||||||
All third-party packages (packages not included in the Python 3.7 standard library) should be deployed along with plugin files. Use the following command structure:
|
All third-party packages (packages not included in the Python 3.13 standard library) should be deployed along with plugin files. Use the following command structure:
|
||||||
|
|
||||||
```pip install DEP --target DIR --implementation cp --python-version 37```
|
```pip install DEP --target DIR --implementation cp --python-version 313```
|
||||||
|
|
||||||
For example, a plugin that uses *requests* could have the following structure:
|
For example, a plugin that uses *requests* could have the following structure:
|
||||||
|
|
||||||
@@ -125,4 +153,4 @@ installed
|
|||||||
|
|
||||||
## Legal Notice
|
## Legal Notice
|
||||||
|
|
||||||
By integrating or attempting to integrate any applications or content with or into GOG Galaxy 2.0 you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws.
|
By integrating or attempting to integrate any applications or content with or into GOG GALAXY 2.1 you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG GALAXY 2.1 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -70,6 +70,6 @@ html_theme_options = {
|
|||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = ['_static']
|
# html_static_path = ["_static"]
|
||||||
|
|
||||||
master_doc = 'index'
|
master_doc = 'index'
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugin
|
|||||||
.. automodule:: galaxy.api.plugin
|
.. automodule:: galaxy.api.plugin
|
||||||
:members:
|
:members:
|
||||||
:undoc-members:
|
:undoc-members:
|
||||||
:exclude-members: JSONEncoder, features, achievements_import_finished, game_times_import_finished, start_achievements_import, start_game_times_import, get_game_times, get_unlocked_achievements
|
:exclude-members: JSONEncoder, features
|
||||||
|
|
||||||
types
|
types
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|||||||
69
pyproject.toml
Normal file
69
pyproject.toml
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "galaxy.plugin.api"
|
||||||
|
version = "0.71"
|
||||||
|
description = "GOG Galaxy Integrations Python API"
|
||||||
|
authors = [
|
||||||
|
{name = "Galaxy team", email = "galaxy@gog.com"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
license-files = ["LICENSE"]
|
||||||
|
requires-python = "~=3.13.0"
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"aiohttp>=3.12.15",
|
||||||
|
"certifi>=2026.1.4",
|
||||||
|
"psutil>=5.6.6; sys_platform == 'darwin'"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest==8.4.1",
|
||||||
|
"pytest-asyncio==1.1.0",
|
||||||
|
"pytest-mock==3.14.1",
|
||||||
|
"pytest-mypy==1.0.1",
|
||||||
|
"pytest-flakes==4.0.5",
|
||||||
|
"types-certifi==2021.10.8.3",
|
||||||
|
"setuptools==80.9.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
package-dir = {"" = "src"}
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"--strict-markers",
|
||||||
|
"--strict-config",
|
||||||
|
"--verbose",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.13"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
warn_no_return = true
|
||||||
|
warn_unreachable = true
|
||||||
|
strict_equality = true
|
||||||
8
requirements-dev.txt
Normal file
8
requirements-dev.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
-r requirements.txt
|
||||||
|
pytest==8.4.1
|
||||||
|
pytest-asyncio==1.1.0
|
||||||
|
pytest-mock==3.14.1
|
||||||
|
pytest-mypy==1.0.1
|
||||||
|
pytest-flakes==4.0.5
|
||||||
|
types-certifi==2021.10.8.3
|
||||||
|
setuptools==80.9.0
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
-e .
|
-e .
|
||||||
pytest==5.2.2
|
# 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
|
aiohttp==3.12.15
|
||||||
pytest-mypy==0.4.1
|
certifi==2026.1.4
|
||||||
pytest-flakes==4.0.0
|
psutil==5.6.6; sys_platform == 'darwin'
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
# End of copy from setup.py
|
||||||
aiohttp==3.5.4
|
|
||||||
certifi==2019.3.9
|
|
||||||
psutil==5.6.3; sys_platform == 'darwin'
|
|
||||||
|
|||||||
16
setup.py
16
setup.py
@@ -1,15 +1,3 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup()
|
||||||
name="galaxy.plugin.api",
|
|
||||||
version="0.57",
|
|
||||||
description="GOG Galaxy Integrations Python API",
|
|
||||||
author='Galaxy team',
|
|
||||||
author_email='galaxy@gog.com',
|
|
||||||
packages=find_packages("src"),
|
|
||||||
package_dir={'': 'src'},
|
|
||||||
install_requires=[
|
|
||||||
"aiohttp==3.5.4",
|
|
||||||
"certifi==2019.3.9"
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ sphinx:
|
|||||||
formats: all
|
formats: all
|
||||||
|
|
||||||
python:
|
python:
|
||||||
version: 3.7
|
version: 3.13
|
||||||
install:
|
install:
|
||||||
- requirements: requirements.txt
|
- requirements: requirements.txt
|
||||||
- requirements: docs/requirements.txt
|
- requirements: docs/requirements.txt
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger(__name__).setLevel(logging.INFO)
|
||||||
|
|
||||||
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class Feature(Enum):
|
|||||||
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
ImportOSCompatibility = "ImportOSCompatibility"
|
ImportOSCompatibility = "ImportOSCompatibility"
|
||||||
ImportUserPresence = "ImportUserPresence"
|
ImportUserPresence = "ImportUserPresence"
|
||||||
|
ImportLocalSize = "ImportLocalSize"
|
||||||
|
ImportSubscriptions = "ImportSubscriptions"
|
||||||
|
ImportSubscriptionGames = "ImportSubscriptionGames"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -149,3 +152,13 @@ class PresenceState(Enum):
|
|||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionDiscovery(Flag):
|
||||||
|
"""Possible capabilities which inform what methods of subscriptions ownership detection are supported.
|
||||||
|
|
||||||
|
:param AUTOMATIC: integration can retrieve the proper status of subscription ownership.
|
||||||
|
:param USER_ENABLED: integration can handle override of ~class::`Subscription.owned` value to True
|
||||||
|
"""
|
||||||
|
AUTOMATIC = 1
|
||||||
|
USER_ENABLED = 2
|
||||||
|
|||||||
@@ -1,75 +1,88 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
assert UnknownError
|
|
||||||
|
assert UnknownError is not None # UnknownError not used directly in errors.py, but we want to ensure it's defined
|
||||||
|
|
||||||
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),
|
||||||
|
f"{self._name} import",
|
||||||
|
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 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())
|
||||||
|
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,11 +8,20 @@ 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):
|
||||||
@@ -21,37 +30,42 @@ class JsonRpcError(Exception):
|
|||||||
def json(self):
|
def json(self):
|
||||||
obj = {
|
obj = {
|
||||||
"code": self.code,
|
"code": self.code,
|
||||||
"message": self.message
|
"message": self.message,
|
||||||
|
"data": self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.data is not None:
|
|
||||||
obj["error"]["data"] = self.data
|
|
||||||
|
|
||||||
return obj
|
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):
|
||||||
@@ -59,9 +73,11 @@ 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, {}, {}])
|
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
|
||||||
@@ -89,7 +105,6 @@ class Connection():
|
|||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._task_manager = TaskManager("jsonrpc server")
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
self._write_lock = asyncio.Lock()
|
|
||||||
self._last_request_id = 0
|
self._last_request_id = 0
|
||||||
self._requests_futures = {}
|
self._requests_futures = {}
|
||||||
|
|
||||||
@@ -133,7 +148,7 @@ class Connection():
|
|||||||
future = loop.create_future()
|
future = loop.create_future()
|
||||||
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
||||||
|
|
||||||
logging.info(
|
logger.info(
|
||||||
"Sending request: id=%s, method=%s, params=%s",
|
"Sending request: id=%s, method=%s, params=%s",
|
||||||
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
)
|
)
|
||||||
@@ -151,7 +166,7 @@ class Connection():
|
|||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logging.info(
|
logger.info(
|
||||||
"Sending notification: method=%s, params=%s",
|
"Sending notification: method=%s, params=%s",
|
||||||
method, anonymise_sensitive_params(params, sensitive_params)
|
method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
)
|
)
|
||||||
@@ -169,20 +184,20 @@ class Connection():
|
|||||||
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):
|
||||||
if self._active:
|
if self._active:
|
||||||
logging.info("Closing JSON-RPC server - not more messages will be read")
|
logger.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
self._active = False
|
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):
|
||||||
@@ -204,7 +219,7 @@ class Connection():
|
|||||||
request_future = self._requests_futures.get(int(response.id))
|
request_future = self._requests_futures.get(int(response.id))
|
||||||
if request_future is None:
|
if request_future is None:
|
||||||
response_type = "response" if response.result is not None else "error"
|
response_type = "response" if response.result is not None else "error"
|
||||||
logging.warning("Received %s for unknown request: %s", response_type, response.id)
|
logger.warning("Received %s for unknown request: %s", response_type, response.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
future, sensitive_params = request_future
|
future, sensitive_params = request_future
|
||||||
@@ -225,7 +240,7 @@ class Connection():
|
|||||||
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
|
||||||
@@ -242,12 +257,12 @@ class Connection():
|
|||||||
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
|
||||||
|
|
||||||
@@ -274,7 +289,7 @@ class Connection():
|
|||||||
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)
|
||||||
@@ -282,7 +297,7 @@ class Connection():
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_message(data):
|
def _parse_message(data):
|
||||||
try:
|
try:
|
||||||
jsonrpc_message = json.loads(data, encoding="utf-8")
|
jsonrpc_message = json.loads(data)
|
||||||
if jsonrpc_message.get("jsonrpc") != "2.0":
|
if jsonrpc_message.get("jsonrpc") != "2.0":
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
del jsonrpc_message["jsonrpc"]
|
del jsonrpc_message["jsonrpc"]
|
||||||
@@ -296,19 +311,14 @@ class Connection():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data, log_level=logging.DEBUG):
|
||||||
async def send_task(data_):
|
|
||||||
async with self._write_lock:
|
|
||||||
self._writer.write(data_)
|
|
||||||
await self._writer.drain()
|
|
||||||
|
|
||||||
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._task_manager.create_task(send_task(data), "send")
|
self._writer.write(data)
|
||||||
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 = {
|
||||||
@@ -316,7 +326,7 @@ class Connection():
|
|||||||
"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 = {
|
||||||
@@ -324,8 +334,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": error.json()
|
"error": error.json()
|
||||||
}
|
}
|
||||||
|
self._send(response, logging.ERROR)
|
||||||
self._send(response)
|
|
||||||
|
|
||||||
def _send_request(self, request_id, method, params):
|
def _send_request(self, request_id, method, params):
|
||||||
request = {
|
request = {
|
||||||
@@ -334,7 +343,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(request)
|
self._send(request, logging.NOTSET)
|
||||||
|
|
||||||
def _send_notification(self, method, params):
|
def _send_notification(self, method, params):
|
||||||
notification = {
|
notification = {
|
||||||
@@ -342,24 +351,25 @@ class Connection():
|
|||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(notification)
|
self._send(notification, logging.NOTSET)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_request(request, sensitive_params):
|
def _log_request(request, sensitive_params):
|
||||||
params = anonymise_sensitive_params(request.params, sensitive_params)
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
if request.id is not None:
|
if request.id is not None:
|
||||||
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
logger.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
else:
|
else:
|
||||||
logging.info("Handling notification: method=%s, params=%s", request.method, params)
|
logger.info("Handling notification: method=%s, params=%s", request.method, params)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_response(response, sensitive_params):
|
def _log_response(response, sensitive_params):
|
||||||
result = anonymise_sensitive_params(response.result, sensitive_params)
|
result = anonymise_sensitive_params(response.result, sensitive_params)
|
||||||
logging.info("Handling response: id=%s, result=%s", response.id, result)
|
logger.info("Handling response: id=%s, result=%s", response.id, result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_error(response, error, sensitive_params):
|
def _log_error(response, error, sensitive_params):
|
||||||
data = anonymise_sensitive_params(error.data, sensitive_params)
|
params = error.data if error.data is not None else {}
|
||||||
logging.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
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
|
response.id, error.code, error.message, data
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ 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, OSCompatibility
|
from galaxy.api.consts import Feature, OSCompatibility
|
||||||
from galaxy.api.errors import ImportInProgress, UnknownError
|
|
||||||
from galaxy.api.jsonrpc import ApplicationError, Connection
|
from galaxy.api.jsonrpc import ApplicationError, Connection
|
||||||
from galaxy.api.types import (
|
from galaxy.api.types import (
|
||||||
Achievement, Authentication, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserInfo, UserPresence
|
Achievement, Authentication, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserInfo, UserPresence,
|
||||||
|
Subscription, SubscriptionGame
|
||||||
)
|
)
|
||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
|
from galaxy.api.importer import Importer, CollectionImporter, SynchroneousImporter
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
@@ -33,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
|
||||||
|
|
||||||
@@ -46,17 +49,83 @@ class Plugin:
|
|||||||
encoder = JSONEncoder()
|
encoder = JSONEncoder()
|
||||||
self._connection = Connection(self._reader, self._writer, encoder)
|
self._connection = Connection(self._reader, self._writer, encoder)
|
||||||
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
self._game_library_settings_import_in_progress = False
|
|
||||||
self._os_compatibility_import_in_progress = False
|
|
||||||
self._user_presence_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)
|
||||||
@@ -123,6 +192,15 @@ class Plugin:
|
|||||||
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
self._register_method("start_user_presence_import", self._start_user_presence_import)
|
||||||
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
|
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
|
||||||
|
|
||||||
|
self._register_method("start_local_size_import", self._start_local_size_import)
|
||||||
|
self._detect_feature(Feature.ImportLocalSize, ["get_local_size"])
|
||||||
|
|
||||||
|
self._register_method("import_subscriptions", self.get_subscriptions, result_name="subscriptions")
|
||||||
|
self._detect_feature(Feature.ImportSubscriptions, ["get_subscriptions"])
|
||||||
|
|
||||||
|
self._register_method("start_subscription_games_import", self._start_subscription_games_import)
|
||||||
|
self._detect_feature(Feature.ImportSubscriptionGames, ["get_subscription_games"])
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -150,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 = {
|
||||||
@@ -189,39 +268,46 @@ class Plugin:
|
|||||||
async def run(self):
|
async def run(self):
|
||||||
"""Plugin's main coroutine."""
|
"""Plugin's main coroutine."""
|
||||||
await self._connection.run()
|
await self._connection.run()
|
||||||
logging.debug("Plugin run loop finished")
|
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._connection.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:
|
||||||
logging.debug("Waiting for plugin to close")
|
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._connection.wait_closed()
|
await self._connection.wait_closed()
|
||||||
logging.debug("Plugin closed")
|
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"""
|
||||||
return self._external_task_manager.create_task(coro, description)
|
return self._external_task_manager.create_task(coro, description)
|
||||||
|
|
||||||
async def _pass_control(self):
|
async def _pass_control(self):
|
||||||
while self._active:
|
while self._active and self._connection._active:
|
||||||
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()
|
||||||
@@ -238,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
|
||||||
@@ -426,7 +512,7 @@ class Plugin:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def _game_time_import_success(self, game_time: GameTime) -> None:
|
def _game_time_import_success(self, game_id: str, game_time: GameTime) -> None:
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._connection.send_notification("game_time_import_success", params)
|
self._connection.send_notification("game_time_import_success", params)
|
||||||
|
|
||||||
@@ -440,7 +526,7 @@ class Plugin:
|
|||||||
def _game_times_import_finished(self) -> None:
|
def _game_times_import_finished(self) -> None:
|
||||||
self._connection.send_notification("game_times_import_finished", None)
|
self._connection.send_notification("game_times_import_finished", None)
|
||||||
|
|
||||||
def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None:
|
def _game_library_settings_import_success(self, game_id: str, game_library_settings: GameLibrarySettings) -> None:
|
||||||
params = {"game_library_settings": game_library_settings}
|
params = {"game_library_settings": game_library_settings}
|
||||||
self._connection.send_notification("game_library_settings_import_success", params)
|
self._connection.send_notification("game_library_settings_import_success", params)
|
||||||
|
|
||||||
@@ -496,6 +582,57 @@ class Plugin:
|
|||||||
def _user_presence_import_finished(self) -> None:
|
def _user_presence_import_finished(self) -> None:
|
||||||
self._connection.send_notification("user_presence_import_finished", None)
|
self._connection.send_notification("user_presence_import_finished", None)
|
||||||
|
|
||||||
|
def _local_size_import_success(self, game_id: str, size: Optional[int]) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"local_size_import_success",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"local_size": size
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _local_size_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"local_size_import_failure",
|
||||||
|
{
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _local_size_import_finished(self) -> None:
|
||||||
|
self._connection.send_notification("local_size_import_finished", None)
|
||||||
|
|
||||||
|
def _subscription_games_import_success(self, subscription_name: str,
|
||||||
|
subscription_games: Optional[List[SubscriptionGame]]) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"subscription_games_import_success",
|
||||||
|
{
|
||||||
|
"subscription_name": subscription_name,
|
||||||
|
"subscription_games": subscription_games
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _subscription_games_import_failure(self, subscription_name: str, error: ApplicationError) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"subscription_games_import_failure",
|
||||||
|
{
|
||||||
|
"subscription_name": subscription_name,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _subscriptions_games_partial_import_finished(self, subscription_name: str) -> None:
|
||||||
|
self._connection.send_notification(
|
||||||
|
"subscription_games_partial_import_finished",
|
||||||
|
{
|
||||||
|
"subscription_name": subscription_name
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _subscription_games_import_finished(self) -> None:
|
||||||
|
self._connection.send_notification("subscription_games_import_finished", None)
|
||||||
|
|
||||||
def lost_authentication(self) -> None:
|
def lost_authentication(self) -> None:
|
||||||
"""Notify the client that integration has lost authentication for the
|
"""Notify the client that integration has lost authentication for the
|
||||||
current user and is unable to perform actions which would require it.
|
current user and is unable to perform actions which would require it.
|
||||||
@@ -555,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:
|
||||||
@@ -577,7 +714,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
||||||
-> Union[NextStep, Authentication]:
|
-> Union[NextStep, Authentication]:
|
||||||
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
||||||
or :meth:`.pass_login_credentials`.
|
or :meth:`.pass_login_credentials`.
|
||||||
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
||||||
@@ -627,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.
|
||||||
@@ -791,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.
|
||||||
@@ -849,36 +928,7 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
||||||
if self._game_library_settings_import_in_progress:
|
await self._game_library_settings_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_game_library_settings_context(game_ids)
|
|
||||||
|
|
||||||
async def import_game_library_settings(game_id, context_):
|
|
||||||
try:
|
|
||||||
game_library_settings = await self.get_game_library_settings(game_id, context_)
|
|
||||||
self._game_library_settings_import_success(game_library_settings)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._game_library_settings_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_game_library_settings")
|
|
||||||
self._game_library_settings_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_game_library_settings_set(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
imports = [import_game_library_settings(game_id, context_) for game_id in game_ids_]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
finally:
|
|
||||||
self._game_library_settings_import_finished()
|
|
||||||
self._game_library_settings_import_in_progress = False
|
|
||||||
self.game_library_settings_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_game_library_settings_set(game_ids, context),
|
|
||||||
"game library settings import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._game_library_settings_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_game_library_settings.
|
"""Override this method to prepare context for get_game_library_settings.
|
||||||
@@ -907,37 +957,7 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
||||||
if self._os_compatibility_import_in_progress:
|
await self._os_compatibility_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_os_compatibility_context(game_ids)
|
|
||||||
|
|
||||||
async def import_os_compatibility(game_id, context_):
|
|
||||||
try:
|
|
||||||
os_compatibility = await self.get_os_compatibility(game_id, context_)
|
|
||||||
self._os_compatibility_import_success(game_id, os_compatibility)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._os_compatibility_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_os_compatibility")
|
|
||||||
self._os_compatibility_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_os_compatibility_set(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
await asyncio.gather(*[
|
|
||||||
import_os_compatibility(game_id, context_) for game_id in game_ids_
|
|
||||||
])
|
|
||||||
finally:
|
|
||||||
self._os_compatibility_import_finished()
|
|
||||||
self._os_compatibility_import_in_progress = False
|
|
||||||
self.os_compatibility_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_os_compatibility_set(game_ids, context),
|
|
||||||
"game OS compatibility import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._os_compatibility_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_os_compatibility.
|
"""Override this method to prepare context for get_os_compatibility.
|
||||||
@@ -962,45 +982,15 @@ class Plugin:
|
|||||||
def os_compatibility_import_complete(self) -> None:
|
def os_compatibility_import_complete(self) -> None:
|
||||||
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
||||||
|
|
||||||
async def _start_user_presence_import(self, user_ids: List[str]) -> None:
|
async def _start_user_presence_import(self, user_id_list: List[str]) -> None:
|
||||||
if self._user_presence_import_in_progress:
|
await self._user_presence_importer.start(user_id_list)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_user_presence_context(user_ids)
|
async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for :meth:`get_user_presence`.
|
||||||
async def import_user_presence(user_id, context_) -> None:
|
|
||||||
try:
|
|
||||||
self._user_presence_import_success(user_id, await self.get_user_presence(user_id, context_))
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._user_presence_import_failure(user_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_user_presence")
|
|
||||||
self._user_presence_import_failure(user_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_user_presence_set(user_ids_, context_) -> None:
|
|
||||||
try:
|
|
||||||
await asyncio.gather(*[
|
|
||||||
import_user_presence(user_id, context_)
|
|
||||||
for user_id in user_ids_
|
|
||||||
])
|
|
||||||
finally:
|
|
||||||
self._user_presence_import_finished()
|
|
||||||
self._user_presence_import_in_progress = False
|
|
||||||
self.user_presence_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_user_presence_set(user_ids, context),
|
|
||||||
"user presence import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._user_presence_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_user_presence_context(self, user_ids: List[str]) -> Any:
|
|
||||||
"""Override this method to prepare context for get_user_presence.
|
|
||||||
This allows for optimizations like batch requests to platform API.
|
This allows for optimizations like batch requests to platform API.
|
||||||
Default implementation returns None.
|
Default implementation returns None.
|
||||||
|
|
||||||
:param user_ids: the ids of the users for whom presence information is imported
|
:param user_id_list: the ids of the users for whom presence information is imported
|
||||||
:return: context
|
:return: context
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
@@ -1018,6 +1008,81 @@ class Plugin:
|
|||||||
def user_presence_import_complete(self) -> None:
|
def user_presence_import_complete(self) -> None:
|
||||||
"""Override this method to handle operations after presence import is finished (like updating cache)."""
|
"""Override this method to handle operations after presence import is finished (like updating cache)."""
|
||||||
|
|
||||||
|
async def _start_local_size_import(self, game_ids: List[str]) -> None:
|
||||||
|
await self._local_size_importer.start(game_ids)
|
||||||
|
|
||||||
|
async def prepare_local_size_context(self, game_ids: List[str]) -> Any:
|
||||||
|
"""Override this method to prepare context for :meth:`get_local_size`
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which information about size is imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_local_size(self, game_id: str, context: Any) -> Optional[int]:
|
||||||
|
"""Override this method to return installed game size.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
It is preferable to avoid iterating over local game files when overriding this method.
|
||||||
|
If possible, please use a more efficient way of game size retrieval.
|
||||||
|
|
||||||
|
:param 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.
|
||||||
@@ -1037,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]
|
||||||
@@ -1045,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":
|
||||||
@@ -1069,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,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState, SubscriptionDiscovery
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -33,7 +33,7 @@ class Cookie:
|
|||||||
|
|
||||||
@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
|
||||||
@@ -62,10 +62,10 @@ class NextStep:
|
|||||||
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
||||||
|
|
||||||
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
||||||
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
||||||
:param cookies: browser initial set of cookies
|
:param cookies: browser initial set of cookies
|
||||||
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
||||||
on every document at given step of internal browser authentication.
|
on every document at given step of internal browser authentication.
|
||||||
"""
|
"""
|
||||||
next_step: str
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
@@ -166,8 +166,8 @@ class UserInfo:
|
|||||||
"""
|
"""
|
||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
avatar_url: Optional[str]
|
avatar_url: Optional[str] = None
|
||||||
profile_url: Optional[str]
|
profile_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -216,3 +216,42 @@ class UserPresence:
|
|||||||
game_title: Optional[str] = None
|
game_title: Optional[str] = None
|
||||||
in_game_status: Optional[str] = None
|
in_game_status: Optional[str] = None
|
||||||
full_status: Optional[str] = None
|
full_status: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Subscription:
|
||||||
|
"""Information about a subscription.
|
||||||
|
|
||||||
|
:param subscription_name: name of the subscription, will also be used as its identifier.
|
||||||
|
:param owned: whether the subscription is owned or not, None if unknown.
|
||||||
|
:param end_time: unix timestamp of when the subscription ends, None if unknown.
|
||||||
|
:param subscription_discovery: combination of settings that can be manually
|
||||||
|
chosen by user to determine subscription handling behaviour. For example, if the integration cannot retrieve games
|
||||||
|
for subscription when user doesn't own it, then USER_ENABLED should not be used.
|
||||||
|
If the integration cannot determine subscription ownership for a user then AUTOMATIC should not be used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
subscription_name: str
|
||||||
|
owned: Optional[bool] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
subscription_discovery: SubscriptionDiscovery = SubscriptionDiscovery.AUTOMATIC | \
|
||||||
|
SubscriptionDiscovery.USER_ENABLED
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.subscription_discovery in [SubscriptionDiscovery.AUTOMATIC, SubscriptionDiscovery.USER_ENABLED,
|
||||||
|
SubscriptionDiscovery.AUTOMATIC | SubscriptionDiscovery.USER_ENABLED]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubscriptionGame:
|
||||||
|
"""Information about a game from a subscription.
|
||||||
|
|
||||||
|
:param game_title: title of the game
|
||||||
|
:param game_id: id of the game
|
||||||
|
:param start_time: unix timestamp of when the game has been added to subscription
|
||||||
|
:param end_time: unix timestamp of when the game will be removed from subscription.
|
||||||
|
"""
|
||||||
|
game_title: str
|
||||||
|
game_id: str
|
||||||
|
start_time: Optional[int] = None
|
||||||
|
end_time: Optional[int] = None
|
||||||
|
|||||||
@@ -1,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>`_
|
||||||
"""
|
"""
|
||||||
@@ -84,11 +85,11 @@ def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
|||||||
|
|
||||||
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
||||||
"""
|
"""
|
||||||
Creates client session with resonable defaults.
|
Creates client session with reasonable defaults.
|
||||||
For details about available parameters refer to
|
For details about available parameters refer to
|
||||||
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||||
|
|
||||||
Examplary customization:
|
Exemplary customization:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@@ -122,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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,37 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
def delayed_return_value(return_value, loop_iterations_delay=0):
|
||||||
|
async def return_fn(*args, **kwargs):
|
||||||
|
for _ in range(loop_iterations_delay):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return return_value
|
||||||
|
return return_fn
|
||||||
|
|
||||||
|
|
||||||
|
def delayed_return_value_iterable(return_value, loop_iterations_delay=0):
|
||||||
|
iterable = iter(return_value)
|
||||||
|
async def return_fn(*args, **kwargs):
|
||||||
|
for _ in range(loop_iterations_delay):
|
||||||
|
await asyncio.sleep(0.001)
|
||||||
|
last_value = next(iterable)
|
||||||
|
return last_value
|
||||||
|
return return_fn
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from contextlib import ExitStack
|
from contextlib import ExitStack
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -12,12 +12,12 @@ 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 = AsyncMock()
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
async def writer():
|
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
|
||||||
@@ -34,7 +34,7 @@ def write(writer):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
async def plugin(reader, writer):
|
def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
methods = (
|
methods = (
|
||||||
"handshake_complete",
|
"handshake_complete",
|
||||||
@@ -64,15 +64,24 @@ async def plugin(reader, writer):
|
|||||||
"get_user_presence",
|
"get_user_presence",
|
||||||
"prepare_user_presence_context",
|
"prepare_user_presence_context",
|
||||||
"user_presence_import_complete",
|
"user_presence_import_complete",
|
||||||
|
"get_local_size",
|
||||||
|
"prepare_local_size_context",
|
||||||
|
"local_size_import_complete",
|
||||||
|
"get_subscriptions",
|
||||||
|
"get_subscription_games",
|
||||||
|
"prepare_subscription_games_context",
|
||||||
|
"subscription_games_import_complete"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Patch the class methods BEFORE creating the instance
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
|
|
||||||
async with Plugin(Platform.Generic, "0.1", reader, writer, "token") as plugin:
|
# Now create the plugin instance
|
||||||
plugin.shutdown.return_value = async_return_value(None)
|
plugin = Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||||
yield plugin
|
plugin.shutdown.return_value = None
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
|
|||||||
@@ -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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ def test_initialization_no_id_nor_name():
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_unlocked_achievements_success(plugin, read, write):
|
async def test_get_unlocked_achievements_success(plugin, read, write):
|
||||||
plugin.prepare_achievements_context.return_value = async_return_value(5)
|
plugin.prepare_achievements_context.return_value = 5
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -31,16 +31,17 @@ async def test_get_unlocked_achievements_success(plugin, read, write):
|
|||||||
"game_ids": ["14"]
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_unlocked_achievements.return_value = async_return_value([
|
plugin.get_unlocked_achievements.return_value = [
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||||
])
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.prepare_achievements_context.assert_called_with(["14"])
|
plugin.prepare_achievements_context.assert_called_with(["14"])
|
||||||
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
||||||
plugin.achievements_import_complete.asert_called_with()
|
plugin.achievements_import_complete.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -79,12 +80,12 @@ 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 = None
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -94,11 +95,12 @@ async def test_get_unlocked_achievements_error(exception, code, message, plugin,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_unlocked_achievements.side_effect = exception
|
plugin.get_unlocked_achievements.side_effect = exception
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
plugin.achievements_import_complete.asert_called_with()
|
plugin.achievements_import_complete.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -113,7 +115,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,9 +138,10 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -145,7 +149,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"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -153,8 +158,8 @@ async def test_prepare_get_unlocked_achievements_context_error(plugin, read, wri
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_in_progress(plugin, read, write):
|
async def test_import_in_progress(plugin, read, write):
|
||||||
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
plugin.prepare_achievements_context.return_value = None
|
||||||
plugin.get_unlocked_achievements.return_value = async_return_value([])
|
plugin.get_unlocked_achievements.return_value = []
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -174,12 +179,13 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
create_message(requests[0]),
|
||||||
async_return_value(create_message(requests[1])),
|
create_message(requests[1]),
|
||||||
async_return_value(b"", 10)
|
b""
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
messages = get_messages(write)
|
messages = get_messages(write)
|
||||||
assert {
|
assert {
|
||||||
@@ -192,7 +198,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
|
||||||
|
|
||||||
|
|||||||
@@ -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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -17,9 +25,10 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -35,29 +44,29 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.authenticate.side_effect = error()
|
plugin.authenticate.side_effect = error()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -66,7 +75,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,9 +94,10 @@ async def test_stored_credentials(plugin, read, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
write.assert_called()
|
write.assert_called()
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import json
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_chunked_messages(plugin, read):
|
async def test_chunked_messages(plugin, read):
|
||||||
@@ -16,8 +14,9 @@ async def test_chunked_messages(plugin, read):
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = json.dumps(request).encode() + b"\n"
|
message = json.dumps(request).encode() + b"\n"
|
||||||
read.side_effect = [async_return_value(message[:5]), async_return_value(message[5:]), async_return_value(b"")]
|
read.side_effect = [message[:5], message[5:], b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
|
|
||||||
@@ -41,8 +40,9 @@ async def test_joined_messages(plugin, read):
|
|||||||
]
|
]
|
||||||
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
||||||
|
|
||||||
read.side_effect = [async_return_value(data), async_return_value(b"")]
|
read.side_effect = [data, b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ async def test_not_finished(plugin, read):
|
|||||||
}
|
}
|
||||||
|
|
||||||
message = json.dumps(request).encode() # no new line
|
message = json.dumps(request).encode() # no new line
|
||||||
read.side_effect = [async_return_value(message), async_return_value(b"")]
|
read.side_effect = [message, b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.install_game.assert_not_called()
|
plugin.install_game.assert_not_called()
|
||||||
|
|||||||
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)
|
||||||
@@ -17,7 +17,10 @@ def test_base_class():
|
|||||||
Feature.LaunchPlatformClient,
|
Feature.LaunchPlatformClient,
|
||||||
Feature.ImportGameLibrarySettings,
|
Feature.ImportGameLibrarySettings,
|
||||||
Feature.ImportOSCompatibility,
|
Feature.ImportOSCompatibility,
|
||||||
Feature.ImportUserPresence
|
Feature.ImportUserPresence,
|
||||||
|
Feature.ImportLocalSize,
|
||||||
|
Feature.ImportSubscriptions,
|
||||||
|
Feature.ImportSubscriptionGames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from galaxy.api.types import UserInfo
|
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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -15,12 +15,15 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_friends.return_value = async_return_value([
|
plugin.get_friends.return_value = [
|
||||||
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
UserInfo("5", "Ola", None, "https://profile.url/u5")
|
UserInfo("5", "Ola", None, "https://profile.url/u5"),
|
||||||
])
|
UserInfo("6", "Ola2", None),
|
||||||
|
UserInfo("7", "Ola3"),
|
||||||
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -30,7 +33,9 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"result": {
|
"result": {
|
||||||
"friend_info_list": [
|
"friend_info_list": [
|
||||||
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||||
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
|
{"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,9 +50,10 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_friends.side_effect = UnknownError()
|
plugin.get_friends.side_effect = UnknownError()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -57,6 +63,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"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -68,6 +75,7 @@ async def test_add_friend(plugin, write):
|
|||||||
|
|
||||||
plugin.add_friend(friend)
|
plugin.add_friend(friend)
|
||||||
await skip_loop()
|
await skip_loop()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -89,7 +97,7 @@ async def test_add_friend(plugin, write):
|
|||||||
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()
|
await skip_loop()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -107,7 +115,7 @@ async def test_update_friend_info(plugin, write):
|
|||||||
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
|
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
|
||||||
)
|
)
|
||||||
await skip_loop()
|
await skip_loop()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ from unittest.mock import call
|
|||||||
import pytest
|
import pytest
|
||||||
from galaxy.api.types import GameLibrarySettings
|
from galaxy.api.types import GameLibrarySettings
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_library_settings_success(plugin, read, write):
|
async def test_get_library_settings_success(plugin, read, write):
|
||||||
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
|
plugin.prepare_game_library_settings_context.return_value = "abc"
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -19,13 +18,14 @@ async def test_get_library_settings_success(plugin, read, write):
|
|||||||
"game_ids": ["3", "5", "7"]
|
"game_ids": ["3", "5", "7"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_game_library_settings.side_effect = [
|
plugin.get_game_library_settings.side_effect = [
|
||||||
async_return_value(GameLibrarySettings("3", None, True)),
|
GameLibrarySettings("3", None, True),
|
||||||
async_return_value(GameLibrarySettings("5", [], False)),
|
GameLibrarySettings("5", [], False),
|
||||||
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
|
GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None),
|
||||||
]
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_game_library_settings.assert_has_calls([
|
plugin.get_game_library_settings.assert_has_calls([
|
||||||
call("3", "abc"),
|
call("3", "abc"),
|
||||||
call("5", "abc"),
|
call("5", "abc"),
|
||||||
@@ -79,12 +79,12 @@ async def test_get_library_settings_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_library_settings_error(exception, code, message, plugin, read, write):
|
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)
|
plugin.prepare_game_library_settings_context.return_value = None
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -93,9 +93,10 @@ async def test_get_game_library_settings_error(exception, code, message, plugin,
|
|||||||
"game_ids": ["6"]
|
"game_ids": ["6"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_game_library_settings.side_effect = exception
|
plugin.get_game_library_settings.side_effect = exception
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_game_library_settings.assert_called()
|
plugin.get_game_library_settings.assert_called()
|
||||||
plugin.game_library_settings_import_complete.assert_called_once_with()
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -112,7 +113,8 @@ async def test_get_game_library_settings_error(exception, code, message, plugin,
|
|||||||
"game_id": "6",
|
"game_id": "6",
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -135,8 +137,9 @@ async def test_prepare_get_game_library_settings_context_error(plugin, read, wri
|
|||||||
"game_ids": ["6"]
|
"game_ids": ["6"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -144,7 +147,8 @@ async def test_prepare_get_game_library_settings_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"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -152,7 +156,7 @@ async def test_prepare_get_game_library_settings_context_error(plugin, read, wri
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_in_progress(plugin, read, write):
|
async def test_import_in_progress(plugin, read, write):
|
||||||
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
plugin.prepare_game_library_settings_context.return_value = None
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -172,12 +176,13 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
create_message(requests[0]),
|
||||||
async_return_value(create_message(requests[1])),
|
create_message(requests[1]),
|
||||||
async_return_value(b"", 10)
|
b""
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
messages = get_messages(write)
|
messages = get_messages(write)
|
||||||
assert {
|
assert {
|
||||||
@@ -190,7 +195,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
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ 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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_game_time_success(plugin, read, write):
|
async def test_get_game_time_success(plugin, read, write):
|
||||||
plugin.prepare_game_times_context.return_value = async_return_value("abc")
|
plugin.prepare_game_times_context.return_value = "abc"
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -19,13 +19,14 @@ async def test_get_game_time_success(plugin, read, write):
|
|||||||
"game_ids": ["3", "5", "7"]
|
"game_ids": ["3", "5", "7"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_game_time.side_effect = [
|
plugin.get_game_time.side_effect = [
|
||||||
async_return_value(GameTime("3", 60, 1549550504)),
|
GameTime("3", 60, 1549550504),
|
||||||
async_return_value(GameTime("5", 10, None)),
|
GameTime("5", 10, None),
|
||||||
async_return_value(GameTime("7", None, 1549550502)),
|
GameTime("7", None, 1549550502),
|
||||||
]
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_game_time.assert_has_calls([
|
plugin.get_game_time.assert_has_calls([
|
||||||
call("3", "abc"),
|
call("3", "abc"),
|
||||||
call("5", "abc"),
|
call("5", "abc"),
|
||||||
@@ -79,12 +80,12 @@ 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 = None
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -93,9 +94,10 @@ async def test_get_game_time_error(exception, code, message, plugin, read, write
|
|||||||
"game_ids": ["6"]
|
"game_ids": ["6"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_game_time.side_effect = exception
|
plugin.get_game_time.side_effect = exception
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_game_time.assert_called()
|
plugin.get_game_time.assert_called()
|
||||||
plugin.game_times_import_complete.assert_called_once_with()
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -112,7 +114,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,16 +138,17 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -152,7 +156,7 @@ async def test_prepare_get_game_time_context_error(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_in_progress(plugin, read, write):
|
async def test_import_in_progress(plugin, read, write):
|
||||||
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
plugin.prepare_game_times_context.return_value = None
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -172,13 +176,14 @@ async def test_import_in_progress(plugin, read, write):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
create_message(requests[0]),
|
||||||
async_return_value(create_message(requests[1])),
|
create_message(requests[1]),
|
||||||
async_return_value(b"", 10)
|
b""
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
messages = get_messages(write)
|
messages = get_messages(write)
|
||||||
assert {
|
assert {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -190,7 +195,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
|
||||||
|
|
||||||
@@ -200,7 +206,7 @@ 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()
|
await skip_loop()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +13,7 @@ async def test_success(plugin, read):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import delayed_return_value_iterable
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -20,8 +20,9 @@ async def test_get_capabilities(reader, writer, read, write):
|
|||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -44,7 +45,7 @@ async def test_shutdown(plugin, read, write):
|
|||||||
"id": "5",
|
"id": "5",
|
||||||
"method": "shutdown"
|
"method": "shutdown"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request))]
|
read.side_effect = [create_message(request)]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
await plugin.wait_closed()
|
await plugin.wait_closed()
|
||||||
plugin.shutdown.assert_called_with()
|
plugin.shutdown.assert_called_with()
|
||||||
@@ -64,8 +65,9 @@ async def test_ping(plugin, read, write):
|
|||||||
"id": "7",
|
"id": "7",
|
||||||
"method": "ping"
|
"method": "ping"
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -77,8 +79,9 @@ async def test_ping(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_tick_before_handshake(plugin, read):
|
async def test_tick_before_handshake(plugin, read):
|
||||||
read.side_effect = [async_return_value(b"")]
|
read.side_effect = [b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.tick.assert_not_called()
|
plugin.tick.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ async def test_tick_after_handshake(plugin, read):
|
|||||||
"method": "initialize_cache",
|
"method": "initialize_cache",
|
||||||
"params": {"data": {}}
|
"params": {"data": {}}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = delayed_return_value_iterable([create_message(request), b""], 1)
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.tick.assert_called_with()
|
plugin.tick.assert_called_with()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
@@ -15,6 +13,7 @@ async def test_success(plugin, read):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message
|
from tests import create_message
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -11,7 +9,8 @@ async def test_success(plugin, read):
|
|||||||
"method": "launch_platform_client"
|
"method": "launch_platform_client"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.launch_platform_client.return_value = async_return_value(None)
|
plugin.launch_platform_client.return_value = None
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.launch_platform_client.assert_called_with()
|
plugin.launch_platform_client.assert_called_with()
|
||||||
|
|||||||
@@ -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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -15,14 +15,15 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
plugin.get_local_games.return_value = async_return_value([
|
plugin.get_local_games.return_value = [
|
||||||
LocalGame("1", LocalGameState.Running),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", LocalGameState.Installed),
|
LocalGame("2", LocalGameState.Installed),
|
||||||
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
])
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -51,21 +52,22 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_local_games.side_effect = error()
|
plugin.get_local_games.side_effect = error()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -74,7 +76,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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
208
tests/test_local_size.py
Normal file
208
tests/test_local_size.py
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.errors import FailedParsingManifest
|
||||||
|
from galaxy.unittest.mock import delayed_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 = context
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": ["777", "13", "42"]}
|
||||||
|
}
|
||||||
|
read.side_effect = [create_message(request), b""]
|
||||||
|
plugin.get_local_size.side_effect = [
|
||||||
|
100000000000,
|
||||||
|
None,
|
||||||
|
3333333
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
prepare_local_size_context_response = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "11",
|
||||||
|
"result": None
|
||||||
|
}
|
||||||
|
# response for prepare_local_size_context may be returned before or after
|
||||||
|
# notifications about import success
|
||||||
|
assert prepare_local_size_context_response in messages
|
||||||
|
messages.remove(prepare_local_size_context_response)
|
||||||
|
assert messages == [
|
||||||
|
{
|
||||||
|
"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 = None
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": request_id,
|
||||||
|
"method": "start_local_size_import",
|
||||||
|
"params": {"game_ids": [game_id]}
|
||||||
|
}
|
||||||
|
read.side_effect = [create_message(request), b""]
|
||||||
|
plugin.get_local_size.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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 = [create_message(request), b""]
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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 = None
|
||||||
|
print("******", plugin.get_local_size)
|
||||||
|
|
||||||
|
plugin.get_local_size.side_effect = delayed_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 = [
|
||||||
|
create_message(requests[0]),
|
||||||
|
create_message(requests[1]),
|
||||||
|
b""
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
|
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,6 @@ from unittest.mock import call
|
|||||||
import pytest
|
import pytest
|
||||||
from galaxy.api.consts import OSCompatibility
|
from galaxy.api.consts import OSCompatibility
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -11,20 +10,21 @@ from tests import create_message, get_messages
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_os_compatibility_success(plugin, read, write):
|
async def test_get_os_compatibility_success(plugin, read, write):
|
||||||
context = "abc"
|
context = "abc"
|
||||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(context)
|
plugin.prepare_os_compatibility_context.return_value = context
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "11",
|
"id": "11",
|
||||||
"method": "start_os_compatibility_import",
|
"method": "start_os_compatibility_import",
|
||||||
"params": {"game_ids": ["666", "13", "42"]}
|
"params": {"game_ids": ["666", "13", "42"]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_os_compatibility.side_effect = [
|
plugin.get_os_compatibility.side_effect = [
|
||||||
async_return_value(OSCompatibility.Linux),
|
OSCompatibility.Linux,
|
||||||
async_return_value(None),
|
None,
|
||||||
async_return_value(OSCompatibility.Windows | OSCompatibility.MacOS),
|
OSCompatibility.Windows | OSCompatibility.MacOS,
|
||||||
]
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_os_compatibility.assert_has_calls([
|
plugin.get_os_compatibility.assert_has_calls([
|
||||||
call("666", context),
|
call("666", context),
|
||||||
call("13", context),
|
call("13", context),
|
||||||
@@ -71,23 +71,24 @@ async def test_get_os_compatibility_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_os_compatibility_error(exception, code, message, plugin, read, write):
|
async def test_get_os_compatibility_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
game_id = "6"
|
game_id = "6"
|
||||||
request_id = "55"
|
request_id = "55"
|
||||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
plugin.prepare_os_compatibility_context.return_value = None
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": "start_os_compatibility_import",
|
"method": "start_os_compatibility_import",
|
||||||
"params": {"game_ids": [game_id]}
|
"params": {"game_ids": [game_id]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_os_compatibility.side_effect = exception
|
plugin.get_os_compatibility.side_effect = exception
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_os_compatibility.assert_called()
|
plugin.get_os_compatibility.assert_called()
|
||||||
plugin.os_compatibility_import_complete.assert_called_once_with()
|
plugin.os_compatibility_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -104,7 +105,8 @@ async def test_get_os_compatibility_error(exception, code, message, plugin, read
|
|||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {"internal_type": internal_type}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -126,8 +128,9 @@ async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
|||||||
"method": "start_os_compatibility_import",
|
"method": "start_os_compatibility_import",
|
||||||
"params": {"game_ids": ["6"]}
|
"params": {"game_ids": ["6"]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -135,7 +138,8 @@ async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {"internal_type": "BackendError"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -143,7 +147,7 @@ async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_already_in_progress_error(plugin, read, write):
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
|
plugin.prepare_os_compatibility_context.return_value = None
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -163,12 +167,13 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
create_message(requests[0]),
|
||||||
async_return_value(create_message(requests[1])),
|
create_message(requests[1]),
|
||||||
async_return_value(b"", 10)
|
b""
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
|
||||||
responses = get_messages(write)
|
responses = get_messages(write)
|
||||||
assert {
|
assert {
|
||||||
@@ -181,7 +186,8 @@ async def test_import_already_in_progress_error(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 responses
|
} 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, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
plugin.get_owned_games.return_value = async_return_value([
|
plugin.get_owned_games.return_value = [
|
||||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
@@ -27,8 +27,9 @@ async def test_success(plugin, read, write):
|
|||||||
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||||
],
|
],
|
||||||
LicenseInfo(LicenseType.SinglePurchase, None))
|
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
])
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -80,9 +81,10 @@ 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"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_owned_games.side_effect = UnknownError()
|
plugin.get_owned_games.side_effect = UnknownError()
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
@@ -90,7 +92,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"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
@@ -42,10 +42,11 @@ async def test_initialize_cache(plugin, read, write, cache_data):
|
|||||||
"method": "initialize_cache",
|
"method": "initialize_cache",
|
||||||
"params": {"data": cache_data}
|
"params": {"data": cache_data}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.handshake_complete.assert_called_once_with()
|
plugin.handshake_complete.assert_called_once_with()
|
||||||
assert cache_data == plugin.persistent_cache
|
assert cache_data == plugin.persistent_cache
|
||||||
assert_rpc_response(write, response_id=request_id)
|
assert_rpc_response(write, response_id=request_id)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
)
|
)
|
||||||
from galaxy.api.jsonrpc import JsonRpcError
|
from galaxy.api.jsonrpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_refresh_credentials_success(plugin, read, write):
|
async def test_refresh_credentials_success(plugin, read, write):
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ async def test_refresh_credentials_success(plugin, read, write):
|
|||||||
"result": refreshed_credentials
|
"result": refreshed_credentials
|
||||||
}
|
}
|
||||||
# 2 loop iterations delay is to force sending response after request has been sent
|
# 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)]
|
read.side_effect = [create_message(response), b""]
|
||||||
|
|
||||||
result = await plugin.refresh_credentials({}, False)
|
result = await plugin.refresh_credentials({}, False)
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
@@ -40,7 +41,7 @@ async def test_refresh_credentials_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception", [
|
@pytest.mark.parametrize("exception", [
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
])
|
])
|
||||||
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
||||||
|
|
||||||
@@ -53,12 +54,13 @@ async def test_refresh_credentials_failure(exception, plugin, read, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# 2 loop iterations delay is to force sending response after request has been sent
|
# 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)]
|
read.side_effect = [create_message(response), b""]
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError) as e:
|
with pytest.raises(JsonRpcError) as e:
|
||||||
await plugin.refresh_credentials({}, False)
|
await plugin.refresh_credentials({}, False)
|
||||||
|
|
||||||
assert error == e.value
|
# 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) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message
|
from tests import create_message
|
||||||
|
|
||||||
@@ -11,7 +10,8 @@ async def test_success(plugin, read):
|
|||||||
"method": "shutdown_platform_client"
|
"method": "shutdown_platform_client"
|
||||||
}
|
}
|
||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.shutdown_platform_client.return_value = async_return_value(None)
|
plugin.shutdown_platform_client.return_value = None
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.shutdown_platform_client.assert_called_with()
|
plugin.shutdown_platform_client.assert_called_with()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.reader import StreamLineReader
|
from galaxy.reader import StreamLineReader
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@@ -11,14 +10,14 @@ def stream_line_reader(reader):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_message(stream_line_reader, read):
|
async def test_message(stream_line_reader, read):
|
||||||
read.return_value = async_return_value(b"a\n")
|
read.return_value = b"a\n"
|
||||||
assert await stream_line_reader.readline() == b"a"
|
assert await stream_line_reader.readline() == b"a"
|
||||||
read.assert_called_once()
|
read.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_separate_messages(stream_line_reader, read):
|
async def test_separate_messages(stream_line_reader, read):
|
||||||
read.side_effect = [async_return_value(b"a\n"), async_return_value(b"b\n")]
|
read.side_effect = [b"a\n", b"b\n"]
|
||||||
assert await stream_line_reader.readline() == b"a"
|
assert await stream_line_reader.readline() == b"a"
|
||||||
assert await stream_line_reader.readline() == b"b"
|
assert await stream_line_reader.readline() == b"b"
|
||||||
assert read.call_count == 2
|
assert read.call_count == 2
|
||||||
@@ -26,7 +25,7 @@ async def test_separate_messages(stream_line_reader, read):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_connected_messages(stream_line_reader, read):
|
async def test_connected_messages(stream_line_reader, read):
|
||||||
read.return_value = async_return_value(b"a\nb\n")
|
read.return_value = b"a\nb\n"
|
||||||
assert await stream_line_reader.readline() == b"a"
|
assert await stream_line_reader.readline() == b"a"
|
||||||
assert await stream_line_reader.readline() == b"b"
|
assert await stream_line_reader.readline() == b"b"
|
||||||
read.assert_called_once()
|
read.assert_called_once()
|
||||||
@@ -34,13 +33,13 @@ async def test_connected_messages(stream_line_reader, read):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cut_message(stream_line_reader, read):
|
async def test_cut_message(stream_line_reader, read):
|
||||||
read.side_effect = [async_return_value(b"a"), async_return_value(b"b\n")]
|
read.side_effect = [b"a", b"b\n"]
|
||||||
assert await stream_line_reader.readline() == b"ab"
|
assert await stream_line_reader.readline() == b"ab"
|
||||||
assert read.call_count == 2
|
assert read.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_half_message(stream_line_reader, read):
|
async def test_half_message(stream_line_reader, read):
|
||||||
read.side_effect = [async_return_value(b"a"), async_return_value(b"")]
|
read.side_effect = [b"a", b""]
|
||||||
assert await stream_line_reader.readline() == b""
|
assert await stream_line_reader.readline() == b""
|
||||||
assert read.call_count == 2
|
assert read.call_count == 2
|
||||||
|
|||||||
364
tests/test_subscriptions.py
Normal file
364
tests/test_subscriptions.py
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.api.types import Subscription, SubscriptionGame
|
||||||
|
from galaxy.api.consts import SubscriptionDiscovery
|
||||||
|
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
|
||||||
|
|
||||||
|
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 = [create_message(request), b""]
|
||||||
|
|
||||||
|
plugin.get_subscriptions.return_value = [
|
||||||
|
Subscription("1"),
|
||||||
|
Subscription("2", False, subscription_discovery=SubscriptionDiscovery.AUTOMATIC),
|
||||||
|
Subscription("3", True, 1580899100, SubscriptionDiscovery.USER_ENABLED)
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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 = [create_message(request), b""]
|
||||||
|
plugin.get_subscriptions.side_effect = error()
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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 = 5
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
|
# Create an async generator that yields the games
|
||||||
|
async def mock_subscription_games_generator(subscription_name, context):
|
||||||
|
games = [
|
||||||
|
# first chunk of the 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)],
|
||||||
|
# second chunk of the games
|
||||||
|
[SubscriptionGame(game_title="game D", game_id="game_D", start_time=1548495632, end_time=1548495633)],
|
||||||
|
]
|
||||||
|
for game in games:
|
||||||
|
yield game
|
||||||
|
|
||||||
|
plugin.get_subscription_games.side_effect = mock_subscription_games_generator
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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.assert_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
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "subscription_games_import_success",
|
||||||
|
"params": {
|
||||||
|
"subscription_name": "sub_a",
|
||||||
|
"subscription_games": [
|
||||||
|
{
|
||||||
|
"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 = 5
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [create_message(request), b""]
|
||||||
|
|
||||||
|
async def sub_games():
|
||||||
|
yield None
|
||||||
|
|
||||||
|
plugin.get_subscription_games.return_value = sub_games()
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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.assert_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 = None
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_subscription_games_import",
|
||||||
|
"params": {
|
||||||
|
"subscription_names": ["sub_a"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [create_message(request), b""]
|
||||||
|
plugin.get_subscription_games.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
plugin.get_subscription_games.assert_called()
|
||||||
|
plugin.subscription_games_import_complete.assert_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 = [create_message(request), b""]
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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 = 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 = [
|
||||||
|
create_message(requests[0]),
|
||||||
|
create_message(requests[1]),
|
||||||
|
b""
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.unittest.mock import async_return_value
|
|
||||||
|
|
||||||
from tests import create_message
|
from tests import create_message
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -13,7 +11,8 @@ async def test_success(plugin, read):
|
|||||||
"game_id": "3"
|
"game_id": "3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_owned_games.return_value = None
|
plugin.get_owned_games.return_value = None
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -5,62 +5,63 @@ import pytest
|
|||||||
from galaxy.api.consts import PresenceState
|
from galaxy.api.consts import PresenceState
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.api.types import UserPresence
|
from galaxy.api.types import UserPresence
|
||||||
from galaxy.unittest.mock import async_return_value, skip_loop
|
from galaxy.unittest.mock import skip_loop
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_user_presence_success(plugin, read, write):
|
async def test_get_user_presence_success(plugin, read, write):
|
||||||
context = "abc"
|
context = "abc"
|
||||||
user_ids = ["666", "13", "42", "69", "22"]
|
user_id_list = ["666", "13", "42", "69", "22"]
|
||||||
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
plugin.prepare_user_presence_context.return_value = context
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "11",
|
"id": "11",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": user_ids}
|
"params": {"user_id_list": user_id_list}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_user_presence.side_effect = [
|
plugin.get_user_presence.side_effect = [
|
||||||
async_return_value(UserPresence(
|
UserPresence(
|
||||||
PresenceState.Unknown,
|
PresenceState.Unknown,
|
||||||
"game-id1",
|
"game-id1",
|
||||||
None,
|
None,
|
||||||
"unknown state",
|
"unknown state",
|
||||||
None
|
None
|
||||||
)),
|
),
|
||||||
async_return_value(UserPresence(
|
UserPresence(
|
||||||
PresenceState.Offline,
|
PresenceState.Offline,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
"Going to grandma's house",
|
"Going to grandma's house",
|
||||||
None
|
None
|
||||||
)),
|
),
|
||||||
async_return_value(UserPresence(
|
UserPresence(
|
||||||
PresenceState.Online,
|
PresenceState.Online,
|
||||||
"game-id3",
|
"game-id3",
|
||||||
"game-title3",
|
"game-title3",
|
||||||
"Pew pew",
|
"Pew pew",
|
||||||
None
|
None
|
||||||
)),
|
),
|
||||||
async_return_value(UserPresence(
|
UserPresence(
|
||||||
PresenceState.Away,
|
PresenceState.Away,
|
||||||
None,
|
None,
|
||||||
"game-title4",
|
"game-title4",
|
||||||
"AFKKTHXBY",
|
"AFKKTHXBY",
|
||||||
None
|
None
|
||||||
)),
|
),
|
||||||
async_return_value(UserPresence(
|
UserPresence(
|
||||||
PresenceState.Away,
|
PresenceState.Away,
|
||||||
None,
|
None,
|
||||||
"game-title5",
|
"game-title5",
|
||||||
None,
|
None,
|
||||||
"Playing game-title5: In Menu"
|
"Playing game-title5: In Menu"
|
||||||
)),
|
),
|
||||||
]
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_user_presence.assert_has_calls([
|
plugin.get_user_presence.assert_has_calls([
|
||||||
call(user_id, context) for user_id in user_ids
|
call(user_id, context) for user_id in user_id_list
|
||||||
])
|
])
|
||||||
plugin.user_presence_import_complete.assert_called_once_with()
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -139,23 +140,24 @@ async def test_get_user_presence_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_user_presence_error(exception, code, message, plugin, read, write):
|
async def test_get_user_presence_error(exception, code, message, internal_type, plugin, read, write):
|
||||||
user_id = "69"
|
user_id = "69"
|
||||||
request_id = "55"
|
request_id = "55"
|
||||||
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
plugin.prepare_user_presence_context.return_value = None
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": [user_id]}
|
"params": {"user_id_list": [user_id]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
plugin.get_user_presence.side_effect = exception
|
plugin.get_user_presence.side_effect = exception
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.get_user_presence.assert_called()
|
plugin.get_user_presence.assert_called()
|
||||||
plugin.user_presence_import_complete.assert_called_once_with()
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -172,7 +174,10 @@ async def test_get_user_presence_error(exception, code, message, plugin, read, w
|
|||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": code,
|
"code": code,
|
||||||
"message": message
|
"message": message,
|
||||||
|
"data": {
|
||||||
|
"internal_type": internal_type
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -192,18 +197,21 @@ async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": ["6"]}
|
"params": {"user_id_list": ["6"]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [create_message(request), b""]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
assert get_messages(write) == [
|
assert get_messages(write) == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": {
|
||||||
"code": 4,
|
"code": 4,
|
||||||
"message": "Backend error"
|
"message": "Backend error",
|
||||||
|
"data": {
|
||||||
|
"internal_type": "BackendError"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -211,14 +219,14 @@ async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_already_in_progress_error(plugin, read, write):
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
plugin.prepare_user_presence_context.return_value = async_return_value(None)
|
plugin.prepare_user_presence_context.return_value = None
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {
|
"params": {
|
||||||
"user_ids": ["42"]
|
"user_id_list": ["42"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -226,18 +234,18 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {
|
"params": {
|
||||||
"user_ids": ["666"]
|
"user_id_list": ["666"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
read.side_effect = [
|
read.side_effect = [
|
||||||
async_return_value(create_message(requests[0])),
|
create_message(requests[0]),
|
||||||
async_return_value(create_message(requests[1])),
|
create_message(requests[1]),
|
||||||
async_return_value(b"", 10)
|
b""
|
||||||
]
|
]
|
||||||
|
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
responses = get_messages(write)
|
responses = get_messages(write)
|
||||||
assert {
|
assert {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -249,7 +257,8 @@ async def test_import_already_in_progress_error(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 responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user