mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-02 11:58:14 -05:00
Compare commits
93 Commits
deployed_0
...
0.52
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e2aa8c4a0 | ||
|
|
f57e03db2d | ||
|
|
66085e2239 | ||
|
|
4d3c9b78c4 | ||
|
|
392e4c5f68 | ||
|
|
4d6d3b8eb2 | ||
|
|
d5610221a9 | ||
|
|
aa7b398d3b | ||
|
|
8d6ec500f9 | ||
|
|
bab0be9994 | ||
|
|
0294e2a1f1 | ||
|
|
0ab00e4119 | ||
|
|
b20fce057b | ||
|
|
dec59f47dd | ||
|
|
ca85b2428b | ||
|
|
d8a00d58a6 | ||
|
|
d4cd1cedfd | ||
|
|
161122b94d | ||
|
|
cec36695b6 | ||
|
|
4cc8be8f5d | ||
|
|
f5eb32aa19 | ||
|
|
a76345ff6b | ||
|
|
c3bbeee54d | ||
|
|
13a3f7577b | ||
|
|
f5b9adfbd5 | ||
|
|
e33dd09a8d | ||
|
|
f4ea2af924 | ||
|
|
3bcc674518 | ||
|
|
b14595bef5 | ||
|
|
a9acb7a0db | ||
|
|
d95aacb9d8 | ||
|
|
49ae2beab9 | ||
|
|
c9e190772c | ||
|
|
789415d31b | ||
|
|
223adf6a38 | ||
|
|
bfb63a42bd | ||
|
|
53b3062719 | ||
|
|
49eb10ac8a | ||
|
|
10ecef791f | ||
|
|
ce193f39bc | ||
|
|
630d878a3c | ||
|
|
cc63c24bde | ||
|
|
0d0f657240 | ||
|
|
33c630225d | ||
|
|
f4bd18a8ab | ||
|
|
fa4541434f | ||
|
|
c083a3089a | ||
|
|
f6b5a12b24 | ||
|
|
8a67747df5 | ||
|
|
2db9d0f383 | ||
|
|
9d93762867 | ||
|
|
c364b716f4 | ||
|
|
48e1782484 | ||
|
|
ff30675a25 | ||
|
|
7b3965ff4b | ||
|
|
2ebdfabd9b | ||
|
|
4e1ea8056d | ||
|
|
67e8681de6 | ||
|
|
77d742ce18 | ||
|
|
f1fd00fcd3 | ||
|
|
692bdbf370 | ||
|
|
207b1e1313 | ||
|
|
05042fe430 | ||
|
|
58b17d94fa | ||
|
|
be03c83d45 | ||
|
|
1edf4ff5ba | ||
|
|
8d210e7f3e | ||
|
|
c07c7a2c2a | ||
|
|
9d5d48032e | ||
|
|
cb1a5fa5e4 | ||
|
|
179fd147c1 | ||
|
|
4790238638 | ||
|
|
5d90ba0c09 | ||
|
|
d74ed3a4b5 | ||
|
|
d6f2d00fb9 | ||
|
|
ce9f33f5d0 | ||
|
|
b28fc60088 | ||
|
|
be3d3bb7e5 | ||
|
|
6dec4a99d3 | ||
|
|
69ffef2fde | ||
|
|
7789927ed9 | ||
|
|
da59670d8e | ||
|
|
ed1049b543 | ||
|
|
e2f26271cb | ||
|
|
9e8748b032 | ||
|
|
bb482d4ed6 | ||
|
|
3bd0b71ab3 | ||
|
|
192d655d51 | ||
|
|
6c6dc42cd6 | ||
|
|
f97b6c8971 | ||
|
|
0af7387342 | ||
|
|
60fab25a55 | ||
|
|
6f717a1e31 |
@@ -14,13 +14,19 @@ test_package:
|
|||||||
|
|
||||||
deploy_package:
|
deploy_package:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
|
variables:
|
||||||
|
TWINE_USERNAME: $PYPI_USERNAME
|
||||||
|
TWINE_PASSWORD: $PYPI_PASSWORD
|
||||||
script:
|
script:
|
||||||
|
- pip install twine wheel
|
||||||
|
- rm -rf dist
|
||||||
- export VERSION=$(python setup.py --version)
|
- export VERSION=$(python setup.py --version)
|
||||||
- python setup.py sdist --formats=gztar upload -r gog-pypi
|
- python setup.py sdist --formats=gztar bdist_wheel
|
||||||
|
- 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}"
|
||||||
when: manual
|
when: manual
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
except:
|
except:
|
||||||
- tags
|
- tags
|
||||||
|
|||||||
8
.travis.yml
Normal file
8
.travis.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
dist: xenial # required for Python >= 3.7
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "3.7"
|
||||||
|
install:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
script:
|
||||||
|
- pytest
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 GOG sp. z o.o.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
82
PLATFORM_IDs.md
Normal file
82
PLATFORM_IDs.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
### PLATFORM ID LIST
|
||||||
|
|
||||||
|
Platform ID list for GOG Galaxy 2.0 Integrations
|
||||||
|
|
||||||
|
| ID | Name |
|
||||||
|
| --- | --- |
|
||||||
|
| steam | Steam |
|
||||||
|
| psn | PlayStation Network |
|
||||||
|
| xboxone | Xbox Live |
|
||||||
|
| generic | Manually added games |
|
||||||
|
| origin | Origin |
|
||||||
|
| uplay | Uplay |
|
||||||
|
| battlenet | Battle.net |
|
||||||
|
| epic | Epic Games Store |
|
||||||
|
| bethesda | Bethesda.net |
|
||||||
|
| paradox | Paradox Plaza |
|
||||||
|
| humble | Humble Bundle |
|
||||||
|
| kartridge | Kartridge |
|
||||||
|
| itch | Itch.io |
|
||||||
|
| nswitch | Nintendo Switch |
|
||||||
|
| nwiiu | Nintendo Wii U |
|
||||||
|
| nwii | Nintendo Wii |
|
||||||
|
| ncube | Nintendo GameCube |
|
||||||
|
| riot | Riot |
|
||||||
|
| wargaming | Wargaming |
|
||||||
|
| ngameboy | Nintendo Game Boy |
|
||||||
|
| atari | Atari |
|
||||||
|
| amiga | Amiga |
|
||||||
|
| snes | SNES |
|
||||||
|
| beamdog | Beamdog |
|
||||||
|
| d2d | Direct2Drive |
|
||||||
|
| discord | Discord |
|
||||||
|
| dotemu | DotEmu |
|
||||||
|
| gamehouse | GameHouse |
|
||||||
|
| gmg | Green Man Gaming |
|
||||||
|
| weplay | WePlay |
|
||||||
|
| zx | Zx Spectrum PC |
|
||||||
|
| vision | ColecoVision |
|
||||||
|
| nes | NES |
|
||||||
|
| sms | Sega Master System |
|
||||||
|
| c64 | Commodore 64 |
|
||||||
|
| pce | PC Engine |
|
||||||
|
| segag | Sega Genesis |
|
||||||
|
| neo | NeoGeo |
|
||||||
|
| sega32 | Sega 32X |
|
||||||
|
| segacd | Sega CD |
|
||||||
|
| 3do | 3DO Interactive |
|
||||||
|
| saturn | SegaSaturn |
|
||||||
|
| psx | Sony PlayStation |
|
||||||
|
| ps2 | Sony PlayStation 2 |
|
||||||
|
| n64 | Nintendo64 |
|
||||||
|
| jaguar | Atari Jaguar |
|
||||||
|
| dc | Sega Dreamcast |
|
||||||
|
| xboxog | Original Xbox games |
|
||||||
|
| amazon | Amazon |
|
||||||
|
| gg | GamersGate |
|
||||||
|
| egg | Newegg |
|
||||||
|
| bb | BestBuy |
|
||||||
|
| gameuk | Game UK |
|
||||||
|
| fanatical | Fanatical store |
|
||||||
|
| playasia | Play-Asia |
|
||||||
|
| stadia | Google Stadia |
|
||||||
|
| arc | ARC |
|
||||||
|
| eso | ESO |
|
||||||
|
| glyph | Trion World |
|
||||||
|
| aionl | Aion: Legions of War |
|
||||||
|
| aion | Aion |
|
||||||
|
| blade | Blade & Soul |
|
||||||
|
| gw | Guild Wars |
|
||||||
|
| gw2 | Guild Wars 2 |
|
||||||
|
| lin2 | Lineage 2 |
|
||||||
|
| ffxi | Final Fantasy XI |
|
||||||
|
| ffxiv | Final Fantasy XIV |
|
||||||
|
| totalwar | Total War |
|
||||||
|
| winstore | Windows Store |
|
||||||
|
| elites | Elite Dangerous |
|
||||||
|
| star | Star Citizen |
|
||||||
|
| psp | PlayStation Portable |
|
||||||
|
| psvita | PlayStation Vita |
|
||||||
|
| nds | Nintendo DS |
|
||||||
|
| 3ds | Nintendo 3DS |
|
||||||
|
| pathofexile | Path of Exile |
|
||||||
78
README.md
78
README.md
@@ -1,29 +1,35 @@
|
|||||||
# GOG Galaxy Integrations Python API
|
# GOG Galaxy Integrations Python API
|
||||||
|
|
||||||
This Python library allows 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.0.
|
||||||
|
|
||||||
- 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>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that which needs to communicate with main instance of GOG Galaxy 2.0.
|
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.
|
||||||
|
|
||||||
The provided features are:
|
The provided features are:
|
||||||
|
|
||||||
- multistep authorisation using a browser built into GOG Galaxy 2.0
|
- multistep authorization using a browser built into GOG Galaxy 2.0
|
||||||
- support for GOG Galaxy 2.0 features:
|
- support for GOG Galaxy 2.0 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
|
||||||
- importing friends lists and statuses
|
- importing friends lists and statuses
|
||||||
- importing friends recomendations list
|
- importing friends recommendations list
|
||||||
- receiving and sending chat messages
|
- receiving and sending chat messages
|
||||||
- cache storage
|
- cache storage
|
||||||
|
|
||||||
|
## Platform Id's
|
||||||
|
|
||||||
|
Each integration can implement only one platform. Each integration must declare which platform it's integrating.
|
||||||
|
|
||||||
|
[List of possible Platform IDs](PLATFORM_IDs.md)
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
Eeach 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 in the appropriate times.
|
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 of those method can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
|
Each of those methods can raise exceptions inherited from the :exc:`~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: :meth:`~galaxy.api.plugin.Plugin.update_local_game_status`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -55,8 +61,25 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
The client has a built-in Python 3.7 interpreter, so the integrations are delivered as `.py` files.
|
The client has a built-in Python 3.7 interpreter, so integrations are delivered as Python modules.
|
||||||
The additional `manifest.json` file is required:
|
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).
|
||||||
|
|
||||||
|
### Lookup directory
|
||||||
|
|
||||||
|
<a name="deploy-location"></a>
|
||||||
|
|
||||||
|
- Windows:
|
||||||
|
|
||||||
|
`%localappdata%\GOG.com\Galaxy\plugins\installed`
|
||||||
|
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
||||||
|
|
||||||
|
### Manifest
|
||||||
|
|
||||||
|
<a name="deploy-manifest"></a>
|
||||||
|
Obligatory JSON file to be placed in an integration folder.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -71,6 +94,35 @@ The additional `manifest.json` file is required:
|
|||||||
"script": "plugin.py"
|
"script": "plugin.py"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| property | description |
|
||||||
|
|---------------|---|
|
||||||
|
| `guid` | |
|
||||||
|
| `description` | |
|
||||||
|
| `url` | |
|
||||||
|
| `script` | path of the entry point module, relative to the integration folder |
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```pip install DEP --target DIR --implementation cp --python-version 37```
|
||||||
|
|
||||||
|
For example, a plugin that uses *requests* could have the following structure:
|
||||||
|
|
||||||
|
<a name="deploy-structure-example"></a>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
installed
|
||||||
|
└── my_integration
|
||||||
|
├── galaxy
|
||||||
|
│ └── api
|
||||||
|
├── requests
|
||||||
|
│ └── ...
|
||||||
|
├── plugin.py
|
||||||
|
└── manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
## 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.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.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
Sphinx==2.0.1
|
Sphinx==2.0.1
|
||||||
sphinx-rtd-theme==0.4.3
|
sphinx-rtd-theme==0.4.3
|
||||||
sphinx-autodoc-typehints==1.6.0
|
sphinx-autodoc-typehints==1.6.0
|
||||||
|
sphinxcontrib-asyncio==0.2.0
|
||||||
m2r==0.2.1
|
m2r==0.2.1
|
||||||
@@ -32,12 +32,13 @@ release = _version
|
|||||||
# ones.
|
# ones.
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinxcontrib.asyncio',
|
||||||
'sphinx_autodoc_typehints',
|
'sphinx_autodoc_typehints',
|
||||||
'm2r' # mdinclude directive for makrdown files
|
'm2r' # mdinclude directive for makrdown files
|
||||||
]
|
]
|
||||||
autodoc_member_order = 'bysource'
|
autodoc_member_order = 'bysource'
|
||||||
autodoc_inherit_docstrings = False
|
autodoc_inherit_docstrings = False
|
||||||
autodoc_mock_imports = ["galaxy.http"]
|
autodoc_mock_imports = ["aiohttp"]
|
||||||
|
|
||||||
set_type_checking_flag = True
|
set_type_checking_flag = True
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ templates_path = ['_templates']
|
|||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = []
|
exclude_patterns = [] # type: ignore
|
||||||
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|||||||
8
docs/source/galaxy.http.rst
Normal file
8
docs/source/galaxy.http.rst
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
galaxy.http
|
||||||
|
=================
|
||||||
|
|
||||||
|
.. automodule:: galaxy.http
|
||||||
|
:members:
|
||||||
|
:special-members: __init__
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
@@ -6,7 +6,9 @@ GOG Galaxy Integrations Python API
|
|||||||
:includehidden:
|
:includehidden:
|
||||||
|
|
||||||
Overview <overview>
|
Overview <overview>
|
||||||
API <galaxy.api>
|
galaxy.api
|
||||||
|
galaxy.http
|
||||||
|
Platform ID's <platforms>
|
||||||
|
|
||||||
Index
|
Index
|
||||||
-------------------
|
-------------------
|
||||||
|
|||||||
@@ -5,3 +5,11 @@
|
|||||||
|
|
||||||
.. mdinclude:: ../../README.md
|
.. mdinclude:: ../../README.md
|
||||||
:start-line: 6
|
:start-line: 6
|
||||||
|
:end-line: 26
|
||||||
|
|
||||||
|
.. excluding Platforms Id's link
|
||||||
|
|
||||||
|
:ref:`platforms-link`
|
||||||
|
|
||||||
|
.. mdinclude:: ../../README.md
|
||||||
|
:start-line: 28
|
||||||
|
|||||||
2
docs/source/platforms.rst
Normal file
2
docs/source/platforms.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.. _platforms-link:
|
||||||
|
.. mdinclude:: ../../PLATFORM_IDs.md
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
stage('Upload to github')
|
|
||||||
{
|
|
||||||
node('ActiveClientMacosxBuilder') {
|
|
||||||
deleteDir()
|
|
||||||
checkout scm
|
|
||||||
withPythonEnv('/usr/local/bin/python3.7') {
|
|
||||||
withCredentials([string(credentialsId: 'github_goggalaxy', variable: 'GITHUB_TOKEN')]) {
|
|
||||||
sh 'pip install -r jenkins/requirements.txt'
|
|
||||||
def version = sh(returnStdout: true, script: 'python setup.py --version').trim()
|
|
||||||
sh "python jenkins/release.py $version"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from galaxy.github.exporter import transfer_repo
|
|
||||||
|
|
||||||
GITHUB_USERNAME = "goggalaxy"
|
|
||||||
GITHUB_EMAIL = "galaxy-sdk@gog.com"
|
|
||||||
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
|
||||||
GITHUB_REPO_NAME = "galaxy-integrations-python-api"
|
|
||||||
SOURCE_BRANCH = os.environ["GIT_REFSPEC"]
|
|
||||||
|
|
||||||
GITLAB_USERNAME = "galaxy-client"
|
|
||||||
GITLAB_REPO_NAME = "galaxy-plugin-api"
|
|
||||||
|
|
||||||
def version_provider(_):
|
|
||||||
return sys.argv[1]
|
|
||||||
|
|
||||||
gh_version = transfer_repo(
|
|
||||||
version_provider=version_provider,
|
|
||||||
source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME),
|
|
||||||
source_include_elements=["src", "docs", "tests", "requirements.txt", ".readthedocs.yml" ".gitignore", "*.md", "pytest.ini", "setup.py"],
|
|
||||||
source_branch=SOURCE_BRANCH,
|
|
||||||
dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME),
|
|
||||||
dest_branch="master",
|
|
||||||
dest_user_email=GITHUB_EMAIL,
|
|
||||||
dest_user_name="GOG Galaxy SDK Team"
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
addopts = --flakes
|
addopts = --flakes --mypy
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
pytest==4.2.0
|
pytest==4.2.0
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-mock==1.10.3
|
pytest-mock==1.10.3
|
||||||
|
pytest-mypy==0.4.1
|
||||||
pytest-flakes==4.0.0
|
pytest-flakes==4.0.0
|
||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
# because of pip bug https://github.com/pypa/pip/issues/4780
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
|
psutil==5.6.3; sys_platform == 'darwin'
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.33",
|
version="0.52",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -13,6 +13,83 @@ class Platform(Enum):
|
|||||||
Uplay = "uplay"
|
Uplay = "uplay"
|
||||||
Battlenet = "battlenet"
|
Battlenet = "battlenet"
|
||||||
Epic = "epic"
|
Epic = "epic"
|
||||||
|
Bethesda = "bethesda"
|
||||||
|
ParadoxPlaza = "paradox"
|
||||||
|
HumbleBundle = "humble"
|
||||||
|
Kartridge = "kartridge"
|
||||||
|
ItchIo = "itch"
|
||||||
|
NintendoSwitch = "nswitch"
|
||||||
|
NintendoWiiU = "nwiiu"
|
||||||
|
NintendoWii = "nwii"
|
||||||
|
NintendoGameCube = "ncube"
|
||||||
|
RiotGames = "riot"
|
||||||
|
Wargaming = "wargaming"
|
||||||
|
NintendoGameBoy = "ngameboy"
|
||||||
|
Atari = "atari"
|
||||||
|
Amiga = "amiga"
|
||||||
|
SuperNintendoEntertainmentSystem = "snes"
|
||||||
|
Beamdog = "beamdog"
|
||||||
|
Direct2Drive = "d2d"
|
||||||
|
Discord = "discord"
|
||||||
|
DotEmu = "dotemu"
|
||||||
|
GameHouse = "gamehouse"
|
||||||
|
GreenManGaming = "gmg"
|
||||||
|
WePlay = "weplay"
|
||||||
|
ZxSpectrum = "zx"
|
||||||
|
ColecoVision = "vision"
|
||||||
|
NintendoEntertainmentSystem = "nes"
|
||||||
|
SegaMasterSystem = "sms"
|
||||||
|
Commodore64 = "c64"
|
||||||
|
PcEngine = "pce"
|
||||||
|
SegaGenesis = "segag"
|
||||||
|
NeoGeo = "neo"
|
||||||
|
Sega32X = "sega32"
|
||||||
|
SegaCd = "segacd"
|
||||||
|
_3Do = "3do"
|
||||||
|
SegaSaturn = "saturn"
|
||||||
|
PlayStation = "psx"
|
||||||
|
PlayStation2 = "ps2"
|
||||||
|
Nintendo64 = "n64"
|
||||||
|
AtariJaguar = "jaguar"
|
||||||
|
SegaDreamcast = "dc"
|
||||||
|
Xbox = "xboxog"
|
||||||
|
Amazon = "amazon"
|
||||||
|
GamersGate = "gg"
|
||||||
|
Newegg = "egg"
|
||||||
|
BestBuy = "bb"
|
||||||
|
GameUk = "gameuk"
|
||||||
|
Fanatical = "fanatical"
|
||||||
|
PlayAsia = "playasia"
|
||||||
|
Stadia = "stadia"
|
||||||
|
Arc = "arc"
|
||||||
|
ElderScrollsOnline = "eso"
|
||||||
|
Glyph = "glyph"
|
||||||
|
AionLegionsOfWar = "aionl"
|
||||||
|
Aion = "aion"
|
||||||
|
BladeAndSoul = "blade"
|
||||||
|
GuildWars = "gw"
|
||||||
|
GuildWars2 = "gw2"
|
||||||
|
Lineage2 = "lin2"
|
||||||
|
FinalFantasy11 = "ffxi"
|
||||||
|
FinalFantasy14 = "ffxiv"
|
||||||
|
TotalWar = "totalwar"
|
||||||
|
WindowsStore = "winstore"
|
||||||
|
EliteDangerous = "elites"
|
||||||
|
StarCitizen = "star"
|
||||||
|
PlayStationPortable = "psp"
|
||||||
|
PlayStationVita = "psvita"
|
||||||
|
NintendoDs = "nds"
|
||||||
|
Nintendo3Ds = "3ds"
|
||||||
|
PathOfExile = "pathofexile"
|
||||||
|
Twitch = "twitch"
|
||||||
|
Minecraft = "minecraft"
|
||||||
|
GameSessions = "gamesessions"
|
||||||
|
Nuuvem = "nuuvem"
|
||||||
|
FXStore = "fxstore"
|
||||||
|
IndieGala = "indiegala"
|
||||||
|
Playfire = "playfire"
|
||||||
|
Oculus = "oculus"
|
||||||
|
Test = "test"
|
||||||
|
|
||||||
|
|
||||||
class Feature(Enum):
|
class Feature(Enum):
|
||||||
@@ -31,6 +108,9 @@ class Feature(Enum):
|
|||||||
ImportUsers = "ImportUsers"
|
ImportUsers = "ImportUsers"
|
||||||
VerifyGame = "VerifyGame"
|
VerifyGame = "VerifyGame"
|
||||||
ImportFriends = "ImportFriends"
|
ImportFriends = "ImportFriends"
|
||||||
|
ShutdownPlatformClient = "ShutdownPlatformClient"
|
||||||
|
LaunchPlatformClient = "LaunchPlatformClient"
|
||||||
|
ImportGameLibrarySettings = "ImportGameLibrarySettings"
|
||||||
|
|
||||||
|
|
||||||
class LicenseType(Enum):
|
class LicenseType(Enum):
|
||||||
@@ -49,11 +129,3 @@ class LocalGameState(Flag):
|
|||||||
None_ = 0
|
None_ = 0
|
||||||
Installed = 1
|
Installed = 1
|
||||||
Running = 2
|
Running = 2
|
||||||
|
|
||||||
|
|
||||||
class PresenceState(Enum):
|
|
||||||
""""Possible states that a user can be in."""
|
|
||||||
Unknown = "Unknown"
|
|
||||||
Online = "online"
|
|
||||||
Offline = "offline"
|
|
||||||
Away = "away"
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
UnknownError = UnknownError
|
assert UnknownError
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import logging
|
|||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from galaxy.reader import StreamLineReader
|
||||||
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
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
|
||||||
@@ -15,6 +18,17 @@ class JsonRpcError(Exception):
|
|||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.code == other.code and self.message == other.message and self.data == other.data
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
obj = {
|
||||||
|
"code": self.code,
|
||||||
|
"message": self.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.data is not None:
|
||||||
|
obj["error"]["data"] = self.data
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, "Parse error")
|
||||||
@@ -50,7 +64,8 @@ class UnknownError(ApplicationError):
|
|||||||
super().__init__(0, "Unknown error", data)
|
super().__init__(0, "Unknown error", data)
|
||||||
|
|
||||||
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"])
|
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
|
||||||
|
|
||||||
|
|
||||||
def anonymise_sensitive_params(params, sensitive_params):
|
def anonymise_sensitive_params(params, sensitive_params):
|
||||||
anomized_data = "****"
|
anomized_data = "****"
|
||||||
@@ -67,14 +82,14 @@ def anonymise_sensitive_params(params, sensitive_params):
|
|||||||
class Server():
|
class Server():
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
self._active = True
|
self._active = True
|
||||||
self._reader = reader
|
self._reader = StreamLineReader(reader)
|
||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._eof_listeners = []
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
|
|
||||||
def register_method(self, name, callback, internal, sensitive_params=False):
|
def register_method(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register method
|
Register method
|
||||||
|
|
||||||
@@ -84,9 +99,9 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
self._methods[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal, sensitive_params=False):
|
def register_notification(self, name, callback, immediate, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
Register notification
|
Register notification
|
||||||
|
|
||||||
@@ -96,10 +111,7 @@ class Server():
|
|||||||
:param sensitive_params: list of parameters that are anonymized before logging; \
|
:param sensitive_params: list of parameters that are anonymized before logging; \
|
||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
|
self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)
|
||||||
|
|
||||||
def register_eof(self, callback):
|
|
||||||
self._eof_listeners.append(callback)
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
while self._active:
|
while self._active:
|
||||||
@@ -114,15 +126,18 @@ class Server():
|
|||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received %d bytes of data", len(data))
|
logging.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
|
||||||
|
|
||||||
def stop(self):
|
def close(self):
|
||||||
|
logging.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
|
async def wait_closed(self):
|
||||||
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _eof(self):
|
def _eof(self):
|
||||||
logging.info("Received EOF")
|
logging.info("Received EOF")
|
||||||
self.stop()
|
self.close()
|
||||||
for listener in self._eof_listeners:
|
|
||||||
listener()
|
|
||||||
|
|
||||||
def _handle_input(self, data):
|
def _handle_input(self, data):
|
||||||
try:
|
try:
|
||||||
@@ -142,7 +157,7 @@ class Server():
|
|||||||
logging.error("Received unknown notification: %s", request.method)
|
logging.error("Received unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -150,12 +165,11 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
callback(*bound_args.args, **bound_args.kwargs)
|
callback(*bound_args.args, **bound_args.kwargs)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
|
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")
|
logging.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
@@ -166,7 +180,7 @@ class Server():
|
|||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, internal, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
self._log_request(request, sensitive_params)
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -174,8 +188,7 @@ class Server():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
self._send_error(request.id, InvalidParams())
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
if internal:
|
if immediate:
|
||||||
# internal requests are handled immediately
|
|
||||||
response = callback(*bound_args.args, **bound_args.kwargs)
|
response = callback(*bound_args.args, **bound_args.kwargs)
|
||||||
self._send_response(request.id, response)
|
self._send_response(request.id, response)
|
||||||
else:
|
else:
|
||||||
@@ -187,11 +200,13 @@ class Server():
|
|||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
self._send_error(request.id, error)
|
self._send_error(request.id, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
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")
|
logging.exception("Unexpected exception raised in plugin handler")
|
||||||
self._send_error(request.id, UnknownError(str(e)))
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
self._task_manager.create_task(handle(), request.method)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_request(data):
|
def _parse_request(data):
|
||||||
@@ -212,7 +227,7 @@ class Server():
|
|||||||
logging.debug("Sending data: %s", line)
|
logging.debug("Sending data: %s", line)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
self._task_manager.create_task(self._writer.drain(), "drain")
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logging.error(str(error))
|
||||||
|
|
||||||
@@ -228,15 +243,9 @@ class Server():
|
|||||||
response = {
|
response = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if error.data is not None:
|
|
||||||
response["error"]["data"] = error.data
|
|
||||||
|
|
||||||
self._send(response)
|
self._send(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -252,6 +261,7 @@ class NotificationClient():
|
|||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
self._methods = {}
|
||||||
|
self._task_manager = TaskManager("notification client")
|
||||||
|
|
||||||
def notify(self, method, params, sensitive_params=False):
|
def notify(self, method, params, sensitive_params=False):
|
||||||
"""
|
"""
|
||||||
@@ -270,13 +280,16 @@ class NotificationClient():
|
|||||||
self._log(method, params, sensitive_params)
|
self._log(method, params, sensitive_params)
|
||||||
self._send(notification)
|
self._send(notification)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
logging.debug("Sending %d byte of data", len(data))
|
logging.debug("Sending %d byte of data", len(data))
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
self._task_manager.create_task(self._writer.drain(), "drain")
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error("Failed to parse outgoing message: %s", str(error))
|
logging.error("Failed to parse outgoing message: %s", str(error))
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
import logging.handlers
|
||||||
import dataclasses
|
|
||||||
from enum import Enum
|
|
||||||
from collections import OrderedDict
|
|
||||||
import sys
|
import sys
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Optional, Set, Union
|
||||||
|
|
||||||
from typing import List, Dict
|
|
||||||
|
|
||||||
from galaxy.api.types import Achievement, Game, LocalGame, FriendInfo, GameTime, UserInfo, Room
|
|
||||||
|
|
||||||
from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError
|
|
||||||
from galaxy.api.consts import Feature
|
from galaxy.api.consts import Feature
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress
|
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||||
|
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
|
||||||
|
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep, GameLibrarySettings
|
||||||
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=method-hidden
|
def default(self, o): # pylint: disable=method-hidden
|
||||||
@@ -22,6 +19,7 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
# filter None values
|
# filter None values
|
||||||
def dict_factory(elements):
|
def dict_factory(elements):
|
||||||
return {k: v for k, v in elements if v is not None}
|
return {k: v for k, v in elements if v is not None}
|
||||||
|
|
||||||
return dataclasses.asdict(o, dict_factory=dict_factory)
|
return dataclasses.asdict(o, dict_factory=dict_factory)
|
||||||
if isinstance(o, Enum):
|
if isinstance(o, Enum):
|
||||||
return o.value
|
return o.value
|
||||||
@@ -30,12 +28,13 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""Use and override methods of this class to create a new platform integration."""
|
"""Use and override methods of this class to create a new platform integration."""
|
||||||
|
|
||||||
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)
|
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
self._version = version
|
self._version = version
|
||||||
|
|
||||||
self._feature_methods = OrderedDict()
|
self._features: Set[Feature] = set()
|
||||||
self._active = True
|
self._active = True
|
||||||
|
|
||||||
self._reader, self._writer = reader, writer
|
self._reader, self._writer = reader, writer
|
||||||
@@ -45,20 +44,26 @@ class Plugin:
|
|||||||
self._server = Server(self._reader, self._writer, encoder)
|
self._server = Server(self._reader, self._writer, encoder)
|
||||||
self._notification_client = NotificationClient(self._writer, encoder)
|
self._notification_client = NotificationClient(self._writer, encoder)
|
||||||
|
|
||||||
def eof_handler():
|
|
||||||
self._shutdown()
|
|
||||||
self._server.register_eof(eof_handler)
|
|
||||||
|
|
||||||
self._achievements_import_in_progress = False
|
self._achievements_import_in_progress = False
|
||||||
self._game_times_import_in_progress = False
|
self._game_times_import_in_progress = False
|
||||||
|
self._game_library_settings_import_in_progress = False
|
||||||
|
|
||||||
self._persistent_cache = dict()
|
self._persistent_cache = dict()
|
||||||
|
|
||||||
|
self._internal_task_manager = TaskManager("plugin internal")
|
||||||
|
self._external_task_manager = TaskManager("plugin external")
|
||||||
|
|
||||||
# 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)
|
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
||||||
self._register_method("initialize_cache", self._initialize_cache, internal=True)
|
self._register_method(
|
||||||
self._register_method("ping", self._ping, internal=True)
|
"initialize_cache",
|
||||||
|
self._initialize_cache,
|
||||||
|
internal=True,
|
||||||
|
immediate=True,
|
||||||
|
sensitive_params="data"
|
||||||
|
)
|
||||||
|
self._register_method("ping", self._ping, internal=True, immediate=True)
|
||||||
|
|
||||||
# implemented by developer
|
# implemented by developer
|
||||||
self._register_method(
|
self._register_method(
|
||||||
@@ -74,151 +79,139 @@ class Plugin:
|
|||||||
self._register_method(
|
self._register_method(
|
||||||
"import_owned_games",
|
"import_owned_games",
|
||||||
self.get_owned_games,
|
self.get_owned_games,
|
||||||
result_name="owned_games",
|
result_name="owned_games"
|
||||||
feature=Feature.ImportOwnedGames
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_unlocked_achievements",
|
|
||||||
self.get_unlocked_achievements,
|
|
||||||
result_name="unlocked_achievements",
|
|
||||||
feature=Feature.ImportAchievements
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"start_achievements_import",
|
|
||||||
self.start_achievements_import,
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_local_games",
|
|
||||||
self.get_local_games,
|
|
||||||
result_name="local_games",
|
|
||||||
feature=Feature.ImportInstalledGames
|
|
||||||
)
|
|
||||||
self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame)
|
|
||||||
self._register_notification("install_game", self.install_game, feature=Feature.InstallGame)
|
|
||||||
self._register_notification(
|
|
||||||
"uninstall_game",
|
|
||||||
self.uninstall_game,
|
|
||||||
feature=Feature.UninstallGame
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_friends",
|
|
||||||
self.get_friends,
|
|
||||||
result_name="friend_info_list",
|
|
||||||
feature=Feature.ImportFriends
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_user_infos",
|
|
||||||
self.get_users,
|
|
||||||
result_name="user_info_list",
|
|
||||||
feature=Feature.ImportUsers
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"send_message",
|
|
||||||
self.send_message,
|
|
||||||
feature=Feature.Chat
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"mark_as_read",
|
|
||||||
self.mark_as_read,
|
|
||||||
feature=Feature.Chat
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_rooms",
|
|
||||||
self.get_rooms,
|
|
||||||
result_name="rooms",
|
|
||||||
feature=Feature.Chat
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_room_history_from_message",
|
|
||||||
self.get_room_history_from_message,
|
|
||||||
result_name="messages",
|
|
||||||
feature=Feature.Chat
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_room_history_from_timestamp",
|
|
||||||
self.get_room_history_from_timestamp,
|
|
||||||
result_name="messages",
|
|
||||||
feature=Feature.Chat
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"import_game_times",
|
|
||||||
self.get_game_times,
|
|
||||||
result_name="game_times",
|
|
||||||
feature=Feature.ImportGameTime
|
|
||||||
)
|
|
||||||
self._register_method(
|
|
||||||
"start_game_times_import",
|
|
||||||
self.start_game_times_import,
|
|
||||||
)
|
)
|
||||||
|
self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])
|
||||||
|
|
||||||
|
self._register_method("start_achievements_import", self._start_achievements_import)
|
||||||
|
self._detect_feature(Feature.ImportAchievements, ["get_unlocked_achievements"])
|
||||||
|
|
||||||
|
self._register_method("import_local_games", self.get_local_games, result_name="local_games")
|
||||||
|
self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])
|
||||||
|
|
||||||
|
self._register_notification("launch_game", self.launch_game)
|
||||||
|
self._detect_feature(Feature.LaunchGame, ["launch_game"])
|
||||||
|
|
||||||
|
self._register_notification("install_game", self.install_game)
|
||||||
|
self._detect_feature(Feature.InstallGame, ["install_game"])
|
||||||
|
|
||||||
|
self._register_notification("uninstall_game", self.uninstall_game)
|
||||||
|
self._detect_feature(Feature.UninstallGame, ["uninstall_game"])
|
||||||
|
|
||||||
|
self._register_notification("shutdown_platform_client", self.shutdown_platform_client)
|
||||||
|
self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"])
|
||||||
|
|
||||||
|
self._register_notification("launch_platform_client", self.launch_platform_client)
|
||||||
|
self._detect_feature(Feature.LaunchPlatformClient, ["launch_platform_client"])
|
||||||
|
|
||||||
|
self._register_method("import_friends", self.get_friends, result_name="friend_info_list")
|
||||||
|
self._detect_feature(Feature.ImportFriends, ["get_friends"])
|
||||||
|
|
||||||
|
self._register_method("start_game_times_import", self._start_game_times_import)
|
||||||
|
self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
|
||||||
|
|
||||||
|
self._register_method("start_game_library_settings_import", self._start_game_library_settings_import)
|
||||||
|
self._detect_feature(Feature.ImportGameLibrarySettings, ["get_game_library_settings"])
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
self.close()
|
||||||
|
await self.wait_closed()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self) -> List[Feature]:
|
||||||
features = []
|
return list(self._features)
|
||||||
if self.__class__ != Plugin:
|
|
||||||
for feature, handlers in self._feature_methods.items():
|
|
||||||
if self._implements(handlers):
|
|
||||||
features.append(feature)
|
|
||||||
|
|
||||||
return features
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def persistent_cache(self) -> Dict:
|
def persistent_cache(self) -> Dict[str, str]:
|
||||||
"""The cache is only available after the :meth:`~.handshake_complete()` is called.
|
"""The cache is only available after the :meth:`~.handshake_complete()` is called.
|
||||||
"""
|
"""
|
||||||
return self._persistent_cache
|
return self._persistent_cache
|
||||||
|
|
||||||
def _implements(self, handlers):
|
def _implements(self, methods: List[str]) -> bool:
|
||||||
for handler in handlers:
|
for method in methods:
|
||||||
if handler.__name__ not in self.__class__.__dict__:
|
if method not in self.__class__.__dict__:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
|
def _detect_feature(self, feature: Feature, methods: List[str]):
|
||||||
if internal:
|
if self._implements(methods):
|
||||||
|
self._features.add(feature)
|
||||||
|
|
||||||
|
def _register_method(self, name, handler, result_name=None, internal=False, immediate=False, sensitive_params=False):
|
||||||
|
def wrap_result(result):
|
||||||
|
if result_name:
|
||||||
|
result = {
|
||||||
|
result_name: result
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
if immediate:
|
||||||
def method(*args, **kwargs):
|
def method(*args, **kwargs):
|
||||||
result = handler(*args, **kwargs)
|
result = handler(*args, **kwargs)
|
||||||
if result_name:
|
return wrap_result(result)
|
||||||
result = {
|
|
||||||
result_name: result
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
self._server.register_method(name, method, True, sensitive_params)
|
self._server.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(*args, **kwargs):
|
async def method(*args, **kwargs):
|
||||||
result = await handler(*args, **kwargs)
|
if not internal:
|
||||||
if result_name:
|
handler_ = self._wrap_external_method(handler, name)
|
||||||
result = {
|
else:
|
||||||
result_name: result
|
handler_ = handler
|
||||||
}
|
result = await handler_(*args, **kwargs)
|
||||||
return result
|
return wrap_result(result)
|
||||||
|
|
||||||
self._server.register_method(name, method, False, sensitive_params)
|
self._server.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
def _register_notification(self, name, handler, internal=False, immediate=False, sensitive_params=False):
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
if not internal and not immediate:
|
||||||
|
handler = self._wrap_external_method(handler, name)
|
||||||
|
self._server.register_notification(name, handler, immediate, sensitive_params)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
|
def _wrap_external_method(self, handler, name: str):
|
||||||
self._server.register_notification(name, handler, internal, sensitive_params)
|
async def wrapper(*args, **kwargs):
|
||||||
|
return await self._external_task_manager.create_task(handler(*args, **kwargs), name, False)
|
||||||
if feature is not None:
|
return wrapper
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
|
||||||
|
|
||||||
async def run(self):
|
async def run(self):
|
||||||
"""Plugin's main coroutine."""
|
"""Plugin's main coroutine."""
|
||||||
async def pass_control():
|
await self._server.run()
|
||||||
while self._active:
|
|
||||||
try:
|
|
||||||
self.tick()
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in plugin tick")
|
|
||||||
await asyncio.sleep(1)
|
|
||||||
|
|
||||||
await asyncio.gather(pass_control(), self._server.run())
|
def close(self) -> None:
|
||||||
|
if not self._active:
|
||||||
|
return
|
||||||
|
|
||||||
def _shutdown(self):
|
logging.info("Closing plugin")
|
||||||
logging.info("Shutting down")
|
self._server.close()
|
||||||
self._server.stop()
|
self._external_task_manager.cancel()
|
||||||
|
self._internal_task_manager.create_task(self.shutdown(), "shutdown")
|
||||||
self._active = False
|
self._active = False
|
||||||
self.shutdown()
|
|
||||||
|
async def wait_closed(self) -> None:
|
||||||
|
await self._external_task_manager.wait()
|
||||||
|
await self._internal_task_manager.wait()
|
||||||
|
await self._server.wait_closed()
|
||||||
|
await self._notification_client.close()
|
||||||
|
|
||||||
|
def create_task(self, coro, description):
|
||||||
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
|
return self._external_task_manager.create_task(coro, description)
|
||||||
|
|
||||||
|
async def _pass_control(self):
|
||||||
|
while self._active:
|
||||||
|
try:
|
||||||
|
self.tick()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in plugin tick")
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
async def _shutdown(self):
|
||||||
|
logging.info("Shutting down")
|
||||||
|
self.close()
|
||||||
|
await self._external_task_manager.wait()
|
||||||
|
await self._internal_task_manager.wait()
|
||||||
|
|
||||||
def _get_capabilities(self):
|
def _get_capabilities(self):
|
||||||
return {
|
return {
|
||||||
@@ -229,14 +222,18 @@ class Plugin:
|
|||||||
|
|
||||||
def _initialize_cache(self, data: Dict):
|
def _initialize_cache(self, data: Dict):
|
||||||
self._persistent_cache = data
|
self._persistent_cache = data
|
||||||
self.handshake_complete()
|
try:
|
||||||
|
self.handshake_complete()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unhandled exception during `handshake_complete` step")
|
||||||
|
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _ping():
|
def _ping():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# notifications
|
# notifications
|
||||||
def store_credentials(self, credentials: dict):
|
def store_credentials(self, credentials: Dict[str, Any]) -> None:
|
||||||
"""Notify the client to store authentication credentials.
|
"""Notify the client to store authentication credentials.
|
||||||
Credentials are passed on the next authenticate call.
|
Credentials are passed on the next authenticate call.
|
||||||
|
|
||||||
@@ -257,10 +254,13 @@ class Plugin:
|
|||||||
self.store_credentials(user_data['credentials'])
|
self.store_credentials(user_data['credentials'])
|
||||||
return Authentication(user_data['userId'], user_data['username'])
|
return Authentication(user_data['userId'], user_data['username'])
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# temporary solution for persistent_cache vs credentials issue
|
||||||
|
self.persistent_cache['credentials'] = credentials # type: ignore
|
||||||
|
|
||||||
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
|
||||||
|
|
||||||
def add_game(self, game: Game):
|
def add_game(self, game: Game) -> None:
|
||||||
"""Notify the client to add game to the list of owned games
|
"""Notify the client to add game to the list of owned games
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
|
|
||||||
@@ -282,11 +282,11 @@ class Plugin:
|
|||||||
params = {"owned_game": game}
|
params = {"owned_game": game}
|
||||||
self._notification_client.notify("owned_game_added", params)
|
self._notification_client.notify("owned_game_added", params)
|
||||||
|
|
||||||
def remove_game(self, game_id: str):
|
def remove_game(self, game_id: str) -> None:
|
||||||
"""Notify the client to remove game from the list of owned games
|
"""Notify the client to remove game from the list of owned games
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
|
|
||||||
:param game_id: game id of the game to remove from the list of owned games
|
:param game_id: the id of the game to remove from the list of owned games
|
||||||
|
|
||||||
Example use case of remove_game:
|
Example use case of remove_game:
|
||||||
|
|
||||||
@@ -304,7 +304,7 @@ class Plugin:
|
|||||||
params = {"game_id": game_id}
|
params = {"game_id": game_id}
|
||||||
self._notification_client.notify("owned_game_removed", params)
|
self._notification_client.notify("owned_game_removed", params)
|
||||||
|
|
||||||
def update_game(self, game: Game):
|
def update_game(self, game: Game) -> None:
|
||||||
"""Notify the client to update the status of a game
|
"""Notify the client to update the status of a game
|
||||||
owned by the currently authenticated user.
|
owned by the currently authenticated user.
|
||||||
|
|
||||||
@@ -313,10 +313,10 @@ class Plugin:
|
|||||||
params = {"owned_game": game}
|
params = {"owned_game": game}
|
||||||
self._notification_client.notify("owned_game_updated", params)
|
self._notification_client.notify("owned_game_updated", params)
|
||||||
|
|
||||||
def unlock_achievement(self, game_id: str, achievement: Achievement):
|
def unlock_achievement(self, game_id: str, achievement: Achievement) -> None:
|
||||||
"""Notify the client to unlock an achievement for a specific game.
|
"""Notify the client to unlock an achievement for a specific game.
|
||||||
|
|
||||||
:param game_id: game_id of the game for which to unlock an achievement.
|
:param game_id: the id of the game for which to unlock an achievement.
|
||||||
:param achievement: achievement to unlock.
|
:param achievement: achievement to unlock.
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
@@ -325,41 +325,24 @@ class Plugin:
|
|||||||
}
|
}
|
||||||
self._notification_client.notify("achievement_unlocked", params)
|
self._notification_client.notify("achievement_unlocked", params)
|
||||||
|
|
||||||
def game_achievements_import_success(self, game_id: str, achievements):
|
def _game_achievements_import_success(self, game_id: str, achievements: List[Achievement]) -> None:
|
||||||
"""Notify the client that import of achievements for a given game has succeeded.
|
|
||||||
This method is called by import_games_achievements.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the achievements were imported
|
|
||||||
:param achievements: list of imported achievements
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"unlocked_achievements": achievements
|
"unlocked_achievements": achievements
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_success", params)
|
self._notification_client.notify("game_achievements_import_success", params)
|
||||||
|
|
||||||
def game_achievements_import_failure(self, game_id: str, error: ApplicationError):
|
def _game_achievements_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
"""Notify the client that import of achievements for a given game has failed.
|
|
||||||
This method is called by import_games_achievements.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the achievements import failed
|
|
||||||
:param error: error which prevented the achievements import
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_achievements_import_failure", params)
|
self._notification_client.notify("game_achievements_import_failure", params)
|
||||||
|
|
||||||
def achievements_import_finished(self):
|
def _achievements_import_finished(self) -> None:
|
||||||
"""Notify the client that importing achievements has finished.
|
|
||||||
This method is called by import_games_achievements_task"""
|
|
||||||
self._notification_client.notify("achievements_import_finished", None)
|
self._notification_client.notify("achievements_import_finished", None)
|
||||||
|
|
||||||
def update_local_game_status(self, local_game: LocalGame):
|
def update_local_game_status(self, local_game: LocalGame) -> None:
|
||||||
"""Notify the client to update the status of a local game.
|
"""Notify the client to update the status of a local game.
|
||||||
|
|
||||||
:param local_game: the LocalGame to update
|
:param local_game: the LocalGame to update
|
||||||
@@ -376,7 +359,7 @@ class Plugin:
|
|||||||
continue
|
continue
|
||||||
self.update_local_game_status(LocalGame(game.id, game.status))
|
self.update_local_game_status(LocalGame(game.id, game.status))
|
||||||
self._cached_games_statuses[game.id] = game.status
|
self._cached_games_statuses[game.id] = game.status
|
||||||
asyncio.sleep(5) # interval
|
await asyncio.sleep(5) # interval
|
||||||
|
|
||||||
def tick(self):
|
def tick(self):
|
||||||
if self._check_statuses_task is None or self._check_statuses_task.done():
|
if self._check_statuses_task is None or self._check_statuses_task.done():
|
||||||
@@ -385,7 +368,7 @@ class Plugin:
|
|||||||
params = {"local_game": local_game}
|
params = {"local_game": local_game}
|
||||||
self._notification_client.notify("local_game_status_changed", params)
|
self._notification_client.notify("local_game_status_changed", params)
|
||||||
|
|
||||||
def add_friend(self, user: FriendInfo):
|
def add_friend(self, user: FriendInfo) -> None:
|
||||||
"""Notify the client to add a user to friends list of the currently authenticated user.
|
"""Notify the client to add a user to friends list of the currently authenticated user.
|
||||||
|
|
||||||
:param user: FriendInfo of a user that the client will add to friends list
|
:param user: FriendInfo of a user that the client will add to friends list
|
||||||
@@ -393,7 +376,7 @@ class Plugin:
|
|||||||
params = {"friend_info": user}
|
params = {"friend_info": user}
|
||||||
self._notification_client.notify("friend_added", params)
|
self._notification_client.notify("friend_added", params)
|
||||||
|
|
||||||
def remove_friend(self, user_id: str):
|
def remove_friend(self, user_id: str) -> None:
|
||||||
"""Notify the client to remove a user from friends list of the currently authenticated user.
|
"""Notify the client to remove a user from friends list of the currently authenticated user.
|
||||||
|
|
||||||
:param user_id: id of the user to remove from friends list
|
:param user_id: id of the user to remove from friends list
|
||||||
@@ -401,22 +384,7 @@ class Plugin:
|
|||||||
params = {"user_id": user_id}
|
params = {"user_id": user_id}
|
||||||
self._notification_client.notify("friend_removed", params)
|
self._notification_client.notify("friend_removed", params)
|
||||||
|
|
||||||
def update_room(self, room_id: str, unread_message_count=None, new_messages=None):
|
def update_game_time(self, game_time: GameTime) -> None:
|
||||||
"""WIP, Notify the client to update the information regarding
|
|
||||||
a chat room that the currently authenticated user is in.
|
|
||||||
|
|
||||||
:param room_id: id of the room to update
|
|
||||||
:param unread_message_count: information about the new unread message count in the room
|
|
||||||
:param new_messages: list of new messages that the user received
|
|
||||||
"""
|
|
||||||
params = {"room_id": room_id}
|
|
||||||
if unread_message_count is not None:
|
|
||||||
params["unread_message_count"] = unread_message_count
|
|
||||||
if new_messages is not None:
|
|
||||||
params["messages"] = new_messages
|
|
||||||
self._notification_client.notify("chat_room_updated", params)
|
|
||||||
|
|
||||||
def update_game_time(self, game_time: GameTime):
|
|
||||||
"""Notify the client to update game time for a game.
|
"""Notify the client to update game time for a game.
|
||||||
|
|
||||||
:param game_time: game time to update
|
:param game_time: game time to update
|
||||||
@@ -424,60 +392,58 @@ class Plugin:
|
|||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_updated", params)
|
self._notification_client.notify("game_time_updated", params)
|
||||||
|
|
||||||
def game_time_import_success(self, game_time: GameTime):
|
def _game_time_import_success(self, game_time: GameTime) -> None:
|
||||||
"""Notify the client that import of a given game_time has succeeded.
|
|
||||||
This method is called by import_game_times.
|
|
||||||
|
|
||||||
:param game_time: game_time which was imported
|
|
||||||
"""
|
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._notification_client.notify("game_time_import_success", params)
|
self._notification_client.notify("game_time_import_success", params)
|
||||||
|
|
||||||
def game_time_import_failure(self, game_id: str, error: ApplicationError):
|
def _game_time_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
"""Notify the client that import of a game time for a given game has failed.
|
|
||||||
This method is called by import_game_times.
|
|
||||||
|
|
||||||
:param game_id: id of the game for which the game time could not be imported
|
|
||||||
:param error: error which prevented the game time import
|
|
||||||
"""
|
|
||||||
params = {
|
params = {
|
||||||
"game_id": game_id,
|
"game_id": game_id,
|
||||||
"error": {
|
"error": error.json()
|
||||||
"code": error.code,
|
|
||||||
"message": error.message
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self._notification_client.notify("game_time_import_failure", params)
|
self._notification_client.notify("game_time_import_failure", params)
|
||||||
|
|
||||||
def game_times_import_finished(self):
|
def _game_times_import_finished(self) -> None:
|
||||||
"""Notify the client that importing game times has finished.
|
|
||||||
This method is called by :meth:`~.import_game_times_task`.
|
|
||||||
"""
|
|
||||||
self._notification_client.notify("game_times_import_finished", None)
|
self._notification_client.notify("game_times_import_finished", None)
|
||||||
|
|
||||||
def lost_authentication(self):
|
def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None:
|
||||||
|
params = {"game_library_settings": game_library_settings}
|
||||||
|
self._notification_client.notify("game_library_settings_import_success", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_failure(self, game_id: str, error: ApplicationError) -> None:
|
||||||
|
params = {
|
||||||
|
"game_id": game_id,
|
||||||
|
"error": error.json()
|
||||||
|
}
|
||||||
|
self._notification_client.notify("game_library_settings_import_failure", params)
|
||||||
|
|
||||||
|
def _game_library_settings_import_finished(self) -> None:
|
||||||
|
self._notification_client.notify("game_library_settings_import_finished", None)
|
||||||
|
|
||||||
|
def lost_authentication(self) -> None:
|
||||||
"""Notify the client that integration has lost authentication for the
|
"""Notify the client that integration has lost authentication for the
|
||||||
current user and is unable to perform actions which would require it.
|
current user and is unable to perform actions which would require it.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify("authentication_lost", None)
|
self._notification_client.notify("authentication_lost", None)
|
||||||
|
|
||||||
def push_cache(self):
|
def push_cache(self) -> None:
|
||||||
"""Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
|
"""Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
|
||||||
"""
|
"""
|
||||||
self._notification_client.notify(
|
self._notification_client.notify(
|
||||||
"push_cache",
|
"push_cache",
|
||||||
params={"data": self._persistent_cache}
|
params={"data": self._persistent_cache},
|
||||||
|
sensitive_params="data"
|
||||||
)
|
)
|
||||||
|
|
||||||
# handlers
|
# handlers
|
||||||
def handshake_complete(self):
|
def handshake_complete(self) -> None:
|
||||||
"""This method is called right after the handshake with the GOG Galaxy Client is complete and
|
"""This method is called right after the handshake with the GOG Galaxy Client is complete and
|
||||||
before any other operations are called by the GOG Galaxy Client.
|
before any other operations are called by the GOG Galaxy Client.
|
||||||
Persistent cache is available when this method is called.
|
Persistent cache is available when this method is called.
|
||||||
Override it if you need to do additional plugin initializations.
|
Override it if you need to do additional plugin initializations.
|
||||||
This method is called internally."""
|
This method is called internally."""
|
||||||
|
|
||||||
def tick(self):
|
def tick(self) -> None:
|
||||||
"""This method is called periodically.
|
"""This method is called periodically.
|
||||||
Override it to implement periodical non-blocking tasks.
|
Override it to implement periodical non-blocking tasks.
|
||||||
This method is called internally.
|
This method is called internally.
|
||||||
@@ -497,13 +463,13 @@ class Plugin:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def shutdown(self):
|
async def shutdown(self) -> None:
|
||||||
"""This method is called on integration shutdown.
|
"""This method is called on integration shutdown.
|
||||||
Override it to implement tear down.
|
Override it to implement tear down.
|
||||||
This method is called by the GOG Galaxy Client."""
|
This method is called by the GOG Galaxy Client."""
|
||||||
|
|
||||||
# methods
|
# methods
|
||||||
async def authenticate(self, stored_credentials: dict = None):
|
async def authenticate(self, stored_credentials: Optional[Dict] = None) -> Union[NextStep, Authentication]:
|
||||||
"""Override this method to handle user authentication.
|
"""Override this method to handle user authentication.
|
||||||
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
|
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
|
||||||
or :class:`~galaxy.api.types.NextStep` if it requires going to another url.
|
or :class:`~galaxy.api.types.NextStep` if it requires going to another url.
|
||||||
@@ -531,7 +497,8 @@ 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]:
|
||||||
"""This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
|
"""This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
|
||||||
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
||||||
This method should either return galaxy.api.types.Authentication if the authentication is finished
|
This method should either return galaxy.api.types.Authentication if the authentication is finished
|
||||||
@@ -579,50 +546,63 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_unlocked_achievements(self, game_id: str) -> List[Achievement]:
|
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
||||||
"""
|
|
||||||
.. deprecated:: 0.33
|
|
||||||
Use :meth:`~.import_games_achievements`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def start_achievements_import(self, game_ids: List[str]):
|
|
||||||
"""Starts the task of importing achievements.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the achievements are imported
|
|
||||||
"""
|
|
||||||
if self._achievements_import_in_progress:
|
if self._achievements_import_in_progress:
|
||||||
raise ImportInProgress()
|
raise ImportInProgress()
|
||||||
|
|
||||||
async def import_games_achievements_task(game_ids):
|
context = await self.prepare_achievements_context(game_ids)
|
||||||
try:
|
|
||||||
await self.import_games_achievements(game_ids)
|
|
||||||
finally:
|
|
||||||
self.achievements_import_finished()
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
|
|
||||||
asyncio.create_task(import_games_achievements_task(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
|
self._achievements_import_in_progress = True
|
||||||
|
|
||||||
async def import_games_achievements(self, game_ids: List[str]):
|
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 return the unlocked achievements
|
This allows for optimizations like batch requests to platform API.
|
||||||
of the user that is currently logged in to the plugin.
|
Default implementation returns None.
|
||||||
Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which to import unlocked achievements
|
:param game_ids: the ids of the games for which achievements are imported
|
||||||
|
:return: context
|
||||||
"""
|
"""
|
||||||
async def import_game_achievements(game_id):
|
return None
|
||||||
try:
|
|
||||||
achievements = await self.get_unlocked_achievements(game_id)
|
|
||||||
self.game_achievements_import_success(game_id, achievements)
|
|
||||||
except Exception as error:
|
|
||||||
self.game_achievements_import_failure(game_id, error)
|
|
||||||
|
|
||||||
imports = [import_game_achievements(game_id) for game_id in game_ids]
|
async def get_unlocked_achievements(self, game_id: str, context: Any) -> List[Achievement]:
|
||||||
await asyncio.gather(*imports)
|
"""Override this method to return list of unlocked achievements
|
||||||
|
for the game identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the achievements are returned
|
||||||
|
:param context: the value returned from :meth:`prepare_achievements_context`
|
||||||
|
:return: list of Achievement objects
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def achievements_import_complete(self):
|
||||||
|
"""Override this method to handle operations after achievements import is finished
|
||||||
|
(like updating cache).
|
||||||
|
"""
|
||||||
|
|
||||||
async def get_local_games(self) -> List[LocalGame]:
|
async def get_local_games(self) -> List[LocalGame]:
|
||||||
"""Override this method to return the list of
|
"""Override this method to return the list of
|
||||||
@@ -646,12 +626,12 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def launch_game(self, game_id: str):
|
async def launch_game(self, game_id: str) -> None:
|
||||||
"""Override this method to launch the game
|
"""Override this method to launch the game
|
||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to launch
|
:param str game_id: the id of the game to launch
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -664,12 +644,12 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def install_game(self, game_id: str):
|
async def install_game(self, game_id: str) -> None:
|
||||||
"""Override this method to install the game
|
"""Override this method to install the game
|
||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to install
|
:param str game_id: the id of the game to install
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -682,12 +662,12 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def uninstall_game(self, game_id: str):
|
async def uninstall_game(self, game_id: str) -> None:
|
||||||
"""Override this method to uninstall the game
|
"""Override this method to uninstall the game
|
||||||
identified by the provided game_id.
|
identified by the provided game_id.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param str game_id: id of the game to uninstall
|
:param str game_id: the id of the game to uninstall
|
||||||
|
|
||||||
Example of possible override of the method:
|
Example of possible override of the method:
|
||||||
|
|
||||||
@@ -700,6 +680,16 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def shutdown_platform_client(self) -> None:
|
||||||
|
"""Override this method to gracefully terminate platform client.
|
||||||
|
This method is called by the GOG Galaxy Client."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def launch_platform_client(self) -> None:
|
||||||
|
"""Override this method to launch platform client. Preferably minimized to tray.
|
||||||
|
This method is called by the GOG Galaxy Client."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_friends(self) -> List[FriendInfo]:
|
async def get_friends(self) -> List[FriendInfo]:
|
||||||
"""Override this method to return the friends list
|
"""Override this method to return the friends list
|
||||||
of the currently authenticated user.
|
of the currently authenticated user.
|
||||||
@@ -720,106 +710,121 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_users(self, user_id_list: List[str]) -> List[UserInfo]:
|
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
||||||
"""WIP, Override this method to return the list of users matching the provided ids.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param user_id_list: list of user ids
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def send_message(self, room_id: str, message_text: str):
|
|
||||||
"""WIP, Override this method to send message to a chat room.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param room_id: id of the room to which the message should be sent
|
|
||||||
:param message_text: text which should be sent in the message
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def mark_as_read(self, room_id: str, last_message_id: str):
|
|
||||||
"""WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param room_id: id of the room
|
|
||||||
:param last_message_id: id of the last message; room is marked as read only if this id matches
|
|
||||||
the last message id known to the client
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def get_rooms(self) -> List[Room]:
|
|
||||||
"""WIP, Override this method to return the chat rooms in which the user is currently in.
|
|
||||||
This method is called by the GOG Galaxy Client
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def get_room_history_from_message(self, room_id: str, message_id: str):
|
|
||||||
"""WIP, Override this method to return the chat room history since the message provided in parameter.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param room_id: id of the room
|
|
||||||
:param message_id: id of the message since which the history should be retrieved
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int):
|
|
||||||
"""WIP, Override this method to return the chat room history since the timestamp provided in parameter.
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param room_id: id of the room
|
|
||||||
:param from_timestamp: timestamp since which the history should be retrieved
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def get_game_times(self) -> List[GameTime]:
|
|
||||||
"""
|
|
||||||
.. deprecated:: 0.33
|
|
||||||
Use :meth:`~.import_game_times`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
async def start_game_times_import(self, game_ids: List[str]):
|
|
||||||
"""Starts the task of importing game times
|
|
||||||
This method is called by the GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the game time is imported
|
|
||||||
"""
|
|
||||||
if self._game_times_import_in_progress:
|
if self._game_times_import_in_progress:
|
||||||
raise ImportInProgress()
|
raise ImportInProgress()
|
||||||
|
|
||||||
async def import_game_times_task(game_ids):
|
context = await self.prepare_game_times_context(game_ids)
|
||||||
try:
|
|
||||||
await self.import_game_times(game_ids)
|
|
||||||
finally:
|
|
||||||
self.game_times_import_finished()
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
|
|
||||||
asyncio.create_task(import_game_times_task(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
|
self._game_times_import_in_progress = True
|
||||||
|
|
||||||
async def import_game_times(self, game_ids: List[str]):
|
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 return game times for
|
This allows for optimizations like batch requests to platform API.
|
||||||
games owned by the currently authenticated user.
|
Default implementation returns None.
|
||||||
Call game_time_import_success/game_time_import_failure for each game_id on the list.
|
|
||||||
This method is called by GOG Galaxy Client.
|
|
||||||
|
|
||||||
:param game_ids: ids of the games for which the game time is imported
|
:param game_ids: the ids of the games for which game time are imported
|
||||||
|
:return: context
|
||||||
"""
|
"""
|
||||||
try:
|
return None
|
||||||
game_times = await self.get_game_times()
|
|
||||||
game_ids_set = set(game_ids)
|
|
||||||
for game_time in game_times:
|
|
||||||
if game_time.game_id not in game_ids_set:
|
|
||||||
continue
|
|
||||||
self.game_time_import_success(game_time)
|
|
||||||
game_ids_set.discard(game_time.game_id)
|
|
||||||
for game_id in game_ids_set:
|
|
||||||
self.game_time_import_failure(game_id, UnknownError())
|
|
||||||
except Exception as error:
|
|
||||||
for game_id in game_ids:
|
|
||||||
self.game_time_import_failure(game_id, error)
|
|
||||||
|
|
||||||
|
async def get_game_time(self, game_id: str, context: Any) -> GameTime:
|
||||||
|
"""Override this method to return the game time for the game
|
||||||
|
identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game time is returned
|
||||||
|
:param context: the value returned from :meth:`prepare_game_times_context`
|
||||||
|
:return: GameTime object
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def game_times_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after game times import is finished
|
||||||
|
(like updating cache).
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
||||||
|
if self._game_library_settings_import_in_progress:
|
||||||
|
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:
|
||||||
|
"""Override this method to prepare context for get_game_library_settings.
|
||||||
|
This allows for optimizations like batch requests to platform API.
|
||||||
|
Default implementation returns None.
|
||||||
|
|
||||||
|
:param game_ids: the ids of the games for which game time are imported
|
||||||
|
:return: context
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_game_library_settings(self, game_id: str, context: Any) -> GameLibrarySettings:
|
||||||
|
"""Override this method to return the game library settings for the game
|
||||||
|
identified by the provided game_id.
|
||||||
|
This method is called by import task initialized by GOG Galaxy Client.
|
||||||
|
|
||||||
|
:param game_id: the id of the game for which the game time is returned
|
||||||
|
:param context: the value returned from :meth:`prepare_game_library_settings_context`
|
||||||
|
:return: GameLibrarySettings object
|
||||||
|
"""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def game_library_settings_import_complete(self) -> None:
|
||||||
|
"""Override this method to handle operations after game times 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.
|
||||||
@@ -862,10 +867,13 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
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")
|
extra_info = writer.get_extra_info("sockname")
|
||||||
logging.info("Using local address: %s:%u", *extra_info)
|
logging.info("Using local address: %s:%u", *extra_info)
|
||||||
plugin = plugin_class(reader, writer, token)
|
async with plugin_class(reader, writer, token) as plugin:
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||||
|
|
||||||
asyncio.run(coroutine())
|
asyncio.run(coroutine())
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Error while running plugin")
|
logging.exception("Error while running plugin")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
from galaxy.api.consts import LicenseType, LocalGameState
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authentication():
|
class Authentication():
|
||||||
@@ -61,7 +61,6 @@ class NextStep():
|
|||||||
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
||||||
:param cookies: browser initial set of cookies
|
:param cookies: browser initial set of cookies
|
||||||
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed on every document at given step of internal browser authentication.
|
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed on every document at given step of internal browser authentication.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
next_step: str
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
@@ -130,34 +129,6 @@ class LocalGame():
|
|||||||
game_id: str
|
game_id: str
|
||||||
local_game_state: LocalGameState
|
local_game_state: LocalGameState
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Presence():
|
|
||||||
"""Information about a presence of a user.
|
|
||||||
|
|
||||||
:param presence_state: the state in which the user's presence is
|
|
||||||
:param game_id: id of the game which the user is currently playing
|
|
||||||
:param presence_status: optional attached string with the detailed description of the user's presence
|
|
||||||
"""
|
|
||||||
presence_state: PresenceState
|
|
||||||
game_id: Optional[str] = None
|
|
||||||
presence_status: Optional[str] = None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class UserInfo():
|
|
||||||
"""Detailed information about a user.
|
|
||||||
|
|
||||||
:param user_id: of the user
|
|
||||||
:param is_friend: whether the user is a friend of the currently authenticated user
|
|
||||||
:param user_name: of the user
|
|
||||||
:param avatar_url: to the avatar of the user
|
|
||||||
:param presence: about the users presence
|
|
||||||
"""
|
|
||||||
user_id: str
|
|
||||||
is_friend: bool
|
|
||||||
user_name: str
|
|
||||||
avatar_url: str
|
|
||||||
presence: Presence
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class FriendInfo():
|
class FriendInfo():
|
||||||
"""Information about a friend of the currently authenticated user.
|
"""Information about a friend of the currently authenticated user.
|
||||||
@@ -168,32 +139,6 @@ class FriendInfo():
|
|||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Room():
|
|
||||||
"""WIP, Chatroom.
|
|
||||||
|
|
||||||
:param room_id: id of the room
|
|
||||||
:param unread_message_count: number of unread messages in the room
|
|
||||||
:param last_message_id: id of the last message in the room
|
|
||||||
"""
|
|
||||||
room_id: str
|
|
||||||
unread_message_count: int
|
|
||||||
last_message_id: str
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Message():
|
|
||||||
"""WIP, A chatroom message.
|
|
||||||
|
|
||||||
:param message_id: id of the message
|
|
||||||
:param sender_id: id of the sender of the message
|
|
||||||
:param sent_time: time at which the message was sent
|
|
||||||
:param message_text: text attached to the message
|
|
||||||
"""
|
|
||||||
message_id: str
|
|
||||||
sender_id: str
|
|
||||||
sent_time: int
|
|
||||||
message_text: str
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GameTime():
|
class GameTime():
|
||||||
"""Game time of a game, defines the total time spent in the game
|
"""Game time of a game, defines the total time spent in the game
|
||||||
@@ -204,5 +149,17 @@ class GameTime():
|
|||||||
:param last_time_played: last time the game was played (**unix timestamp**)
|
:param last_time_played: last time the game was played (**unix timestamp**)
|
||||||
"""
|
"""
|
||||||
game_id: str
|
game_id: str
|
||||||
time_played: int
|
time_played: Optional[int]
|
||||||
last_played_time: int
|
last_played_time: Optional[int]
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GameLibrarySettings():
|
||||||
|
"""Library settings of a game, defines assigned tags and visibility flag.
|
||||||
|
|
||||||
|
:param game_id: id of the related game
|
||||||
|
:param tags: collection of tags assigned to the game
|
||||||
|
:param hidden: indicates if the game should be hidden in GOG Galaxy application
|
||||||
|
"""
|
||||||
|
game_id: str
|
||||||
|
tags: Optional[List[str]]
|
||||||
|
hidden: Optional[bool]
|
||||||
|
|||||||
@@ -1,9 +1,42 @@
|
|||||||
|
"""
|
||||||
|
This module standarize 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.
|
||||||
|
Examplary simple web service could looks like:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
|
class BackendClient:
|
||||||
|
AUTH_URL = 'my-integration.com/auth'
|
||||||
|
HEADERS = {
|
||||||
|
"My-Custom-Header": "true",
|
||||||
|
}
|
||||||
|
def __init__(self):
|
||||||
|
self._session = create_client_session(headers=self.HEADERS)
|
||||||
|
|
||||||
|
async def authenticate(self):
|
||||||
|
await self._session.request('POST', self.AUTH_URL)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
# to be called on plugin shutdown
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def _authorized_request(self, method, url, *args, **kwargs):
|
||||||
|
with handle_exceptions():
|
||||||
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import ssl
|
import ssl
|
||||||
|
from contextlib import contextmanager
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import certifi
|
import certifi
|
||||||
|
import logging
|
||||||
|
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||||
@@ -11,40 +44,101 @@ from galaxy.api.errors import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Default limit of the simultaneous connections for ssl connector.
|
||||||
|
DEFAULT_LIMIT = 20
|
||||||
|
#: Default timeout in seconds used for client session.
|
||||||
|
DEFAULT_TIMEOUT = 60
|
||||||
|
|
||||||
|
|
||||||
class HttpClient:
|
class HttpClient:
|
||||||
def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None):
|
"""
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
.. deprecated:: 0.41
|
||||||
ssl_context.load_verify_locations(certifi.where())
|
Use http module functions instead
|
||||||
connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context)
|
"""
|
||||||
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
def __init__(self, limit=DEFAULT_LIMIT, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT), cookie_jar=None):
|
||||||
|
connector = create_tcp_connector(limit=limit)
|
||||||
|
self._session = create_client_session(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
|
"""Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
|
||||||
await self._session.close()
|
await self._session.close()
|
||||||
|
|
||||||
async def request(self, method, *args, **kwargs):
|
async def request(self, method, url, *args, **kwargs):
|
||||||
try:
|
with handle_exception():
|
||||||
response = await self._session.request(method, *args, **kwargs)
|
return await self._session.request(method, url, *args, **kwargs)
|
||||||
except asyncio.TimeoutError:
|
|
||||||
raise BackendTimeout()
|
|
||||||
except aiohttp.ServerDisconnectedError:
|
|
||||||
raise BackendNotAvailable()
|
|
||||||
except aiohttp.ClientConnectionError:
|
|
||||||
raise NetworkError()
|
|
||||||
except aiohttp.ContentTypeError:
|
|
||||||
raise UnknownBackendResponse()
|
|
||||||
except aiohttp.ClientError:
|
|
||||||
raise UnknownError()
|
|
||||||
if response.status == HTTPStatus.UNAUTHORIZED:
|
|
||||||
raise AuthenticationRequired()
|
|
||||||
if response.status == HTTPStatus.FORBIDDEN:
|
|
||||||
raise AccessDenied()
|
|
||||||
if response.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
|
||||||
raise BackendNotAvailable()
|
|
||||||
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
|
|
||||||
raise TooManyRequests()
|
|
||||||
if response.status >= 500:
|
|
||||||
raise BackendError()
|
|
||||||
if response.status >= 400:
|
|
||||||
raise UnknownError()
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
|
||||||
|
"""
|
||||||
|
Creates TCP connector with resonable defaults.
|
||||||
|
For details about available parameters refer to
|
||||||
|
`aiohttp.TCPConnector <https://docs.aiohttp.org/en/stable/client_reference.html#tcpconnector>`_
|
||||||
|
"""
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
ssl_context.load_verify_locations(certifi.where())
|
||||||
|
kwargs.setdefault("ssl", ssl_context)
|
||||||
|
kwargs.setdefault("limit", DEFAULT_LIMIT)
|
||||||
|
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore due to https://github.com/python/mypy/issues/4001
|
||||||
|
|
||||||
|
|
||||||
|
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
|
||||||
|
"""
|
||||||
|
Creates client session with resonable defaults.
|
||||||
|
For details about available parameters refer to
|
||||||
|
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
|
||||||
|
|
||||||
|
Examplary customization:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from galaxy.http import create_client_session, create_tcp_connector
|
||||||
|
|
||||||
|
session = create_client_session(
|
||||||
|
headers={
|
||||||
|
"Keep-Alive": "true"
|
||||||
|
},
|
||||||
|
connector=create_tcp_connector(limit=40),
|
||||||
|
timeout=100)
|
||||||
|
"""
|
||||||
|
kwargs.setdefault("connector", create_tcp_connector())
|
||||||
|
kwargs.setdefault("timeout", aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT))
|
||||||
|
kwargs.setdefault("raise_for_status", True)
|
||||||
|
return aiohttp.ClientSession(*args, **kwargs) # type: ignore due to https://github.com/python/mypy/issues/4001
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def handle_exception():
|
||||||
|
"""
|
||||||
|
Context manager translating network related exceptions
|
||||||
|
to custom :mod:`~galaxy.api.errors`.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise BackendTimeout()
|
||||||
|
except aiohttp.ServerDisconnectedError:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
except aiohttp.ClientConnectionError:
|
||||||
|
raise NetworkError()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
raise UnknownBackendResponse()
|
||||||
|
except aiohttp.ClientResponseError as error:
|
||||||
|
if error.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
raise AuthenticationRequired()
|
||||||
|
if error.status == HTTPStatus.FORBIDDEN:
|
||||||
|
raise AccessDenied()
|
||||||
|
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||||
|
raise TooManyRequests()
|
||||||
|
if error.status >= 500:
|
||||||
|
raise BackendError()
|
||||||
|
if error.status >= 400:
|
||||||
|
logging.warning(
|
||||||
|
"Got status %d while performing %s request for %s",
|
||||||
|
error.status, error.request_info.method, str(error.request_info.url)
|
||||||
|
)
|
||||||
|
raise UnknownError()
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
logging.exception("Caught exception while performing request")
|
||||||
|
raise UnknownError()
|
||||||
|
|||||||
88
src/galaxy/proc_tools.py
Normal file
88
src/galaxy/proc_tools.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, NewType, Optional, List, cast
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ProcessId = NewType("ProcessId", int)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProcessInfo:
|
||||||
|
pid: ProcessId
|
||||||
|
binary_path: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
if sys.platform == "win32":
|
||||||
|
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
|
||||||
|
from ctypes.wintypes import DWORD
|
||||||
|
|
||||||
|
|
||||||
|
def pids() -> Iterable[ProcessId]:
|
||||||
|
_PROC_ID_T = DWORD
|
||||||
|
list_size = 4096
|
||||||
|
|
||||||
|
def try_get_pids(list_size: int) -> List[ProcessId]:
|
||||||
|
result_size = DWORD()
|
||||||
|
proc_id_list = (_PROC_ID_T * list_size)()
|
||||||
|
|
||||||
|
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
|
||||||
|
raise WinError(descr="Failed to get process ID list: %s" % FormatError()) # type: ignore
|
||||||
|
|
||||||
|
return cast(List[ProcessId], proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))])
|
||||||
|
|
||||||
|
while True:
|
||||||
|
proc_ids = try_get_pids(list_size)
|
||||||
|
if len(proc_ids) < list_size:
|
||||||
|
return proc_ids
|
||||||
|
|
||||||
|
list_size *= 2
|
||||||
|
|
||||||
|
|
||||||
|
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||||
|
_PROC_QUERY_LIMITED_INFORMATION = 0x1000
|
||||||
|
|
||||||
|
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||||
|
|
||||||
|
h_process = windll.kernel32.OpenProcess(_PROC_QUERY_LIMITED_INFORMATION, False, pid)
|
||||||
|
if not h_process:
|
||||||
|
return process_info
|
||||||
|
|
||||||
|
try:
|
||||||
|
def get_exe_path() -> Optional[str]:
|
||||||
|
_MAX_PATH = 260
|
||||||
|
_WIN32_PATH_FORMAT = 0x0000
|
||||||
|
|
||||||
|
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
|
||||||
|
exe_path_len = DWORD(len(exe_path_buffer))
|
||||||
|
|
||||||
|
return cast(str, exe_path_buffer[:exe_path_len.value]) if windll.kernel32.QueryFullProcessImageNameW(
|
||||||
|
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
|
||||||
|
) else None
|
||||||
|
|
||||||
|
process_info.binary_path = get_exe_path()
|
||||||
|
finally:
|
||||||
|
windll.kernel32.CloseHandle(h_process)
|
||||||
|
return process_info
|
||||||
|
else:
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
|
def pids() -> Iterable[ProcessId]:
|
||||||
|
for pid in psutil.pids():
|
||||||
|
yield pid
|
||||||
|
|
||||||
|
|
||||||
|
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||||
|
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||||
|
try:
|
||||||
|
process_info.binary_path = psutil.Process(pid=pid).as_dict(attrs=["exe"])["exe"]
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
return process_info
|
||||||
|
|
||||||
|
|
||||||
|
def process_iter() -> Iterable[Optional[ProcessInfo]]:
|
||||||
|
for pid in pids():
|
||||||
|
yield get_process_info(pid)
|
||||||
28
src/galaxy/reader.py
Normal file
28
src/galaxy/reader.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from asyncio import StreamReader
|
||||||
|
|
||||||
|
|
||||||
|
class StreamLineReader:
|
||||||
|
"""Handles StreamReader readline without buffer limit"""
|
||||||
|
def __init__(self, reader: StreamReader):
|
||||||
|
self._reader = reader
|
||||||
|
self._buffer = bytes()
|
||||||
|
self._processed_buffer_it = 0
|
||||||
|
|
||||||
|
async def readline(self):
|
||||||
|
while True:
|
||||||
|
# check if there is no unprocessed data in the buffer
|
||||||
|
if not self._buffer or self._processed_buffer_it != 0:
|
||||||
|
chunk = await self._reader.read(1024)
|
||||||
|
if not chunk:
|
||||||
|
return bytes() # EOF
|
||||||
|
self._buffer += chunk
|
||||||
|
|
||||||
|
it = self._buffer.find(b"\n", self._processed_buffer_it)
|
||||||
|
if it < 0:
|
||||||
|
self._processed_buffer_it = len(self._buffer)
|
||||||
|
continue
|
||||||
|
|
||||||
|
line = self._buffer[:it]
|
||||||
|
self._buffer = self._buffer[it+1:]
|
||||||
|
self._processed_buffer_it = 0
|
||||||
|
return line
|
||||||
98
src/galaxy/registry_monitor.py
Normal file
98
src/galaxy/registry_monitor.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import sys
|
||||||
|
if sys.platform == "win32":
|
||||||
|
import logging
|
||||||
|
import ctypes
|
||||||
|
from ctypes.wintypes import LONG, HKEY, LPCWSTR, DWORD, BOOL, HANDLE, LPVOID
|
||||||
|
|
||||||
|
LPSECURITY_ATTRIBUTES = LPVOID
|
||||||
|
|
||||||
|
RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
|
||||||
|
RegOpenKeyEx.restype = LONG
|
||||||
|
RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
|
||||||
|
|
||||||
|
RegCloseKey = ctypes.windll.advapi32.RegCloseKey
|
||||||
|
RegCloseKey.restype = LONG
|
||||||
|
RegCloseKey.argtypes = [HKEY]
|
||||||
|
|
||||||
|
RegNotifyChangeKeyValue = ctypes.windll.advapi32.RegNotifyChangeKeyValue
|
||||||
|
RegNotifyChangeKeyValue.restype = LONG
|
||||||
|
RegNotifyChangeKeyValue.argtypes = [HKEY, BOOL, DWORD, HANDLE, BOOL]
|
||||||
|
|
||||||
|
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||||
|
CloseHandle.restype = BOOL
|
||||||
|
CloseHandle.argtypes = [HANDLE]
|
||||||
|
|
||||||
|
CreateEvent = ctypes.windll.kernel32.CreateEventW
|
||||||
|
CreateEvent.restype = BOOL
|
||||||
|
CreateEvent.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
|
||||||
|
|
||||||
|
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
|
||||||
|
WaitForSingleObject.restype = DWORD
|
||||||
|
WaitForSingleObject.argtypes = [HANDLE, DWORD]
|
||||||
|
|
||||||
|
ERROR_SUCCESS = 0x00000000
|
||||||
|
|
||||||
|
KEY_READ = 0x00020019
|
||||||
|
KEY_QUERY_VALUE = 0x00000001
|
||||||
|
|
||||||
|
REG_NOTIFY_CHANGE_NAME = 0x00000001
|
||||||
|
REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
|
||||||
|
|
||||||
|
WAIT_OBJECT_0 = 0x00000000
|
||||||
|
WAIT_TIMEOUT = 0x00000102
|
||||||
|
|
||||||
|
class RegistryMonitor:
|
||||||
|
|
||||||
|
def __init__(self, root, subkey):
|
||||||
|
self._root = root
|
||||||
|
self._subkey = subkey
|
||||||
|
self._event = CreateEvent(None, False, False, None)
|
||||||
|
|
||||||
|
self._key = None
|
||||||
|
self._open_key()
|
||||||
|
if self._key:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
CloseHandle(self._event)
|
||||||
|
if self._key:
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def is_updated(self):
|
||||||
|
wait_result = WaitForSingleObject(self._event, 0)
|
||||||
|
|
||||||
|
# previously watched
|
||||||
|
if wait_result == WAIT_OBJECT_0:
|
||||||
|
self._set_key_update_notification()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# no changes or no key before
|
||||||
|
if wait_result != WAIT_TIMEOUT:
|
||||||
|
# unexpected error
|
||||||
|
logging.warning("Unexpected WaitForSingleObject result %s", wait_result)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._key is None:
|
||||||
|
self._open_key()
|
||||||
|
|
||||||
|
if self._key is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self._set_key_update_notification()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _set_key_update_notification(self):
|
||||||
|
filter_ = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
|
||||||
|
status = RegNotifyChangeKeyValue(self._key, True, filter_, self._event, True)
|
||||||
|
if status != ERROR_SUCCESS:
|
||||||
|
# key was deleted
|
||||||
|
RegCloseKey(self._key)
|
||||||
|
self._key = None
|
||||||
|
|
||||||
|
def _open_key(self):
|
||||||
|
access = KEY_QUERY_VALUE | KEY_READ
|
||||||
|
self._key = HKEY()
|
||||||
|
rc = RegOpenKeyEx(self._root, self._subkey, 0, access, ctypes.byref(self._key))
|
||||||
|
if rc != ERROR_SUCCESS:
|
||||||
|
self._key = None
|
||||||
49
src/galaxy/task_manager.py
Normal file
49
src/galaxy/task_manager.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from itertools import count
|
||||||
|
|
||||||
|
class TaskManager:
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
self._tasks = OrderedDict()
|
||||||
|
self._task_counter = count()
|
||||||
|
|
||||||
|
def create_task(self, coro, description, handle_exceptions=True):
|
||||||
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
|
|
||||||
|
async def task_wrapper(task_id):
|
||||||
|
try:
|
||||||
|
result = await coro
|
||||||
|
logging.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||||
|
return result
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
if handle_exceptions:
|
||||||
|
logging.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
if handle_exceptions:
|
||||||
|
logging.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
del self._tasks[task_id]
|
||||||
|
|
||||||
|
task_id = next(self._task_counter)
|
||||||
|
logging.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
||||||
|
task = asyncio.create_task(task_wrapper(task_id))
|
||||||
|
self._tasks[task_id] = task
|
||||||
|
return task
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
for task in self._tasks.values():
|
||||||
|
task.cancel()
|
||||||
|
|
||||||
|
async def wait(self):
|
||||||
|
# Tasks can spawn other tasks
|
||||||
|
while True:
|
||||||
|
tasks = self._tasks.values()
|
||||||
|
if not tasks:
|
||||||
|
return
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
@@ -3,6 +3,7 @@ import os
|
|||||||
import zipfile
|
import zipfile
|
||||||
from glob import glob
|
from glob import glob
|
||||||
|
|
||||||
|
|
||||||
def zip_folder(folder):
|
def zip_folder(folder):
|
||||||
files = glob(os.path.join(folder, "**"), recursive=True)
|
files = glob(os.path.join(folder, "**"), recursive=True)
|
||||||
files = [file.replace(folder + os.sep, "") for file in files]
|
files = [file.replace(folder + os.sep, "") for file in files]
|
||||||
@@ -14,6 +15,7 @@ def zip_folder(folder):
|
|||||||
zipf.write(os.path.join(folder, file), arcname=file)
|
zipf.write(os.path.join(folder, file), arcname=file)
|
||||||
return zip_buffer
|
return zip_buffer
|
||||||
|
|
||||||
|
|
||||||
def zip_folder_to_file(folder, filename):
|
def zip_folder_to_file(folder, filename):
|
||||||
zip_content = zip_folder(folder).getbuffer()
|
zip_content = zip_folder(folder).getbuffer()
|
||||||
with open(filename, "wb") as archive:
|
with open(filename, "wb") as archive:
|
||||||
|
|||||||
@@ -1,12 +1,31 @@
|
|||||||
from asyncio import coroutine
|
import asyncio
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
|
||||||
class AsyncMock(MagicMock):
|
class AsyncMock(MagicMock):
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
async def __call__(self, *args, **kwargs):
|
async def __call__(self, *args, **kwargs):
|
||||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def coroutine_mock():
|
def coroutine_mock():
|
||||||
|
"""
|
||||||
|
.. deprecated:: 0.45
|
||||||
|
Use: :class:`MagicMock` with meth:`~.async_return_value`.
|
||||||
|
"""
|
||||||
coro = MagicMock(name="CoroutineResult")
|
coro = MagicMock(name="CoroutineResult")
|
||||||
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
|
corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
|
||||||
corofunc.coro = coro
|
corofunc.coro = coro
|
||||||
return corofunc
|
return corofunc
|
||||||
|
|
||||||
|
async def skip_loop(iterations=1):
|
||||||
|
for _ in range(iterations):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
return return_value
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def create_message(request):
|
||||||
|
return json.dumps(request).encode() + b"\n"
|
||||||
|
|
||||||
|
|
||||||
|
def get_messages(write_mock):
|
||||||
|
messages = []
|
||||||
|
for call_args in write_mock.call_args_list:
|
||||||
|
data = call_args[0][0]
|
||||||
|
for line in data.splitlines():
|
||||||
|
message = json.loads(line)
|
||||||
|
messages.append(message)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
|||||||
@@ -6,62 +6,63 @@ 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 AsyncMock, coroutine_mock
|
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.readline = AsyncMock()
|
stream.read = MagicMock()
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def writer():
|
async def writer():
|
||||||
stream = MagicMock(name="stream_writer")
|
stream = MagicMock(name="stream_writer")
|
||||||
stream.write = MagicMock()
|
stream.drain.side_effect = lambda: async_return_value(None)
|
||||||
stream.drain = AsyncMock()
|
|
||||||
yield stream
|
yield stream
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def readline(reader):
|
def read(reader):
|
||||||
yield reader.readline
|
yield reader.read
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def write(writer):
|
def write(writer):
|
||||||
yield writer.write
|
yield writer.write
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def plugin(reader, writer):
|
async def plugin(reader, writer):
|
||||||
"""Return plugin instance with all feature methods mocked"""
|
"""Return plugin instance with all feature methods mocked"""
|
||||||
async_methods = (
|
methods = (
|
||||||
"handshake_complete",
|
"handshake_complete",
|
||||||
"authenticate",
|
"authenticate",
|
||||||
"get_owned_games",
|
"get_owned_games",
|
||||||
|
"prepare_achievements_context",
|
||||||
"get_unlocked_achievements",
|
"get_unlocked_achievements",
|
||||||
|
"achievements_import_complete",
|
||||||
"get_local_games",
|
"get_local_games",
|
||||||
"launch_game",
|
"launch_game",
|
||||||
|
"launch_platform_client",
|
||||||
"install_game",
|
"install_game",
|
||||||
"uninstall_game",
|
"uninstall_game",
|
||||||
"get_friends",
|
"get_friends",
|
||||||
"get_users",
|
"get_game_time",
|
||||||
"send_message",
|
"prepare_game_times_context",
|
||||||
"mark_as_read",
|
"game_times_import_complete",
|
||||||
"get_rooms",
|
"shutdown_platform_client",
|
||||||
"get_room_history_from_message",
|
|
||||||
"get_room_history_from_timestamp",
|
|
||||||
"get_game_times"
|
|
||||||
)
|
|
||||||
|
|
||||||
methods = (
|
|
||||||
"shutdown",
|
"shutdown",
|
||||||
"tick"
|
"tick",
|
||||||
|
"get_game_library_settings",
|
||||||
|
"prepare_game_library_settings_context",
|
||||||
|
"game_library_settings_import_complete",
|
||||||
)
|
)
|
||||||
|
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in async_methods:
|
|
||||||
stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
|
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
|
||||||
|
async with Plugin(Platform.Generic, "0.1", reader, writer, "token") as plugin:
|
||||||
|
plugin.shutdown.return_value = async_return_value(None)
|
||||||
|
yield plugin
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def my_caplog(caplog):
|
def my_caplog(caplog):
|
||||||
|
|||||||
@@ -1,94 +1,206 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
from unittest.mock import call
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import raises
|
from pytest import raises
|
||||||
|
|
||||||
from galaxy.api.types import Achievement
|
from galaxy.api.types import Achievement
|
||||||
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_unlock_time():
|
def test_initialization_no_unlock_time():
|
||||||
with raises(Exception):
|
with raises(Exception):
|
||||||
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
||||||
|
|
||||||
|
|
||||||
def test_initialization_no_id_nor_name():
|
def test_initialization_no_id_nor_name():
|
||||||
with raises(AssertionError):
|
with raises(AssertionError):
|
||||||
Achievement(unlock_time=1234567890)
|
Achievement(unlock_time=1234567890)
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_unlocked_achievements_success(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(5)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.return_value = [
|
plugin.get_unlocked_achievements.return_value = async_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)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
plugin.prepare_achievements_context.assert_called_with(["14"])
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.get_unlocked_achievements.assert_called_with("14", 5)
|
||||||
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"unlocked_achievements": [
|
"result": None
|
||||||
{
|
},
|
||||||
"achievement_id": "lvl10",
|
{
|
||||||
"unlock_time": 1548421241
|
"jsonrpc": "2.0",
|
||||||
},
|
"method": "game_achievements_import_success",
|
||||||
{
|
"params": {
|
||||||
"achievement_name": "Got level 20",
|
"game_id": "14",
|
||||||
"unlock_time": 1548422395
|
"unlocked_achievements": [
|
||||||
},
|
{
|
||||||
{
|
"achievement_id": "lvl10",
|
||||||
"achievement_id": "lvl30",
|
"unlock_time": 1548421241
|
||||||
"achievement_name": "Got level 30",
|
},
|
||||||
"unlock_time": 1548495633
|
{
|
||||||
}
|
"achievement_name": "Got level 20",
|
||||||
]
|
"unlock_time": 1548422395
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl30",
|
||||||
|
"achievement_name": "Got level 30",
|
||||||
|
"unlock_time": 1548495633
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_unlocked_achievements_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_unlocked_achievements",
|
"method": "start_achievements_import",
|
||||||
"params": {
|
"params": {
|
||||||
"game_id": "14"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
|
plugin.get_unlocked_achievements.side_effect = exception
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
response = json.loads(write.call_args[0][0])
|
plugin.achievements_import_complete.asert_called_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_achievements_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "14",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_unlocked_achievements_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"error": {
|
"method": "start_achievements_import",
|
||||||
"code": 0,
|
"params": {
|
||||||
"message": "Unknown error"
|
"game_ids": ["14"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
|
||||||
def test_unlock_achievement(plugin, write):
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_achievements_context.return_value = async_return_value(None)
|
||||||
|
plugin.get_unlocked_achievements.return_value = async_return_value([])
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_achievements_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["14"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_achievements_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["15"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
|
plugin.unlock_achievement("14", achievement)
|
||||||
async def couritine():
|
|
||||||
plugin.unlock_achievement("14", achievement)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
assert response == {
|
assert response == {
|
||||||
@@ -102,92 +214,3 @@ def test_unlock_achievement(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_success(plugin, write):
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.game_achievements_import_success("134", achievements)
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"unlocked_achievements": [
|
|
||||||
{
|
|
||||||
"achievement_id": "lvl10",
|
|
||||||
"unlock_time": 1548421241
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"achievement_name": "Got level 20",
|
|
||||||
"unlock_time": 1548422395
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_achievements_import_failure(plugin, write):
|
|
||||||
plugin.game_achievements_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_achievements_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_achievements_import_finished(plugin, write):
|
|
||||||
plugin.achievements_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "achievements_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_achievements_import(plugin, write, mocker):
|
|
||||||
game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success")
|
|
||||||
game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure")
|
|
||||||
achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5", "9"]
|
|
||||||
error = BackendError()
|
|
||||||
achievements = [
|
|
||||||
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
|
||||||
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
|
||||||
]
|
|
||||||
plugin.get_unlocked_achievements.coro.side_effect = [
|
|
||||||
achievements,
|
|
||||||
[],
|
|
||||||
error
|
|
||||||
]
|
|
||||||
await plugin.start_achievements_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_achievements_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")])
|
|
||||||
game_achievements_import_success.assert_has_calls([
|
|
||||||
call("1", achievements),
|
|
||||||
call("5", [])
|
|
||||||
])
|
|
||||||
game_achievements_import_failure.assert_called_once_with("9", error)
|
|
||||||
achievements_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from galaxy.api.types import Authentication
|
from galaxy.api.types import Authentication
|
||||||
@@ -8,29 +5,36 @@ from galaxy.api.errors import (
|
|||||||
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
|
||||||
)
|
)
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
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)]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"user_id": "132",
|
"result": {
|
||||||
"user_name": "Zenek"
|
"user_id": "132",
|
||||||
|
"user_name": "Zenek"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
@pytest.mark.parametrize("error,code,message", [
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||||
@@ -44,29 +48,32 @@ def test_success(plugin, readline, write):
|
|||||||
pytest.param(Banned, 105, "Banned", id="banned"),
|
pytest.param(Banned, 105, "Banned", id="banned"),
|
||||||
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
|
||||||
])
|
])
|
||||||
def test_failure(plugin, readline, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "init_authentication"
|
"method": "init_authentication"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.coro.side_effect = error()
|
plugin.authenticate.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_stored_credentials(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_stored_credentials(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
@@ -77,39 +84,37 @@ def test_stored_credentials(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
write.assert_called()
|
write.assert_called()
|
||||||
|
|
||||||
def test_store_credentials(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_store_credentials(plugin, write):
|
||||||
credentials = {
|
credentials = {
|
||||||
"token": "ABC"
|
"token": "ABC"
|
||||||
}
|
}
|
||||||
|
plugin.store_credentials(credentials)
|
||||||
|
|
||||||
async def couritine():
|
assert get_messages(write) == [
|
||||||
plugin.store_credentials(credentials)
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "store_credentials",
|
||||||
|
"params": credentials
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
@pytest.mark.asyncio
|
||||||
"jsonrpc": "2.0",
|
async def test_lost_authentication(plugin, write):
|
||||||
"method": "store_credentials",
|
plugin.lost_authentication()
|
||||||
"params": credentials
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_lost_authentication(plugin, readline, write):
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
async def couritine():
|
"jsonrpc": "2.0",
|
||||||
plugin.lost_authentication()
|
"method": "authentication_lost",
|
||||||
|
"params": None
|
||||||
asyncio.run(couritine())
|
}
|
||||||
response = json.loads(write.call_args[0][0])
|
]
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "authentication_lost",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,354 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from galaxy.api.types import Room, Message
|
|
||||||
from galaxy.api.errors import (
|
|
||||||
UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError,
|
|
||||||
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_send_message_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "send_message",
|
|
||||||
"params": {
|
|
||||||
"room_id": "14",
|
|
||||||
"message": "Hello!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.send_message.coro.return_value = None
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"result": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
|
||||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
|
||||||
pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages")
|
|
||||||
])
|
|
||||||
def test_send_message_failure(plugin, readline, write, error, code, message):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "6",
|
|
||||||
"method": "send_message",
|
|
||||||
"params": {
|
|
||||||
"room_id": "15",
|
|
||||||
"message": "Bye"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.send_message.coro.side_effect = error()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "6",
|
|
||||||
"error": {
|
|
||||||
"code": code,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_mark_as_read_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"method": "mark_as_read",
|
|
||||||
"params": {
|
|
||||||
"room_id": "14",
|
|
||||||
"last_message_id": "67"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.mark_as_read.coro.return_value = None
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"result": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
|
||||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
|
||||||
pytest.param(
|
|
||||||
IncoherentLastMessage,
|
|
||||||
400,
|
|
||||||
"Different last message id on backend",
|
|
||||||
id="incoherent_last_message"
|
|
||||||
)
|
|
||||||
])
|
|
||||||
def test_mark_as_read_failure(plugin, readline, write, error, code, message):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "4",
|
|
||||||
"method": "mark_as_read",
|
|
||||||
"params": {
|
|
||||||
"room_id": "18",
|
|
||||||
"last_message_id": "7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.mark_as_read.coro.side_effect = error()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "4",
|
|
||||||
"error": {
|
|
||||||
"code": code,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_rooms_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "2",
|
|
||||||
"method": "import_rooms"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_rooms.coro.return_value = [
|
|
||||||
Room("13", 0, None),
|
|
||||||
Room("15", 34, "8")
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_rooms.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "2",
|
|
||||||
"result": {
|
|
||||||
"rooms": [
|
|
||||||
{
|
|
||||||
"room_id": "13",
|
|
||||||
"unread_message_count": 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"room_id": "15",
|
|
||||||
"unread_message_count": 34,
|
|
||||||
"last_message_id": "8"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_rooms_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "9",
|
|
||||||
"method": "import_rooms"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_rooms.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_rooms.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "9",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_room_history_from_message_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "2",
|
|
||||||
"method": "import_room_history_from_message",
|
|
||||||
"params": {
|
|
||||||
"room_id": "34",
|
|
||||||
"message_id": "66"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_room_history_from_message.coro.return_value = [
|
|
||||||
Message("13", "149", 1549454837, "Hello"),
|
|
||||||
Message("14", "812", 1549454899, "Hi")
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "2",
|
|
||||||
"result": {
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"message_id": "13",
|
|
||||||
"sender_id": "149",
|
|
||||||
"sent_time": 1549454837,
|
|
||||||
"message_text": "Hello"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"message_id": "14",
|
|
||||||
"sender_id": "812",
|
|
||||||
"sent_time": 1549454899,
|
|
||||||
"message_text": "Hi"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("error,code,message", [
|
|
||||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
|
||||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
|
||||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
|
||||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
|
||||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
|
||||||
pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found")
|
|
||||||
])
|
|
||||||
def test_get_room_history_from_message_failure(plugin, readline, write, error, code, message):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"method": "import_room_history_from_message",
|
|
||||||
"params": {
|
|
||||||
"room_id": "33",
|
|
||||||
"message_id": "88"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_room_history_from_message.coro.side_effect = error()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"error": {
|
|
||||||
"code": code,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_room_history_from_timestamp_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"method": "import_room_history_from_timestamp",
|
|
||||||
"params": {
|
|
||||||
"room_id": "12",
|
|
||||||
"from_timestamp": 1549454835
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_room_history_from_timestamp.coro.return_value = [
|
|
||||||
Message("12", "155", 1549454836, "Bye")
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
|
||||||
room_id="12",
|
|
||||||
from_timestamp=1549454835
|
|
||||||
)
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "7",
|
|
||||||
"result": {
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"message_id": "12",
|
|
||||||
"sender_id": "155",
|
|
||||||
"sent_time": 1549454836,
|
|
||||||
"message_text": "Bye"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_room_history_from_timestamp",
|
|
||||||
"params": {
|
|
||||||
"room_id": "10",
|
|
||||||
"from_timestamp": 1549454800
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
|
||||||
room_id="10",
|
|
||||||
from_timestamp=1549454800
|
|
||||||
)
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_room(plugin, write):
|
|
||||||
messages = [
|
|
||||||
Message("10", "898", 1549454832, "Hi")
|
|
||||||
]
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_room("14", 15, messages)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "chat_room_updated",
|
|
||||||
"params": {
|
|
||||||
"room_id": "14",
|
|
||||||
"unread_message_count": 15,
|
|
||||||
"messages": [
|
|
||||||
{
|
|
||||||
"message_id": "10",
|
|
||||||
"sender_id": "898",
|
|
||||||
"sent_time": 1549454832,
|
|
||||||
"message_text": "Hi"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
tests/test_chunk_messages.py
Normal file
63
tests/test_chunk_messages.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_chunked_messages(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "install_game",
|
||||||
|
"params": {
|
||||||
|
"game_id": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = json.dumps(request).encode() + b"\n"
|
||||||
|
read.side_effect = [async_return_value(message[:5]), async_return_value(message[5:]), async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_joined_messages(plugin, read):
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "install_game",
|
||||||
|
"params": {
|
||||||
|
"game_id": "3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "launch_game",
|
||||||
|
"params": {
|
||||||
|
"game_id": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
data = b"".join([json.dumps(request).encode() + b"\n" for request in requests])
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(data), async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_not_finished(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "install_game",
|
||||||
|
"params": {
|
||||||
|
"game_id": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message = json.dumps(request).encode() # no new line
|
||||||
|
read.side_effect = [async_return_value(message), async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.install_game.assert_not_called()
|
||||||
@@ -1,45 +1,51 @@
|
|||||||
|
from galaxy.api.consts import Feature, Platform
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.consts import Platform, Feature
|
|
||||||
|
|
||||||
def test_base_class():
|
def test_base_class():
|
||||||
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert set(plugin.features) == {
|
||||||
|
Feature.ImportInstalledGames,
|
||||||
|
Feature.ImportOwnedGames,
|
||||||
|
Feature.LaunchGame,
|
||||||
|
Feature.InstallGame,
|
||||||
|
Feature.UninstallGame,
|
||||||
|
Feature.ImportAchievements,
|
||||||
|
Feature.ImportGameTime,
|
||||||
|
Feature.ImportFriends,
|
||||||
|
Feature.ShutdownPlatformClient,
|
||||||
|
Feature.LaunchPlatformClient,
|
||||||
|
Feature.ImportGameLibrarySettings
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_no_overloads():
|
def test_no_overloads():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
|
|
||||||
def test_one_method_feature():
|
def test_one_method_feature():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.ImportOwnedGames]
|
assert plugin.features == [Feature.ImportOwnedGames]
|
||||||
|
|
||||||
def test_multiple_methods_feature_all():
|
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
def test_multi_features():
|
||||||
async def send_message(self, room_id, message):
|
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||||
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
async def mark_as_read(self, room_id, last_message_id):
|
|
||||||
|
async def get_unlocked_achievements(self, game_id, context):
|
||||||
pass
|
pass
|
||||||
async def get_rooms(self):
|
|
||||||
pass
|
async def get_game_time(self, game_id, context):
|
||||||
async def get_room_history_from_message(self, room_id, message_id):
|
|
||||||
pass
|
|
||||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.Chat]
|
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime}
|
||||||
|
|
||||||
def test_multiple_methods_feature_not_all():
|
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
|
||||||
async def send_message(self, room_id, message):
|
|
||||||
pass
|
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
|
||||||
assert plugin.features == []
|
|
||||||
|
|||||||
@@ -1,90 +1,94 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.types import FriendInfo
|
from galaxy.api.types import FriendInfo
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_success(plugin, readline, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.return_value = [
|
plugin.get_friends.return_value = async_return_value([
|
||||||
FriendInfo("3", "Jan"),
|
FriendInfo("3", "Jan"),
|
||||||
FriendInfo("5", "Ola")
|
FriendInfo("5", "Ola")
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"result": {
|
"id": "3",
|
||||||
"friend_info_list": [
|
"result": {
|
||||||
{"user_id": "3", "user_name": "Jan"},
|
"friend_info_list": [
|
||||||
{"user_id": "5", "user_name": "Ola"}
|
{"user_id": "3", "user_name": "Jan"},
|
||||||
]
|
{"user_id": "5", "user_name": "Ola"}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_get_friends_failure(plugin, readline, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_friends_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_friends"
|
"method": "import_friends"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.coro.side_effect = UnknownError()
|
plugin.get_friends.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": 0,
|
"error": {
|
||||||
"message": "Unknown error",
|
"code": 0,
|
||||||
|
"message": "Unknown error",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_add_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_friend(plugin, write):
|
||||||
friend = FriendInfo("7", "Kuba")
|
friend = FriendInfo("7", "Kuba")
|
||||||
|
|
||||||
async def couritine():
|
plugin.add_friend(friend)
|
||||||
plugin.add_friend(friend)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_added",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_added",
|
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
||||||
"params": {
|
}
|
||||||
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_remove_friend(plugin, write):
|
@pytest.mark.asyncio
|
||||||
async def couritine():
|
async def test_remove_friend(plugin, write):
|
||||||
plugin.remove_friend("5")
|
plugin.remove_friend("5")
|
||||||
|
|
||||||
asyncio.run(couritine())
|
assert get_messages(write) == [
|
||||||
response = json.loads(write.call_args[0][0])
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
assert response == {
|
"method": "friend_removed",
|
||||||
"jsonrpc": "2.0",
|
"params": {
|
||||||
"method": "friend_removed",
|
"user_id": "5"
|
||||||
"params": {
|
}
|
||||||
"user_id": "5"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|||||||
196
tests/test_game_library_settings.py
Normal file
196
tests/test_game_library_settings.py
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from galaxy.api.types import GameLibrarySettings
|
||||||
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_game_time_success(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = [
|
||||||
|
async_return_value(GameLibrarySettings("3", None, True)),
|
||||||
|
async_return_value(GameLibrarySettings("5", [], False)),
|
||||||
|
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
|
||||||
|
]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_has_calls([
|
||||||
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "3",
|
||||||
|
"hidden": True
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "5",
|
||||||
|
"tags": [],
|
||||||
|
"hidden": False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_library_settings": {
|
||||||
|
"game_id": "7",
|
||||||
|
"tags": ["tag1", "tag2", "tag3"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_game_library_settings_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_library_settings.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_library_settings.assert_called()
|
||||||
|
plugin.game_library_settings_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_library_settings_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_game_library_settings_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_game_library_settings_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_game_library_settings_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
@@ -1,175 +1,215 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
from unittest.mock import call
|
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 UnknownError, ImportInProgress, BackendError
|
from galaxy.api.errors import BackendError
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_game_time_success(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value("abc")
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_game_times"
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["3", "5", "7"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_game_time.side_effect = [
|
||||||
plugin.get_game_times.coro.return_value = [
|
async_return_value(GameTime("3", 60, 1549550504)),
|
||||||
GameTime("3", 60, 1549550504),
|
async_return_value(GameTime("5", 10, None)),
|
||||||
GameTime("5", 10, 1549550502)
|
async_return_value(GameTime("7", None, 1549550502)),
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_game_times.assert_called_with()
|
plugin.get_game_time.assert_has_calls([
|
||||||
response = json.loads(write.call_args[0][0])
|
call("3", "abc"),
|
||||||
|
call("5", "abc"),
|
||||||
|
call("7", "abc"),
|
||||||
|
])
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "3",
|
||||||
|
"last_played_time": 1549550504,
|
||||||
|
"time_played": 60
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "5",
|
||||||
|
"time_played": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "7",
|
||||||
|
"last_played_time": 1549550502
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize("exception,code,message", [
|
||||||
|
(BackendError, 4, "Backend error"),
|
||||||
|
(KeyError, 0, "Unknown error")
|
||||||
|
])
|
||||||
|
async def test_get_game_time_error(exception, code, message, plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"method": "start_game_times_import",
|
||||||
"game_times": [
|
"params": {
|
||||||
{
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
plugin.get_game_time.side_effect = exception
|
||||||
|
await plugin.run()
|
||||||
|
plugin.get_game_time.assert_called()
|
||||||
|
plugin.game_times_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "6",
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prepare_get_game_time_context_error(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.side_effect = BackendError()
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 4,
|
||||||
|
"message": "Backend error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_import_in_progress(plugin, read, write):
|
||||||
|
plugin.prepare_game_times_context.return_value = async_return_value(None)
|
||||||
|
requests = [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["6"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"method": "start_game_times_import",
|
||||||
|
"params": {
|
||||||
|
"game_ids": ["7"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
read.side_effect = [
|
||||||
|
async_return_value(create_message(requests[0])),
|
||||||
|
async_return_value(create_message(requests[1])),
|
||||||
|
async_return_value(b"", 10)
|
||||||
|
]
|
||||||
|
|
||||||
|
await plugin.run()
|
||||||
|
|
||||||
|
messages = get_messages(write)
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": None
|
||||||
|
} in messages
|
||||||
|
assert {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "4",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
} in messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
|
game_time = GameTime("3", 60, 1549550504)
|
||||||
|
plugin.update_game_time(game_time)
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_updated",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"time_played": 60,
|
"time_played": 60,
|
||||||
"last_played_time": 1549550504
|
"last_played_time": 1549550504
|
||||||
},
|
|
||||||
{
|
|
||||||
"game_id": "5",
|
|
||||||
"time_played": 10,
|
|
||||||
"last_played_time": 1549550502
|
|
||||||
}
|
}
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_game_times"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_game_times.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_game_times.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
game_time = GameTime("3", 60, 1549550504)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_game_time(game_time)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_updated",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_success(plugin, write):
|
|
||||||
plugin.game_time_import_success(GameTime("3", 60, 1549550504))
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_success",
|
|
||||||
"params": {
|
|
||||||
"game_time": {
|
|
||||||
"game_id": "3",
|
|
||||||
"time_played": 60,
|
|
||||||
"last_played_time": 1549550504
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_time_import_failure(plugin, write):
|
|
||||||
plugin.game_time_import_failure("134", ImportInProgress())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_time_import_failure",
|
|
||||||
"params": {
|
|
||||||
"game_id": "134",
|
|
||||||
"error": {
|
|
||||||
"code": 600,
|
|
||||||
"message": "Import already in progress"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_game_times_import_finished(plugin, write):
|
|
||||||
plugin.game_times_import_finished()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "game_times_import_finished",
|
|
||||||
"params": None
|
|
||||||
}
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import(plugin, write, mocker):
|
|
||||||
game_time_import_success = mocker.patch.object(plugin, "game_time_import_success")
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
game_time = GameTime("1", 10, 1549550502)
|
|
||||||
plugin.get_game_times.coro.return_value = [
|
|
||||||
game_time
|
|
||||||
]
|
]
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
with pytest.raises(ImportInProgress):
|
|
||||||
await plugin.start_game_times_import(["4", "8"])
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
game_time_import_success.assert_called_once_with(game_time)
|
|
||||||
game_time_import_failure.assert_called_once_with("5", UnknownError())
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_start_game_times_import_failure(plugin, write, mocker):
|
|
||||||
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
|
||||||
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
|
||||||
|
|
||||||
game_ids = ["1", "5"]
|
|
||||||
error = BackendError()
|
|
||||||
plugin.get_game_times.coro.side_effect = error
|
|
||||||
|
|
||||||
await plugin.start_game_times_import(game_ids)
|
|
||||||
|
|
||||||
# wait until all tasks are finished
|
|
||||||
for _ in range(4):
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
|
|
||||||
plugin.get_game_times.coro.assert_called_once_with()
|
|
||||||
|
|
||||||
assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)]
|
|
||||||
game_times_import_finished.assert_called_once_with()
|
|
||||||
|
|||||||
39
tests/test_http.py
Normal file
39
tests/test_http.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import asyncio
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import pytest
|
||||||
|
from multidict import CIMultiDict, CIMultiDictProxy
|
||||||
|
from yarl import URL
|
||||||
|
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
|
||||||
|
TooManyRequests, UnknownBackendResponse, UnknownError
|
||||||
|
)
|
||||||
|
from galaxy.http import handle_exception
|
||||||
|
|
||||||
|
request_info = aiohttp.RequestInfo(URL("http://o.pl"), "GET", CIMultiDictProxy(CIMultiDict()))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"aiohttp_exception,expected_exception_type",
|
||||||
|
[
|
||||||
|
(asyncio.TimeoutError(), BackendTimeout),
|
||||||
|
(aiohttp.ServerDisconnectedError(), BackendNotAvailable),
|
||||||
|
(aiohttp.ClientConnectionError(), NetworkError),
|
||||||
|
(aiohttp.ContentTypeError(request_info, ()), UnknownBackendResponse),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.UNAUTHORIZED), AuthenticationRequired),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.FORBIDDEN), AccessDenied),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.SERVICE_UNAVAILABLE), BackendNotAvailable),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.TOO_MANY_REQUESTS), TooManyRequests),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.INTERNAL_SERVER_ERROR), BackendError),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_IMPLEMENTED), BackendError),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.BAD_REQUEST), UnknownError),
|
||||||
|
(aiohttp.ClientResponseError(request_info, (), status=HTTPStatus.NOT_FOUND), UnknownError),
|
||||||
|
(aiohttp.ClientError(), UnknownError)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def test_handle_exception(aiohttp_exception, expected_exception_type):
|
||||||
|
with pytest.raises(expected_exception_type):
|
||||||
|
with handle_exception():
|
||||||
|
raise aiohttp_exception
|
||||||
|
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, readline):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "install_game",
|
"method": "install_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
plugin.get_owned_games.return_value = None
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.install_game.assert_called_with(game_id="3")
|
plugin.install_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def test_get_capabilites(reader, writer, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_capabilities(reader, writer, read, write):
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
@@ -16,53 +20,76 @@ def test_get_capabilites(reader, writer, readline, write):
|
|||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"platform_name": "generic",
|
"platform_name": "generic",
|
||||||
"features": [
|
"features": [
|
||||||
"ImportOwnedGames"
|
"ImportOwnedGames"
|
||||||
],
|
],
|
||||||
"token": token
|
"token": token
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_shutdown(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_shutdown(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"method": "shutdown"
|
"method": "shutdown"
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request)]
|
read.side_effect = [async_return_value(create_message(request))]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
|
await plugin.wait_closed()
|
||||||
plugin.shutdown.assert_called_with()
|
plugin.shutdown.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "5",
|
"id": "5",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_ping(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_ping(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"method": "ping"
|
"method": "ping"
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "7",
|
"id": "7",
|
||||||
"result": None
|
"result": None
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def test_tick(plugin, readline):
|
|
||||||
readline.side_effect = [""]
|
@pytest.mark.asyncio
|
||||||
asyncio.run(plugin.run())
|
async def test_tick_before_handshake(plugin, read):
|
||||||
|
read.side_effect = [async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
|
plugin.tick.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_tick_after_handshake(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "6",
|
||||||
|
"method": "initialize_cache",
|
||||||
|
"params": {"data": {}}
|
||||||
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
await plugin.run()
|
||||||
plugin.tick.assert_called_with()
|
plugin.tick.assert_called_with()
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, readline):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "launch_game",
|
"method": "launch_game",
|
||||||
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
plugin.get_owned_games.return_value = None
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.launch_game.assert_called_with(game_id="3")
|
plugin.launch_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
17
tests/test_launch_platform_client.py
Normal file
17
tests/test_launch_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "launch_platform_client"
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
plugin.launch_platform_client.return_value = async_return_value(None)
|
||||||
|
await plugin.run()
|
||||||
|
plugin.launch_platform_client.assert_called_with()
|
||||||
@@ -1,51 +1,55 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
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
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
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)]
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_local_games.return_value = async_return_value([
|
||||||
|
|
||||||
plugin.get_local_games.coro.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)
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
|
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
assert response == {
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": LocalGameState.Running.value
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": LocalGameState.Installed.value
|
"local_game_state": LocalGameState.Installed.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "3",
|
"game_id": "3",
|
||||||
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"error,code,message",
|
"error,code,message",
|
||||||
[
|
[
|
||||||
@@ -53,44 +57,42 @@ def test_success(plugin, readline, write):
|
|||||||
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_failure(plugin, readline, write, error, code, message):
|
async def test_failure(plugin, read, write, error, code, message):
|
||||||
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)]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_local_games.side_effect = error()
|
||||||
plugin.get_local_games.coro.side_effect = error()
|
await plugin.run()
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": "3",
|
"jsonrpc": "2.0",
|
||||||
"error": {
|
"id": "3",
|
||||||
"code": code,
|
"error": {
|
||||||
"message": message
|
"code": code,
|
||||||
}
|
"message": message
|
||||||
}
|
|
||||||
|
|
||||||
def test_local_game_state_update(plugin, write):
|
|
||||||
game = LocalGame("1", LocalGameState.Running)
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_local_game_status(game)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "local_game_status_changed",
|
|
||||||
"params": {
|
|
||||||
"local_game": {
|
|
||||||
"game_id": "1",
|
|
||||||
"local_game_state": LocalGameState.Running.value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_local_game_state_update(plugin, write):
|
||||||
|
game = LocalGame("1", LocalGameState.Running)
|
||||||
|
plugin.update_local_game_status(game)
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "local_game_status_changed",
|
||||||
|
"params": {
|
||||||
|
"local_game": {
|
||||||
|
"game_id": "1",
|
||||||
|
"local_game_state": LocalGameState.Running.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"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)]
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
plugin.get_owned_games.return_value = async_return_value([
|
||||||
plugin.get_owned_games.coro.return_value = [
|
|
||||||
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
@@ -23,129 +27,126 @@ def test_success(plugin, readline, 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))
|
||||||
]
|
])
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"result": {
|
||||||
"result": {
|
"owned_games": [
|
||||||
"owned_games": [
|
{
|
||||||
{
|
"game_id": "3",
|
||||||
"game_id": "3",
|
"game_title": "Doom",
|
||||||
"game_title": "Doom",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"game_id": "5",
|
||||||
"game_id": "5",
|
"game_title": "Witcher 3",
|
||||||
"game_title": "Witcher 3",
|
"dlcs": [
|
||||||
"dlcs": [
|
{
|
||||||
{
|
"dlc_id": "7",
|
||||||
"dlc_id": "7",
|
"dlc_title": "Hearts of Stone",
|
||||||
"dlc_title": "Hearts of Stone",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "SinglePurchase"
|
||||||
"license_type": "SinglePurchase"
|
}
|
||||||
}
|
},
|
||||||
},
|
{
|
||||||
{
|
"dlc_id": "8",
|
||||||
"dlc_id": "8",
|
"dlc_title": "Temerian Armor Set",
|
||||||
"dlc_title": "Temerian Armor Set",
|
"license_info": {
|
||||||
"license_info": {
|
"license_type": "FreeToPlay"
|
||||||
"license_type": "FreeToPlay"
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"license_info": {
|
||||||
|
"license_type": "SinglePurchase"
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_failure(plugin, readline, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_failure(plugin, read, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "import_owned_games"
|
"method": "import_owned_games"
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_owned_games.coro.side_effect = UnknownError()
|
plugin.get_owned_games.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
assert response == {
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": "3",
|
||||||
"id": "3",
|
"error": {
|
||||||
"error": {
|
"code": 0,
|
||||||
"code": 0,
|
"message": "Unknown error"
|
||||||
"message": "Unknown error"
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_add_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.add_game(game)
|
||||||
async def couritine():
|
assert get_messages(write) == [
|
||||||
plugin.add_game(game)
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
asyncio.run(couritine())
|
"method": "owned_game_added",
|
||||||
response = json.loads(write.call_args[0][0])
|
"params": {
|
||||||
|
"owned_game": {
|
||||||
assert response == {
|
"game_id": "3",
|
||||||
"jsonrpc": "2.0",
|
"game_title": "Doom",
|
||||||
"method": "owned_game_added",
|
"license_info": {
|
||||||
"params": {
|
"license_type": "SinglePurchase"
|
||||||
"owned_game": {
|
}
|
||||||
"game_id": "3",
|
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_remove_game(plugin, write):
|
|
||||||
async def couritine():
|
|
||||||
plugin.remove_game("5")
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
@pytest.mark.asyncio
|
||||||
response = json.loads(write.call_args[0][0])
|
async def test_remove_game(plugin, write):
|
||||||
|
plugin.remove_game("5")
|
||||||
assert response == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"method": "owned_game_removed",
|
"jsonrpc": "2.0",
|
||||||
"params": {
|
"method": "owned_game_removed",
|
||||||
"game_id": "5"
|
"params": {
|
||||||
|
"game_id": "5"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
plugin.update_game(game)
|
||||||
async def couritine():
|
assert get_messages(write) == [
|
||||||
plugin.update_game(game)
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
asyncio.run(couritine())
|
"method": "owned_game_updated",
|
||||||
response = json.loads(write.call_args[0][0])
|
"params": {
|
||||||
|
"owned_game": {
|
||||||
assert response == {
|
"game_id": "3",
|
||||||
"jsonrpc": "2.0",
|
"game_title": "Doom",
|
||||||
"method": "owned_game_updated",
|
"license_info": {
|
||||||
"params": {
|
"license_type": "SinglePurchase"
|
||||||
"owned_game": {
|
}
|
||||||
"game_id": "3",
|
|
||||||
"game_title": "Doom",
|
|
||||||
"license_info": {
|
|
||||||
"license_type": "SinglePurchase"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
def assert_rpc_response(write, response_id, result=None):
|
def assert_rpc_response(write, response_id, result=None):
|
||||||
assert json.loads(write.call_args[0][0]) == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"id": str(response_id),
|
"jsonrpc": "2.0",
|
||||||
"result": result
|
"id": str(response_id),
|
||||||
}
|
"result": result
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def assert_rpc_request(write, method, params=None):
|
def assert_rpc_request(write, method, params=None):
|
||||||
assert json.loads(write.call_args[0][0]) == {
|
assert get_messages(write) == [
|
||||||
"jsonrpc": "2.0",
|
{
|
||||||
"method": method,
|
"jsonrpc": "2.0",
|
||||||
"params": {"data": params}
|
"method": method,
|
||||||
}
|
"params": {"data": params}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -28,7 +33,8 @@ def cache_data():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_cache(plugin, readline, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
|
async def test_initialize_cache(plugin, read, write, cache_data):
|
||||||
request_id = 3
|
request_id = 3
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -36,36 +42,32 @@ def test_initialize_cache(plugin, readline, write, cache_data):
|
|||||||
"method": "initialize_cache",
|
"method": "initialize_cache",
|
||||||
"params": {"data": cache_data}
|
"params": {"data": cache_data}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
def test_set_cache(plugin, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
async def runner():
|
async def test_set_cache(plugin, write, cache_data):
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
|
|
||||||
plugin.persistent_cache.update(cache_data)
|
plugin.persistent_cache.update(cache_data)
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", cache_data)
|
assert_rpc_request(write, "push_cache", cache_data)
|
||||||
assert cache_data == plugin.persistent_cache
|
assert cache_data == plugin.persistent_cache
|
||||||
|
|
||||||
asyncio.run(runner())
|
|
||||||
|
|
||||||
|
|
||||||
def test_clear_cache(plugin, write, cache_data):
|
@pytest.mark.asyncio
|
||||||
async def runner():
|
async def test_clear_cache(plugin, write, cache_data):
|
||||||
plugin._persistent_cache = cache_data
|
plugin._persistent_cache = cache_data
|
||||||
|
|
||||||
plugin.persistent_cache.clear()
|
plugin.persistent_cache.clear()
|
||||||
plugin.push_cache()
|
plugin.push_cache()
|
||||||
|
|
||||||
assert_rpc_request(write, "push_cache", {})
|
assert_rpc_request(write, "push_cache", {})
|
||||||
assert {} == plugin.persistent_cache
|
assert {} == plugin.persistent_cache
|
||||||
|
|
||||||
asyncio.run(runner())
|
|
||||||
|
|||||||
17
tests/test_shutdown_platform_client.py
Normal file
17
tests/test_shutdown_platform_client.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "shutdown_platform_client"
|
||||||
|
}
|
||||||
|
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
|
plugin.shutdown_platform_client.return_value = async_return_value(None)
|
||||||
|
await plugin.run()
|
||||||
|
plugin.shutdown_platform_client.assert_called_with()
|
||||||
46
tests/test_stream_line_reader.py
Normal file
46
tests/test_stream_line_reader.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from galaxy.reader import StreamLineReader
|
||||||
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def stream_line_reader(reader):
|
||||||
|
return StreamLineReader(reader)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_message(stream_line_reader, read):
|
||||||
|
read.return_value = async_return_value(b"a\n")
|
||||||
|
assert await stream_line_reader.readline() == b"a"
|
||||||
|
read.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_separate_messages(stream_line_reader, read):
|
||||||
|
read.side_effect = [async_return_value(b"a\n"), async_return_value(b"b\n")]
|
||||||
|
assert await stream_line_reader.readline() == b"a"
|
||||||
|
assert await stream_line_reader.readline() == b"b"
|
||||||
|
assert read.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_connected_messages(stream_line_reader, read):
|
||||||
|
read.return_value = async_return_value(b"a\nb\n")
|
||||||
|
assert await stream_line_reader.readline() == b"a"
|
||||||
|
assert await stream_line_reader.readline() == b"b"
|
||||||
|
read.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_cut_message(stream_line_reader, read):
|
||||||
|
read.side_effect = [async_return_value(b"a"), async_return_value(b"b\n")]
|
||||||
|
assert await stream_line_reader.readline() == b"ab"
|
||||||
|
assert read.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_half_message(stream_line_reader, read):
|
||||||
|
read.side_effect = [async_return_value(b"a"), async_return_value(b"")]
|
||||||
|
assert await stream_line_reader.readline() == b""
|
||||||
|
assert read.call_count == 2
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import asyncio
|
import pytest
|
||||||
import json
|
|
||||||
|
|
||||||
def test_success(plugin, readline):
|
from galaxy.unittest.mock import async_return_value
|
||||||
|
|
||||||
|
from tests import create_message
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_success(plugin, read):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "uninstall_game",
|
"method": "uninstall_game",
|
||||||
@@ -9,8 +13,7 @@ def test_success(plugin, readline):
|
|||||||
"game_id": "3"
|
"game_id": "3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_owned_games.return_value = None
|
plugin.get_owned_games.return_value = None
|
||||||
asyncio.run(plugin.run())
|
await plugin.run()
|
||||||
plugin.uninstall_game.assert_called_with(game_id="3")
|
plugin.uninstall_game.assert_called_with(game_id="3")
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
|
||||||
|
|
||||||
from galaxy.api.types import UserInfo, Presence
|
|
||||||
from galaxy.api.errors import UnknownError
|
|
||||||
from galaxy.api.consts import PresenceState
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "8",
|
|
||||||
"method": "import_user_infos",
|
|
||||||
"params": {
|
|
||||||
"user_id_list": ["13"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_users.coro.return_value = [
|
|
||||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_users.assert_called_with(user_id_list=["13"])
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "8",
|
|
||||||
"result": {
|
|
||||||
"user_info_list": [
|
|
||||||
{
|
|
||||||
"user_id": "5",
|
|
||||||
"is_friend": False,
|
|
||||||
"user_name": "Ula",
|
|
||||||
"avatar_url": "http://avatar.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "12",
|
|
||||||
"method": "import_user_infos",
|
|
||||||
"params": {
|
|
||||||
"user_id_list": ["10", "11", "12"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_users.coro.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "12",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user